Update OpenThread from upstream

Updated to April 25, 2024 version, commit
93f311376434605e03c796a7543e3f0289fd3acf

Bug: b/337107059
Change-Id: I418269582bad2d6cb19c750fed6a733d1f791904
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/openthread/+/1037211
Reviewed-by: Sundarajan Srinivasan <sundarsrini@google.com>
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 007430e..953788e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -53,7 +53,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -75,7 +75,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # v1
       with:
         use-verbose-mode: 'yes'
@@ -89,7 +89,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -108,7 +108,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -148,7 +148,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -167,7 +167,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -186,14 +186,14 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         repository: ARMmbed/mbedtls
         ref: v3.5.0
@@ -242,7 +242,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -283,7 +283,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -316,7 +316,7 @@
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
         with:
           submodules: true
       - name: Bootstrap
@@ -354,7 +354,7 @@
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
         with:
           submodules: true
       - name: Bootstrap
@@ -382,7 +382,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -418,7 +418,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -442,7 +442,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Install unzip
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 158892f..cc1f543 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -59,14 +59,14 @@
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
     - name: Checkout repository
-      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+      uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
 
     - 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@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
+      uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -80,6 +80,6 @@
         ./script/test build
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
+      uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
       with:
         category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index c336a2b..b145023 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -59,7 +59,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
 
@@ -83,7 +83,7 @@
           ${TAGS} --file ${DOCKER_FILE} ." >> $GITHUB_OUTPUT
 
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
+      uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3.2.0
 
     - name: Docker Buildx (build)
       run: |
@@ -91,7 +91,7 @@
 
     - name: Login to DockerHub
       if: success() && github.repository == 'openthread/openthread' && github.event_name != 'pull_request'
-      uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
+      uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
       with:
         username: ${{ secrets.DOCKER_USERNAME }}
         password: ${{ secrets.DOCKER_PASSWORD }}
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
index 97cb4b5..da612bb 100644
--- a/.github/workflows/fuzz.yml
+++ b/.github/workflows/fuzz.yml
@@ -61,7 +61,7 @@
        fuzz-seconds: 1800
        dry-run: false
    - name: Upload Crash
-     uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+     uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
      if: failure()
      with:
        name: artifacts
diff --git a/.github/workflows/makefile-check.yml b/.github/workflows/makefile-check.yml
index 83bce98..1d7fa55 100644
--- a/.github/workflows/makefile-check.yml
+++ b/.github/workflows/makefile-check.yml
@@ -52,7 +52,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Check
diff --git a/.github/workflows/otbr.yml b/.github/workflows/otbr.yml
index 4ba7ea6..ac04744 100644
--- a/.github/workflows/otbr.yml
+++ b/.github/workflows/otbr.yml
@@ -62,7 +62,7 @@
       # of OMR prefix and Domain prefix is not deterministic.
       BORDER_ROUTING: 0
     steps:
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Build OTBR Docker
@@ -86,12 +86,12 @@
         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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-thread-1-3-backbone-docker
         path: /tmp/coverage/
         retention-days: 1
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: thread-1-3-backbone-results
@@ -104,7 +104,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-thread-1-3-backbone
         path: tmp/coverage.info
@@ -181,7 +181,7 @@
       NAT64: ${{ matrix.nat64 }}
       MAX_JOBS: 3
     steps:
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Set firewall environment variables
       if: ${{ matrix.use_core_firewall }}
       run: |
@@ -208,12 +208,12 @@
         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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-br-docker-${{ matrix.description }}-${{ matrix.otbr_mdns }}-${{matrix.otbr_trel}}
         path: /tmp/coverage/
         retention-days: 1
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: br-results-${{ matrix.description }}-${{ matrix.otbr_mdns }}-${{matrix.otbr_trel}}
@@ -226,7 +226,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-br-${{ matrix.description }}-${{ matrix.otbr_mdns }}-${{matrix.otbr_trel}}
         path: tmp/coverage.info
@@ -238,13 +238,13 @@
     - thread-border-router
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+    - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
       with:
         path: coverage/
         pattern: cov-*
@@ -255,7 +255,7 @@
         script/test combine_coverage
     - name: Upload Coverage
       continue-on-error: true
-      uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+      uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
       with:
diff --git a/.github/workflows/otci.yml b/.github/workflows/otci.yml
index 37a0149..d22c227 100644
--- a/.github/workflows/otci.yml
+++ b/.github/workflows/otci.yml
@@ -61,7 +61,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
diff --git a/.github/workflows/otns.yml b/.github/workflows/otns.yml
index 4b46095..ac90a37 100644
--- a/.github/workflows/otns.yml
+++ b/.github/workflows/otns.yml
@@ -62,12 +62,12 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
       with:
         go-version: "1.20"
     - name: Set up Python
-      uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
+      uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
       with:
         python-version: "3.9"
     - name: Bootstrap
@@ -82,7 +82,7 @@
           cd /tmp/otns
           ./script/test py-unittests
         )
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: unittests-pcaps
@@ -92,7 +92,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-otns-unittests
         path: tmp/coverage.info
@@ -102,12 +102,12 @@
     name: Examples
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
         with:
           go-version: "1.20"
       - name: Set up Python
-        uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
+        uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
         with:
           python-version: "3.9"
       - name: Bootstrap
@@ -122,7 +122,7 @@
             cd /tmp/otns
             ./script/test py-examples
           )
-      - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+      - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
         if: ${{ failure() }}
         with:
           name: examples-pcaps
@@ -132,7 +132,7 @@
       - name: Generate Coverage
         run: |
           ./script/test generate_coverage gcc
-      - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+      - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
         with:
           name: cov-otns-examples
           path: tmp/coverage.info
@@ -164,12 +164,12 @@
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
         with:
           go-version: "1.20"
       - name: Set up Python
-        uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
+        uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
         with:
           python-version: "3.9"
       - name: Bootstrap
@@ -184,7 +184,7 @@
             cd /tmp/otns
             ./script/test stress-tests ${{ matrix.suite }}
           )
-      - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+      - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
         if: ${{ failure() }}
         with:
           name: stress-tests-${{ matrix.suite }}-pcaps
@@ -194,7 +194,7 @@
       - name: Generate Coverage
         run: |
           ./script/test generate_coverage gcc
-      - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+      - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
         with:
           name: cov-otns-stress-tests-${{ matrix.suite }}
           path: tmp/coverage.info
@@ -212,11 +212,11 @@
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       - name: Bootstrap
         run: |
           sudo apt-get --no-install-recommends install -y lcov
-      - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+      - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
         with:
           path: coverage/
           pattern: cov-*
diff --git a/.github/workflows/posix.yml b/.github/workflows/posix.yml
index 7ab0ef2..fd8e7d8 100644
--- a/.github/workflows/posix.yml
+++ b/.github/workflows/posix.yml
@@ -56,10 +56,11 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat
+        pip install bleak
     - name: Run RCP Mode
       run: |
         ulimit -c unlimited
@@ -75,7 +76,7 @@
           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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() && env.CRASHED_RCP == '1' }}
       with:
         name: core-expect-rcp
@@ -84,7 +85,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-expects-linux-1
         path: tmp/coverage.info
@@ -108,13 +109,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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() && env.CRASHED_TUN == '1' }}
       with:
         name: core-expect-linux
         path: |
           ./ot-core-dump/*
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: syslog-expect-linux
@@ -122,7 +123,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-expects-linux-2
         path: tmp/coverage.info
@@ -141,7 +142,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -155,7 +156,7 @@
     - name: Run
       run: |
         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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: thread-cert
@@ -163,7 +164,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-thread-cert
         path: tmp/coverage.info
@@ -185,7 +186,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
@@ -213,7 +214,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-pty-linux-${{ matrix.OT_DAEMON }}
         path: tmp/coverage.info
@@ -235,7 +236,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Bootstrap
       run: |
         rm -f /usr/local/bin/2to3
@@ -265,7 +266,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Bootstrap
       env:
         GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
@@ -281,7 +282,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-rcp-stack-reset
         path: tmp/coverage.info
@@ -299,13 +300,13 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+    - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
       with:
         path: coverage/
         pattern: cov-*
@@ -314,7 +315,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+      uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
       with:
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 5fd261b..a9a13cc 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -60,7 +60,7 @@
 
     steps:
       - name: "Checkout code"
-        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+        uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
         with:
           persist-credentials: false
 
@@ -87,7 +87,7 @@
       # 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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v3.1.0
+        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v3.1.0
         with:
           name: SARIF file
           path: results.sarif
@@ -95,6 +95,6 @@
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v2.1.27
+        uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # 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 9f64444..ee08f39 100644
--- a/.github/workflows/simulation-1.1.yml
+++ b/.github/workflows/simulation-1.1.yml
@@ -59,7 +59,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -76,7 +76,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: packet-verification-pcaps
@@ -86,7 +86,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-packet-verification
         path: tmp/coverage.info
@@ -108,7 +108,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -122,7 +122,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: cli-ftd-thread-cert
@@ -130,7 +130,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-cli-ftd
         path: tmp/coverage.info
@@ -159,7 +159,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -173,7 +173,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: cli-mtd-thread-cert
@@ -181,7 +181,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-cli-mtd-${{ matrix.message_use_heap }}
         path: tmp/coverage.info
@@ -203,7 +203,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -217,7 +217,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: cli-time-sync-thread-cert
@@ -225,7 +225,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-cli-time-sync
         path: tmp/coverage.info
@@ -243,10 +243,11 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat
+        pip install bleak
     - name: Run
       run: |
         ulimit -c unlimited
@@ -258,7 +259,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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() && env.CRASHED_CLI == '1' }}
       with:
         name: core-expect-cli
@@ -267,7 +268,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-expects
         path: tmp/coverage.info
@@ -283,7 +284,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -317,7 +318,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-ot-commissioner
         path: tmp/coverage.info
@@ -336,7 +337,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -349,7 +350,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: ot_testing
@@ -357,7 +358,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-multiple-instance
         path: tmp/coverage.info
@@ -379,13 +380,13 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+    - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
       with:
         path: coverage/
         pattern: cov-*
@@ -394,7 +395,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+      uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
       with:
diff --git a/.github/workflows/simulation-1.2.yml b/.github/workflows/simulation-1.2.yml
index 35a0f0d..fbd70f0 100644
--- a/.github/workflows/simulation-1.2.yml
+++ b/.github/workflows/simulation-1.2.yml
@@ -70,7 +70,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -95,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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: thread-1-3-${{ matrix.compiler.c }}-${{ matrix.arch }}-pcaps
         path: "*.pcap"
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-packet-verification-thread-1-3
@@ -109,7 +109,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage "${{ matrix.compiler.gcov }}"
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-thread-1-3-${{ matrix.compiler.c }}-${{ matrix.arch }}
         path: tmp/coverage.info
@@ -132,7 +132,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -166,14 +166,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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: packet-verification-low-power-pcaps
         path: |
           *.pcap
           *.json
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-packet-verification-low-power
@@ -182,7 +182,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-packet-verification-low-power
         path: tmp/coverage.info
@@ -203,7 +203,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -220,7 +220,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: packet-verification-1.1-on-1.3-pcaps
@@ -230,12 +230,62 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-packet-verification-1-1-on-1-3
         path: tmp/coverage.info
         retention-days: 1
 
+  channel-manager-csl:
+    runs-on: ubuntu-20.04
+    env:
+      CFLAGS: -m32
+      CXXFLAGS: -m32
+      LDFLAGS: -m32
+      COVERAGE: 1
+      THREAD_VERSION: 1.3
+      VIRTUAL_TIME: 1
+      INTER_OP: 1
+      INTER_OP_BBR: 1
+      ADDON_FEAT_1_2: 1
+    steps:
+    - name: Harden Runner
+      uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+      with:
+        egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
+
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
+      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 lcov ninja-build python3-setuptools python3-wheel
+        python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
+    - name: Build
+      run: |
+        OT_OPTIONS="-DOT_CHANNEL_MANAGER_CSL=ON" ./script/test build
+    - name: Run
+      run: |
+        ulimit -c unlimited
+        ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py
+        ./script/test cert_suite ./tests/scripts/thread-cert/test_*.py
+        ./script/test cert_suite ./tests/scripts/thread-cert/v1_2_*.py
+        ./script/test cert_suite ./tests/scripts/thread-cert/addon_test_channel_manager_autocsl*.py
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+      if: ${{ failure() }}
+      with:
+        name: channel-manager-csl
+        path: ot_testing
+    - name: Generate Coverage
+      run: |
+        ./script/test generate_coverage gcc
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+      with:
+        name: cov-channel-manager-csl
+        path: tmp/coverage.info
+        retention-days: 1
+
   expects:
     runs-on: ubuntu-20.04
     env:
@@ -248,12 +298,13 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat
+        pip install bleak
     - name: Run RCP Mode
       run: |
         ulimit -c unlimited
@@ -265,7 +316,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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-expect-1-3
@@ -274,7 +325,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-expects
         path: tmp/coverage.info
@@ -297,7 +348,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -324,12 +375,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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() }}
       with:
         name: thread-1-3-posix-pcaps
         path: "*.pcap"
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-thread-1-3-posix
@@ -338,7 +389,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-thread-1-3-posix
         path: tmp/coverage.info
@@ -358,13 +409,13 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+    - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
       with:
         path: coverage/
         pattern: cov-*
@@ -373,7 +424,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+      uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
       with:
diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml
index 7b0358b..98de9d7 100644
--- a/.github/workflows/size.yml
+++ b/.github/workflows/size.yml
@@ -53,7 +53,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
     - name: Run
       env:
         OT_BASE_BRANCH: "${{ github.base_ref }}"
diff --git a/.github/workflows/toranj.yml b/.github/workflows/toranj.yml
index 29f2d8b..6b79b13 100644
--- a/.github/workflows/toranj.yml
+++ b/.github/workflows/toranj.yml
@@ -63,7 +63,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -94,7 +94,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -111,7 +111,7 @@
       if: "matrix.TORANJ_RADIO != 'multi'"
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       if: "matrix.TORANJ_RADIO != 'multi'"
       with:
         name: cov-toranj-cli-${{ matrix.TORANJ_RADIO }}
@@ -127,7 +127,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -159,6 +159,28 @@
         git clean -dfx
         ./tests/toranj/build.sh --enable-plat-key-ref all
 
+  toranj-macos:
+    name: toranj-macos
+    runs-on: macos-14
+    steps:
+    - name: Harden Runner
+      uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+      with:
+        egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
+
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
+      with:
+        submodules: true
+    - name: Bootstrap
+      env:
+        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+      run: |
+        brew update
+        brew install ninja
+    - name: Build & Run
+      run: |
+        ./tests/toranj/build.sh posix-15.4
+
   upload-coverage:
     needs:
     - toranj-cli
@@ -169,13 +191,13 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+    - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
       with:
         path: coverage/
         pattern: cov-*
@@ -184,7 +206,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+      uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
       with:
diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml
index 672aac3..bf685ca 100644
--- a/.github/workflows/unit.yml
+++ b/.github/workflows/unit.yml
@@ -53,7 +53,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Build
@@ -71,7 +71,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
@@ -93,7 +93,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+    - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
       with:
         name: cov-unit-tests
         path: tmp/coverage.info
@@ -108,13 +108,13 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+    - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
       with:
         path: coverage/
         pattern: cov-*
@@ -123,7 +123,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+      uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
       with:
diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml
index 5b6d3e7..16dc637 100644
--- a/.github/workflows/version.yml
+++ b/.github/workflows/version.yml
@@ -49,7 +49,7 @@
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
-    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+    - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
       with:
         submodules: true
     - name: Check
diff --git a/doc/ot_api_doc.h b/doc/ot_api_doc.h
index ac6ec5a..2513e09 100644
--- a/doc/ot_api_doc.h
+++ b/doc/ot_api_doc.h
@@ -58,6 +58,7 @@
  * @defgroup api-dnssd-server         DNS-SD Server
  * @defgroup api-icmp6                ICMPv6
  * @defgroup api-ip6                  IPv6
+ * @defgroup api-mdns                 Multicast DNS
  * @defgroup api-nat64                NAT64
  * @defgroup api-srp                  SRP
  * @defgroup api-ping-sender          Ping Sender
@@ -154,6 +155,7 @@
  * @}
  *
  * @defgroup api-sntp                 SNTP
+ * @defgroup api-verhoeff-checksum    Verhoeff Checksum
  *
  * @}
  *
@@ -180,6 +182,7 @@
  * @defgroup plat-memory              Memory
  * @defgroup plat-messagepool         Message Pool
  * @defgroup plat-misc                Miscellaneous
+ * @defgroup plat-mdns                Multicast DNS
  * @defgroup plat-multipan            Multipan
  * @defgroup plat-otns                Network Simulator
  * @defgroup plat-radio               Radio
diff --git a/doc/ot_config_doc.h b/doc/ot_config_doc.h
index 564950b..c61b92e 100644
--- a/doc/ot_config_doc.h
+++ b/doc/ot_config_doc.h
@@ -47,15 +47,15 @@
  * @defgroup config-channel-manager          Channel Manager
  * @defgroup config-channel-monitor          Channel Monitor
  * @defgroup config-child-supervision        Child Supervision
- * @defgroup config-coap         	     CoAP
- * @defgroup config-commissioner	     Commissioner
+ * @defgroup config-coap                     CoAP
+ * @defgroup config-commissioner             Commissioner
  * @defgroup config-crypto                   Crypto Backend Library
  * @defgroup config-dataset-updater          Dataset Updater
  * @defgroup config-dhcpv6-client            DHCPv6 Client
  * @defgroup config-dhcpv6-server            DHCPv6 Server
  * @defgroup config-diag                     DIAG Service
- * @defgroup config-dns-client		     DNS Client
- * @defgroup config-dns-dso	             DNS Stateful Operations
+ * @defgroup config-dns-client               DNS Client
+ * @defgroup config-dns-dso                  DNS Stateful Operations
  * @defgroup config-dnssd-server             DNS-SD Server
  * @defgroup config-history-tracker          History Tracker
  * @defgroup config-ip6                      IP6 Service
@@ -69,6 +69,7 @@
  * @defgroup config-mesh-forwarder           Mesh Forwarder
  * @defgroup config-misc                     Miscellaneous Constants
  * @defgroup config-mle                      MLE Service
+ * @defgroup config-mdns                     Multicast DNS
  * @defgroup config-nat64                    NAT64
  * @defgroup config-netdata-publisher        Network Data Publisher
  * @defgroup config-network-diagnostic       Network Diagnostics
@@ -83,7 +84,7 @@
  * @defgroup config-srp-server               SRP Server
  * @defgroup config-time-sync                Time Sync Service
  * @defgroup config-tmf                      Thread Management Framework Service
- * @defgroup config-trel		     TREL
+ * @defgroup config-trel                     TREL
  *
  * @}
  *
diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake
index bad9193..eb41ec8 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -174,12 +174,14 @@
 ot_option(OT_BACKBONE_ROUTER_MULTICAST_ROUTING OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE "BBR MR")
 ot_option(OT_BLE_TCAT OPENTHREAD_CONFIG_BLE_TCAT_ENABLE "Ble based thread commissioning")
 ot_option(OT_BORDER_AGENT OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE "border agent")
+ot_option(OT_BORDER_AGENT_EPSKC OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE "border agent ephemeral PSKc")
 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_DHCP6_PD OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE "dhcpv6 pd support in 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_MANAGER_CSL OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE "channel manager for csl channel")
 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)")
@@ -214,6 +216,7 @@
 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_MDNS OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE "multicast DNS (mDNS)")
 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)")
@@ -248,6 +251,7 @@
 ot_option(OT_TX_QUEUE_STATS OPENTHREAD_CONFIG_TX_QUEUE_STATISTICS_ENABLE "tx queue statistics")
 ot_option(OT_UDP_FORWARD OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE "UDP forward")
 ot_option(OT_UPTIME OPENTHREAD_CONFIG_UPTIME_ENABLE "uptime")
+ot_option(OT_VERHOEFF_CHECKSUM OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE "verhoeff checksum")
 
 option(OT_DOC "build OpenThread documentation")
 message(STATUS "- - - - - - - - - - - - - - - - ")
@@ -278,7 +282,7 @@
 endif()
 
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-set(OT_THREAD_VERSION_VALUES "1.1" "1.2" "1.3" "1.3.1")
+set(OT_THREAD_VERSION_VALUES "1.1" "1.2" "1.3" "1.3.1" "1.4")
 set(OT_THREAD_VERSION "1.3" CACHE STRING "set Thread version")
 set_property(CACHE OT_THREAD_VERSION PROPERTY STRINGS "${OT_THREAD_VERSION_VALUES}")
 list(FIND OT_THREAD_VERSION_VALUES "${OT_THREAD_VERSION}" ot_index)
@@ -286,7 +290,7 @@
     message(STATUS "OT_THREAD_VERSION=\"${OT_THREAD_VERSION}\"")
     message(FATAL_ERROR "Invalid value for OT_THREAD_VERSION - valid values are: " "${OT_THREAD_VERSION_VALUES}")
 endif()
-set(OT_VERSION_SUFFIX_LIST "1_1" "1_2" "1_3" "1_3_1")
+set(OT_VERSION_SUFFIX_LIST "1_1" "1_2" "1_3" "1_3_1" "1_4")
 list(GET OT_VERSION_SUFFIX_LIST ${ot_index} OT_VERSION_SUFFIX)
 target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_${OT_VERSION_SUFFIX}")
 message(STATUS "OT_THREAD_VERSION=\"${OT_THREAD_VERSION}\" -> OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_${OT_VERSION_SUFFIX}")
diff --git a/examples/config/ot-core-config-check-size-br.h b/examples/config/ot-core-config-check-size-br.h
index 8310019..d8b2b13 100644
--- a/examples/config/ot-core-config-check-size-br.h
+++ b/examples/config/ot-core-config-check-size-br.h
@@ -40,6 +40,7 @@
 #define OPENTHREAD_CONFIG_ASSERT_ENABLE 1
 #define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 1
+#define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 1
@@ -78,6 +79,7 @@
 #define OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE 1
 #define OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE 1
 #define OPENTHREAD_CONFIG_MLR_ENABLE 1
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE 1
 #define OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE 0
 #define OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE 1
 #define OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE 1
@@ -98,5 +100,6 @@
 #define OPENTHREAD_CONFIG_DNS_DSO_MOCK_PLAT_APIS_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_MOCK_PLAT_APIS_ENABLE 1
 #define OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_MOCK_PLAT_APIS_ENABLE 1
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE 1
 
 #endif // OT_CORE_CONFIG_CHECK_SIZE_BR_H_
diff --git a/examples/config/ot-core-config-check-size-ftd.h b/examples/config/ot-core-config-check-size-ftd.h
index 63e7ad4..bdd69d0 100644
--- a/examples/config/ot-core-config-check-size-ftd.h
+++ b/examples/config/ot-core-config-check-size-ftd.h
@@ -41,6 +41,7 @@
 #define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1
+#define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 0
diff --git a/examples/config/ot-core-config-check-size-mtd.h b/examples/config/ot-core-config-check-size-mtd.h
index 88b2c11..d5ca74c 100644
--- a/examples/config/ot-core-config-check-size-mtd.h
+++ b/examples/config/ot-core-config-check-size-mtd.h
@@ -41,6 +41,7 @@
 #define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 0
+#define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 0
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 0
diff --git a/include/ble.c b/include/ble.c
index 2fe3c64..d48922a 100644
--- a/include/ble.c
+++ b/include/ble.c
@@ -26,50 +26,191 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
+#include "platform-simulation.h"
+
+#include <errno.h>
+
+#include <stdio.h>
+#include <stdlib.h>
 #include <openthread/platform/ble.h>
 
+#include "openthread/error.h"
+#include "utils/code_utils.h"
+
+#define PLAT_BLE_MSG_DATA_MAX 2048
+static uint8_t sBleBuffer[PLAT_BLE_MSG_DATA_MAX];
+
+static int sFd = -1;
+
+static const uint16_t kPortBase = 10000;
+static uint16_t       sPort     = 0;
+struct sockaddr_in    sSockaddr;
+
+static void initFds(void)
+{
+    int                fd;
+    int                one = 1;
+    struct sockaddr_in sockaddr;
+
+    memset(&sockaddr, 0, sizeof(sockaddr));
+
+    sPort                    = (uint16_t)(kPortBase + gNodeId);
+    sockaddr.sin_family      = AF_INET;
+    sockaddr.sin_port        = htons(sPort);
+    sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
+
+    otEXPECT_ACTION((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1, perror("socket(sFd)"));
+
+    otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) != -1,
+                    perror("setsockopt(sFd, SO_REUSEADDR)"));
+    otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) != -1,
+                    perror("setsockopt(sFd, SO_REUSEPORT)"));
+
+    otEXPECT_ACTION(bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != -1, perror("bind(sFd)"));
+
+    // Fd is successfully initialized.
+    sFd = fd;
+
+exit:
+    if (sFd == -1)
+    {
+        exit(EXIT_FAILURE);
+    }
+}
+
+static void deinitFds(void)
+{
+    if (sFd != -1)
+    {
+        close(sFd);
+        sFd = -1;
+    }
+}
+
 otError otPlatBleEnable(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    initFds();
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleDisable(otInstance *aInstance)
 {
+    deinitFds();
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGapAdvStart(otInstance *aInstance, uint16_t aInterval)
 {
     OT_UNUSED_VARIABLE(aInstance);
     OT_UNUSED_VARIABLE(aInterval);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGapAdvStop(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGapDisconnect(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGattMtuGet(otInstance *aInstance, uint16_t *aMtu)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    OT_UNUSED_VARIABLE(aMtu);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    *aMtu = PLAT_BLE_MSG_DATA_MAX - 1;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGattServerIndicate(otInstance *aInstance, uint16_t aHandle, const otBleRadioPacket *aPacket)
 {
     OT_UNUSED_VARIABLE(aInstance);
     OT_UNUSED_VARIABLE(aHandle);
+
+    ssize_t rval;
+    otError error = OT_ERROR_NONE;
+
+    otEXPECT_ACTION(sFd != -1, error = OT_ERROR_INVALID_STATE);
+    rval = sendto(sFd, (const char *)aPacket->mValue, aPacket->mLength, 0, (struct sockaddr *)&sSockaddr,
+                  sizeof(sSockaddr));
+    if (rval == -1)
+    {
+        perror("BLE simulation sendto failed.");
+    }
+
+exit:
+    return error;
+}
+
+void platformBleDeinit(void) { deinitFds(); }
+
+void platformBleUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, struct timeval *aTimeout, int *aMaxFd)
+{
+    OT_UNUSED_VARIABLE(aTimeout);
+    OT_UNUSED_VARIABLE(aWriteFdSet);
+
+    if (aReadFdSet != NULL && sFd != -1)
+    {
+        FD_SET(sFd, aReadFdSet);
+
+        if (aMaxFd != NULL && *aMaxFd < sFd)
+        {
+            *aMaxFd = sFd;
+        }
+    }
+}
+
+void platformBleProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
+{
+    OT_UNUSED_VARIABLE(aWriteFdSet);
+
+    otEXPECT(sFd != -1);
+
+    if (FD_ISSET(sFd, aReadFdSet))
+    {
+        socklen_t len = sizeof(sSockaddr);
+        ssize_t   rval;
+        memset(&sSockaddr, 0, sizeof(sSockaddr));
+        rval = recvfrom(sFd, sBleBuffer, sizeof(sBleBuffer), 0, (struct sockaddr *)&sSockaddr, &len);
+        if (rval > 0)
+        {
+            otBleRadioPacket myPacket;
+            myPacket.mValue  = sBleBuffer;
+            myPacket.mLength = (uint16_t)rval;
+            myPacket.mPower  = 0;
+            otPlatBleGattServerOnWriteRequest(
+                aInstance, 0,
+                &myPacket); // TODO consider passing otPlatBleGattServerOnWriteRequest as a callback function
+        }
+        else if (rval == 0)
+        {
+            // socket is closed, which should not happen
+            assert(false);
+        }
+        else if (errno != EINTR && errno != EAGAIN)
+        {
+            perror("recvfrom BLE simulation failed");
+            exit(EXIT_FAILURE);
+        }
+    }
+exit:
+    return;
+}
+
+OT_TOOL_WEAK void otPlatBleGattServerOnWriteRequest(otInstance             *aInstance,
+                                                    uint16_t                aHandle,
+                                                    const otBleRadioPacket *aPacket)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aHandle);
     OT_UNUSED_VARIABLE(aPacket);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    assert(false);
+    /* In case of rcp there is a problem with linking to otPlatBleGattServerOnWriteRequest
+     * which is available in FTD/MTD library.
+     */
 }
diff --git a/include/mdns_socket.c b/include/mdns_socket.c
new file mode 100644
index 0000000..88abcb5
--- /dev/null
+++ b/include/mdns_socket.c
@@ -0,0 +1,569 @@
+/*
+ *  Copyright (c) 2024, 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 "platform-simulation.h"
+
+#include <openthread/nat64.h>
+#include <openthread/platform/mdns_socket.h>
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+//---------------------------------------------------------------------------------------------------------------------
+#if OPENTHREAD_SIMULATION_MDNS_SOCKET_IMPLEMENT_POSIX
+
+// Provide a simplified POSIX based implementation of `otPlatMdns`
+// platform APIs. This is intended for testing.
+
+#include <openthread/ip6.h>
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include "simul_utils.h"
+#include "utils/code_utils.h"
+
+#define MAX_BUFFER_SIZE 1600
+
+#define MDNS_PORT 5353
+
+static bool     sEnabled = false;
+static uint32_t sInfraIfIndex;
+static int      sMdnsFd4 = -1;
+static int      sMdnsFd6 = -1;
+
+/* this is a portability hack */
+#ifndef IPV6_ADD_MEMBERSHIP
+#ifdef IPV6_JOIN_GROUP
+#define IPV6_ADD_MEMBERSHIP IPV6_JOIN_GROUP
+#endif
+#endif
+
+#ifndef IPV6_DROP_MEMBERSHIP
+#ifdef IPV6_LEAVE_GROUP
+#define IPV6_DROP_MEMBERSHIP IPV6_LEAVE_GROUP
+#endif
+#endif
+
+#define VerifyOrDie(aCondition, aErrMsg)                                        \
+    do                                                                          \
+    {                                                                           \
+        if (!(aCondition))                                                      \
+        {                                                                       \
+            fprintf(stderr, "\n\r" aErrMsg ". errono:%s\n\r", strerror(errno)); \
+            exit(1);                                                            \
+        }                                                                       \
+    } while (false)
+
+static void SetReuseAddrPort(int aFd)
+{
+    int ret;
+    int yes = 1;
+
+    ret = setsockopt(aFd, SOL_SOCKET, SO_REUSEADDR, (char *)&yes, sizeof(yes));
+    VerifyOrDie(ret >= 0, "setsocketopt(SO_REUSEADDR) failed");
+
+    ret = setsockopt(aFd, SOL_SOCKET, SO_REUSEPORT, (char *)&yes, sizeof(yes));
+    VerifyOrDie(ret >= 0, "setsocketopt(SO_REUSEPORT) failed");
+}
+
+static void OpenIp4Socket(uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    struct sockaddr_in addr;
+    int                fd;
+    int                ret;
+    uint8_t            u8;
+    int                value;
+
+    fd = socket(AF_INET, SOCK_DGRAM, 0);
+    VerifyOrDie(fd >= 0, "socket() failed");
+
+#ifdef __linux__
+    {
+        char        nameBuffer[IF_NAMESIZE];
+        const char *ifname;
+
+        ifname = if_indextoname(aInfraIfIndex, nameBuffer);
+        VerifyOrDie(ifname != NULL, "if_indextoname() failed");
+
+        ret = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname));
+        VerifyOrDie(ret >= 0, "setsocketopt(SO_BINDTODEVICE) failed");
+    }
+#else
+    value = aInfraIfIndex;
+    ret   = setsockopt(fd, IPPROTO_IP, IP_BOUND_IF, &value, sizeof(value));
+#endif
+
+    u8  = 255;
+    ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &u8, sizeof(u8));
+    VerifyOrDie(ret >= 0, "setsocketopt(IP_MULTICAST_TTL) failed");
+
+    value = 255;
+    ret   = setsockopt(fd, IPPROTO_IP, IP_TTL, &value, sizeof(value));
+    VerifyOrDie(ret >= 0, "setsocketopt(IP_TTL) failed");
+
+    u8  = 1;
+    ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &u8, sizeof(u8));
+    VerifyOrDie(ret >= 0, "setsocketopt(IP_MULTICAST_LOOP) failed");
+
+    SetReuseAddrPort(fd);
+
+    {
+        struct ip_mreqn mreqn;
+
+        memset(&mreqn, 0, sizeof(mreqn));
+        mreqn.imr_multiaddr.s_addr = inet_addr("224.0.0.251");
+        mreqn.imr_ifindex          = aInfraIfIndex;
+
+        ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &mreqn, sizeof(mreqn));
+        VerifyOrDie(ret >= 0, "setsocketopt(IP_MULTICAST_IF) failed");
+    }
+
+    memset(&addr, 0, sizeof(addr));
+    addr.sin_family      = AF_INET;
+    addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    addr.sin_port        = htons(MDNS_PORT);
+
+    ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
+    VerifyOrDie(ret >= 0, "bind() failed");
+
+    sMdnsFd4 = fd;
+}
+
+static void JoinOrLeaveIp4MulticastGroup(bool aJoin, uint32_t aInfraIfIndex)
+{
+    struct ip_mreqn mreqn;
+    int             ret;
+
+    memset(&mreqn, 0, sizeof(mreqn));
+    mreqn.imr_multiaddr.s_addr = inet_addr("224.0.0.251");
+    mreqn.imr_ifindex          = aInfraIfIndex;
+
+    if (aJoin)
+    {
+        // Suggested workaround for netif not dropping
+        // a previous multicast membership.
+        setsockopt(sMdnsFd4, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreqn, sizeof(mreqn));
+    }
+
+    ret = setsockopt(sMdnsFd4, IPPROTO_IP, aJoin ? IP_ADD_MEMBERSHIP : IP_DROP_MEMBERSHIP, &mreqn, sizeof(mreqn));
+    VerifyOrDie(ret >= 0, "setsocketopt(IP_ADD/DROP_MEMBERSHIP) failed");
+}
+
+static void OpenIp6Socket(uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    struct sockaddr_in6 addr6;
+    int                 fd;
+    int                 ret;
+    int                 value;
+
+    fd = socket(AF_INET6, SOCK_DGRAM, 0);
+    VerifyOrDie(fd >= 0, "socket() failed");
+
+#ifdef __linux__
+    {
+        char        nameBuffer[IF_NAMESIZE];
+        const char *ifname;
+
+        ifname = if_indextoname(aInfraIfIndex, nameBuffer);
+        VerifyOrDie(ifname != NULL, "if_indextoname() failed");
+
+        ret = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname));
+        VerifyOrDie(ret >= 0, "setsocketopt(SO_BINDTODEVICE) failed");
+    }
+#else
+    value = aInfraIfIndex;
+    ret   = setsockopt(fd, IPPROTO_IPV6, IPV6_BOUND_IF, &value, sizeof(value));
+#endif
+
+    value = 255;
+    ret   = setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &value, sizeof(value));
+    VerifyOrDie(ret >= 0, "setsocketopt(IPV6_MULTICAST_HOPS) failed");
+
+    value = 255;
+    ret   = setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &value, sizeof(value));
+    VerifyOrDie(ret >= 0, "setsocketopt(IPV6_UNICAST_HOPS) failed");
+
+    value = 1;
+    ret   = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &value, sizeof(value));
+    VerifyOrDie(ret >= 0, "setsocketopt(IPV6_V6ONLY) failed");
+
+    value = aInfraIfIndex;
+    ret   = setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &value, sizeof(value));
+    VerifyOrDie(ret >= 0, "setsocketopt(IPV6_MULTICAST_IF) failed");
+
+    value = 1;
+    ret   = setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &value, sizeof(value));
+    VerifyOrDie(ret >= 0, "setsocketopt(IPV6_MULTICAST_LOOP) failed");
+
+    SetReuseAddrPort(fd);
+
+    memset(&addr6, 0, sizeof(addr6));
+    addr6.sin6_family = AF_INET6;
+    addr6.sin6_port   = htons(MDNS_PORT);
+
+    ret = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6));
+    VerifyOrDie(ret >= 0, "bind() failed");
+
+    sMdnsFd6 = fd;
+}
+
+static void JoinOrLeaveIp6MulticastGroup(bool aJoin, uint32_t aInfraIfIndex)
+{
+    struct ipv6_mreq mreq6;
+    int              ret;
+
+    memset(&mreq6, 0, sizeof(mreq6));
+
+    inet_pton(AF_INET6, "ff02::fb", &mreq6.ipv6mr_multiaddr);
+    mreq6.ipv6mr_interface = (int)aInfraIfIndex;
+
+    if (aJoin)
+    {
+        // Suggested workaround for netif not dropping
+        // a previous multicast membership.
+        setsockopt(sMdnsFd6, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, &mreq6, sizeof(mreq6));
+    }
+
+    ret = setsockopt(sMdnsFd6, IPPROTO_IPV6, aJoin ? IPV6_ADD_MEMBERSHIP : IPV6_DROP_MEMBERSHIP, &mreq6, sizeof(mreq6));
+    VerifyOrDie(ret >= 0, "setsocketopt(IP6_ADD/DROP_MEMBERSHIP) failed");
+}
+
+otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    if (aEnable)
+    {
+        otEXPECT(!sEnabled);
+
+        OpenIp4Socket(aInfraIfIndex);
+        JoinOrLeaveIp4MulticastGroup(/* aJoin */ true, aInfraIfIndex);
+        OpenIp6Socket(aInfraIfIndex);
+        JoinOrLeaveIp6MulticastGroup(/* aJoin */ true, aInfraIfIndex);
+
+        sEnabled      = true;
+        sInfraIfIndex = aInfraIfIndex;
+    }
+    else
+    {
+        otEXPECT(sEnabled);
+
+        JoinOrLeaveIp4MulticastGroup(/* aJoin */ false, aInfraIfIndex);
+        JoinOrLeaveIp6MulticastGroup(/* aJoin */ false, aInfraIfIndex);
+        close(sMdnsFd4);
+        close(sMdnsFd6);
+        sEnabled = false;
+    }
+
+exit:
+    return OT_ERROR_NONE;
+}
+
+void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    uint8_t  buffer[MAX_BUFFER_SIZE];
+    uint16_t length;
+    int      bytes;
+
+    otEXPECT(sEnabled);
+
+    length = otMessageRead(aMessage, 0, buffer, sizeof(buffer));
+    otMessageFree(aMessage);
+
+    {
+        struct sockaddr_in addr;
+
+        memset(&addr, 0, sizeof(addr));
+        addr.sin_family      = AF_INET;
+        addr.sin_addr.s_addr = inet_addr("224.0.0.251");
+        addr.sin_port        = htons(MDNS_PORT);
+
+        bytes = sendto(sMdnsFd4, buffer, length, 0, (struct sockaddr *)&addr, sizeof(addr));
+
+        VerifyOrDie((bytes == length), "sendTo(sMdnsFd4) failed");
+    }
+
+    {
+        struct sockaddr_in6 addr6;
+
+        memset(&addr6, 0, sizeof(addr6));
+        addr6.sin6_family = AF_INET6;
+        addr6.sin6_port   = htons(MDNS_PORT);
+        inet_pton(AF_INET6, "ff02::fb", &addr6.sin6_addr);
+
+        bytes = sendto(sMdnsFd6, buffer, length, 0, (struct sockaddr *)&addr6, sizeof(addr6));
+
+        VerifyOrDie((bytes == length), "sendTo(sMdnsFd6) failed");
+    }
+
+exit:
+    return;
+}
+
+void otPlatMdnsSendUnicast(otInstance *aInstance, otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    otIp4Address ip4Addr;
+    uint8_t      buffer[MAX_BUFFER_SIZE];
+    uint16_t     length;
+    int          bytes;
+
+    otEXPECT(sEnabled);
+
+    length = otMessageRead(aMessage, 0, buffer, sizeof(buffer));
+    otMessageFree(aMessage);
+
+    if (otIp4FromIp4MappedIp6Address(&aAddress->mAddress, &ip4Addr) == OT_ERROR_NONE)
+    {
+        struct sockaddr_in addr;
+
+        memset(&addr, 0, sizeof(addr));
+        addr.sin_family = AF_INET;
+        memcpy(&addr.sin_addr.s_addr, &ip4Addr, sizeof(otIp4Address));
+        addr.sin_port = htons(MDNS_PORT);
+
+        bytes = sendto(sMdnsFd4, buffer, length, 0, (struct sockaddr *)&addr, sizeof(addr));
+
+        VerifyOrDie((bytes == length), "sendTo(sMdnsFd4) failed");
+    }
+    else
+    {
+        struct sockaddr_in6 addr6;
+
+        memset(&addr6, 0, sizeof(addr6));
+        addr6.sin6_family = AF_INET6;
+        addr6.sin6_port   = htons(MDNS_PORT);
+        memcpy(&addr6.sin6_addr, &aAddress->mAddress, sizeof(otIp6Address));
+
+        bytes = sendto(sMdnsFd6, buffer, length, 0, (struct sockaddr *)&addr6, sizeof(addr6));
+
+        VerifyOrDie((bytes == length), "sendTo(sMdnsFd6) failed");
+    }
+
+exit:
+    return;
+}
+
+void platformMdnsSocketUpdateFdSet(fd_set *aReadFdSet, int *aMaxFd)
+{
+    otEXPECT(sEnabled);
+
+    utilsAddFdToFdSet(sMdnsFd4, aReadFdSet, aMaxFd);
+    utilsAddFdToFdSet(sMdnsFd6, aReadFdSet, aMaxFd);
+
+exit:
+    return;
+}
+
+void platformMdnsSocketProcess(otInstance *aInstance, const fd_set *aReadFdSet)
+{
+    otEXPECT(sEnabled);
+
+    if (FD_ISSET(sMdnsFd4, aReadFdSet))
+    {
+        uint8_t               buffer[MAX_BUFFER_SIZE];
+        struct sockaddr_in    sockaddr;
+        otPlatMdnsAddressInfo addrInfo;
+        otMessage            *message;
+        socklen_t             len = sizeof(sockaddr);
+        ssize_t               rval;
+
+        memset(&sockaddr, 0, sizeof(sockaddr));
+        rval = recvfrom(sMdnsFd4, (char *)&buffer, sizeof(buffer), 0, (struct sockaddr *)&sockaddr, &len);
+
+        VerifyOrDie(rval >= 0, "recvfrom() failed");
+
+        message = otIp6NewMessage(aInstance, NULL);
+        VerifyOrDie(message != NULL, "otIp6NewMessage() failed");
+
+        VerifyOrDie(otMessageAppend(message, buffer, (uint16_t)rval) == OT_ERROR_NONE, "otMessageAppend() failed");
+
+        memset(&addrInfo, 0, sizeof(addrInfo));
+        otIp4ToIp4MappedIp6Address((otIp4Address *)(&sockaddr.sin_addr.s_addr), &addrInfo.mAddress);
+        addrInfo.mPort         = MDNS_PORT;
+        addrInfo.mInfraIfIndex = sInfraIfIndex;
+
+        otPlatMdnsHandleReceive(aInstance, message, /* aInUnicast */ false, &addrInfo);
+    }
+
+    if (FD_ISSET(sMdnsFd6, aReadFdSet))
+    {
+        uint8_t               buffer[MAX_BUFFER_SIZE];
+        struct sockaddr_in6   sockaddr6;
+        otPlatMdnsAddressInfo addrInfo;
+        otMessage            *message;
+        socklen_t             len = sizeof(sockaddr6);
+        ssize_t               rval;
+
+        memset(&sockaddr6, 0, sizeof(sockaddr6));
+        rval = recvfrom(sMdnsFd6, (char *)&buffer, sizeof(buffer), 0, (struct sockaddr *)&sockaddr6, &len);
+        VerifyOrDie(rval >= 0, "recvfrom(sMdnsFd6) failed");
+
+        message = otIp6NewMessage(aInstance, NULL);
+        VerifyOrDie(message != NULL, "otIp6NewMessage() failed");
+
+        VerifyOrDie(otMessageAppend(message, buffer, (uint16_t)rval) == OT_ERROR_NONE, "otMessageAppend() failed");
+
+        memset(&addrInfo, 0, sizeof(addrInfo));
+        memcpy(&addrInfo.mAddress, &sockaddr6.sin6_addr, sizeof(otIp6Address));
+        addrInfo.mPort         = MDNS_PORT;
+        addrInfo.mInfraIfIndex = sInfraIfIndex;
+
+        otPlatMdnsHandleReceive(aInstance, message, /* aInUnicast */ false, &addrInfo);
+    }
+
+exit:
+    return;
+}
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Add weak implementation of `ot` APIs for RCP build. Note that
+// `simulation` platform does not get `OPENTHREAD_RADIO` config)
+
+OT_TOOL_WEAK uint16_t otMessageRead(const otMessage *aMessage, uint16_t aOffset, void *aBuf, uint16_t aLength)
+{
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aOffset);
+    OT_UNUSED_VARIABLE(aBuf);
+    OT_UNUSED_VARIABLE(aLength);
+
+    fprintf(stderr, "\n\rWeak otMessageRead() is incorrectly used\n\r");
+    exit(1);
+
+    return 0;
+}
+
+OT_TOOL_WEAK void otMessageFree(otMessage *aMessage)
+{
+    OT_UNUSED_VARIABLE(aMessage);
+    fprintf(stderr, "\n\rWeak otMessageFree() is incorrectly used\n\r");
+    exit(1);
+}
+
+OT_TOOL_WEAK otMessage *otIp6NewMessage(otInstance *aInstance, const otMessageSettings *aSettings)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aSettings);
+
+    fprintf(stderr, "\n\rWeak otIp6NewMessage() is incorrectly used\n\r");
+    exit(1);
+
+    return NULL;
+}
+
+OT_TOOL_WEAK otError otMessageAppend(otMessage *aMessage, const void *aBuf, uint16_t aLength)
+{
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aBuf);
+    OT_UNUSED_VARIABLE(aLength);
+
+    fprintf(stderr, "\n\rWeak otMessageFree() is incorrectly used\n\r");
+    exit(1);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK void otIp4ToIp4MappedIp6Address(const otIp4Address *aIp4Address, otIp6Address *aIp6Address)
+{
+    OT_UNUSED_VARIABLE(aIp4Address);
+    OT_UNUSED_VARIABLE(aIp6Address);
+
+    fprintf(stderr, "\n\rWeak otIp4ToIp4MappedIp6Address() is incorrectly used\n\r");
+    exit(1);
+}
+
+OT_TOOL_WEAK otError otIp4FromIp4MappedIp6Address(const otIp6Address *aIp6Address, otIp4Address *aIp4Address)
+{
+    OT_UNUSED_VARIABLE(aIp6Address);
+    OT_UNUSED_VARIABLE(aIp4Address);
+
+    fprintf(stderr, "\n\rWeak otIp4FromIp4MappedIp6Address() is incorrectly used\n\r");
+    exit(1);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK void otPlatMdnsHandleReceive(otInstance                  *aInstance,
+                                          otMessage                   *aMessage,
+                                          bool                         aIsUnicast,
+                                          const otPlatMdnsAddressInfo *aAddress)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aIsUnicast);
+    OT_UNUSED_VARIABLE(aAddress);
+
+    fprintf(stderr, "\n\rWeak otPlatMdnsHandleReceive() is incorrectly used\n\r");
+    exit(1);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+#else // OPENTHREAD_SIMULATION_MDNS_SOCKET_IMPLEMENT_POSIX
+
+otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    otMessageFree(aMessage);
+}
+
+void otPlatMdnsSendUnicast(otInstance *aInstance, otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aAddress);
+    otMessageFree(aMessage);
+}
+
+#endif // OPENTHREAD_SIMULATION_MDNS_SOCKET_IMPLEMENT_POSIX
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn
index 88680fc..a35d3a3 100644
--- a/include/openthread/BUILD.gn
+++ b/include/openthread/BUILD.gn
@@ -87,6 +87,7 @@
     "link_metrics.h",
     "link_raw.h",
     "logging.h",
+    "mdns.h",
     "mesh_diag.h",
     "message.h",
     "multi_radio.h",
@@ -111,6 +112,7 @@
     "platform/flash.h",
     "platform/infra_if.h",
     "platform/logging.h",
+    "platform/mdns_socket.h",
     "platform/memory.h",
     "platform/messagepool.h",
     "platform/misc.h",
@@ -139,6 +141,7 @@
     "thread_ftd.h",
     "trel.h",
     "udp.h",
+    "verhoeff_checksum.h",
   ]
 
   public_deps = [ ":openthread_config" ]
diff --git a/include/openthread/border_agent.h b/include/openthread/border_agent.h
index 1c7a535..33578e7 100644
--- a/include/openthread/border_agent.h
+++ b/include/openthread/border_agent.h
@@ -58,6 +58,30 @@
 #define OT_BORDER_AGENT_ID_LENGTH (16)
 
 /**
+ * Minimum length of the ephemeral key string.
+ *
+ */
+#define OT_BORDER_AGENT_MIN_EPHEMERAL_KEY_LENGTH (6)
+
+/**
+ * Maximum length of the ephemeral key string.
+ *
+ */
+#define OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_LENGTH (32)
+
+/**
+ * Default ephemeral key timeout interval in milliseconds.
+ *
+ */
+#define OT_BORDER_AGENT_DEFAULT_EPHEMERAL_KEY_TIMEOUT (2 * 60 * 1000u)
+
+/**
+ * Maximum ephemeral key timeout interval in milliseconds.
+ *
+ */
+#define OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_TIMEOUT (10 * 60 * 1000u)
+
+/**
  * @struct otBorderAgentId
  *
  * Represents a Border Agent ID.
@@ -109,6 +133,8 @@
 /**
  * Gets the randomly generated Border Agent ID.
  *
+ * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE`.
+ *
  * 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.
@@ -127,6 +153,8 @@
 /**
  * Sets the Border Agent ID.
  *
+ * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE`.
+ *
  * The Border Agent ID will be saved in persistent storage and survive reboots. It's required to
  * set the ID only once after factory reset. If the ID has never been set by calling this function,
  * a random ID will be generated and returned when `otBorderAgentGetId` is called.
@@ -143,6 +171,112 @@
 otError otBorderAgentSetId(otInstance *aInstance, const otBorderAgentId *aId);
 
 /**
+ * Sets the ephemeral key for a given timeout duration.
+ *
+ * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE`.
+ *
+ * The ephemeral key can be set when the Border Agent is already running and is not currently connected to any external
+ * commissioner (i.e., it is in `OT_BORDER_AGENT_STATE_STARTED` state). Otherwise `OT_ERROR_INVALID_STATE` is returned.
+ *
+ * The given @p aKeyString is directly used as the ephemeral PSK (excluding the trailing null `\0` character ).
+ * The @p aKeyString length must be between `OT_BORDER_AGENT_MIN_EPHEMERAL_KEY_LENGTH` and
+ * `OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_LENGTH`, inclusive.
+ *
+ * Setting the ephemeral key again before a previously set key has timed out will replace the previously set key and
+ * reset the timeout.
+ *
+ * While the timeout interval is in effect, the ephemeral key can be used only once by an external commissioner to
+ * connect. Once the commissioner disconnects, the ephemeral key is cleared, and the Border Agent reverts to using
+ * PSKc.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aKeyString   The ephemeral key string (used as PSK excluding the trailing null `\0` character).
+ * @param[in] aTimeout     The timeout duration in milliseconds to use the ephemeral key.
+ *                         If zero, the default `OT_BORDER_AGENT_DEFAULT_EPHEMERAL_KEY_TIMEOUT` value will be used.
+ *                         If the given timeout value is larger than `OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_TIMEOUT`, the
+ *                         max value `OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_TIMEOUT` will be used instead.
+ * @param[in] aUdpPort     The UDP port to use with ephemeral key. If zero, an ephemeral port will be used.
+ *                         `otBorderAgentGetUdpPort()` will return the current UDP port being used.
+ *
+ * @retval OT_ERROR_NONE           Successfully set the ephemeral key.
+ * @retval OT_ERROR_INVALID_STATE  Border Agent is not running or it is connected to an external commissioner.
+ * @retval OT_ERROR_INVALID_ARGS   The given @p aKeyString is not valid (too short or too long).
+ * @retval OT_ERROR_FAILED         Failed to set the key (e.g., could not bind to UDP port).
+
+ *
+ */
+otError otBorderAgentSetEphemeralKey(otInstance *aInstance,
+                                     const char *aKeyString,
+                                     uint32_t    aTimeout,
+                                     uint16_t    aUdpPort);
+
+/**
+ * Cancels the ephemeral key that is in use.
+ *
+ * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE`.
+ *
+ * Can be used to cancel a previously set ephemeral key before it times out. If the Border Agent is not running or
+ * there is no ephemeral key in use, calling this function has no effect.
+ *
+ * If a commissioner is connected using the ephemeral key and is currently active, calling this function does not
+ * change its state. In this case the `otBorderAgentIsEphemeralKeyActive()` will continue to return `TRUE` until the
+ * commissioner disconnects.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ *
+ */
+void otBorderAgentClearEphemeralKey(otInstance *aInstance);
+
+/**
+ * Indicates whether or not an ephemeral key is currently active.
+ *
+ * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE`.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ *
+ * @retval TRUE    An ephemeral key is active.
+ * @retval FALSE   No ephemeral key is active.
+ *
+ */
+bool otBorderAgentIsEphemeralKeyActive(otInstance *aInstance);
+
+/**
+ * Callback function pointer to signal changes related to the Border Agent's ephemeral key.
+ *
+ * This callback is invoked whenever:
+ *
+ * - The Border Agent starts using an ephemeral key.
+ * - Any parameter related to the ephemeral key, such as the port number, changes.
+ * - The Border Agent stops using the ephemeral key due to:
+ *   - A direct call to `otBorderAgentClearEphemeralKey()`.
+ *   - The ephemeral key timing out.
+ *   - An external commissioner successfully using the key to connect and then disconnecting.
+ *   - Reaching the maximum number of allowed failed connection attempts.
+ *
+ * Any OpenThread API, including `otBorderAgent` APIs, can be safely called from this callback.
+ *
+ * @param[in] aContext   A pointer to an arbitrary context (provided when callback is set).
+ *
+ */
+typedef void (*otBorderAgentEphemeralKeyCallback)(void *aContext);
+
+/**
+ * Sets the callback function used by the Border Agent to notify any changes related to use of ephemeral key.
+ *
+ * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE`.
+ *
+ * A subsequent call to this function will replace any previously set callback.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aCallback    The callback function pointer.
+ * @param[in] aContext     The arbitrary context to use with callback.
+ *
+ */
+void otBorderAgentSetEphemeralKeyCallback(otInstance                       *aInstance,
+                                          otBorderAgentEphemeralKeyCallback aCallback,
+                                          void                             *aContext);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/border_routing.h b/include/openthread/border_routing.h
index afd2d7e..4a3b953 100644
--- a/include/openthread/border_routing.h
+++ b/include/openthread/border_routing.h
@@ -240,6 +240,22 @@
 void otBorderRoutingClearRouteInfoOptionPreference(otInstance *aInstance);
 
 /**
+ * Sets additional options to append at the end of emitted Router Advertisement (RA) messages.
+ *
+ * The content of @p aOptions is copied internally, so it can be a temporary buffer (e.g., a stack allocated array).
+ *
+ * Subsequent calls to this function overwrite the previously set value.
+ *
+ * @param[in] aOptions   A pointer to the encoded options. Can be `NULL` to clear.
+ * @param[in] aLength    Number of bytes in @p aOptions.
+ *
+ * @retval OT_ERROR_NONE     Successfully set the extra option bytes.
+ * @retval OT_ERROR_NO_BUFS  Could not allocate buffer to save the buffer.
+ *
+ */
+otError otBorderRoutingSetExtraRouterAdvertOptions(otInstance *aInstance, const uint8_t *aOptions, uint16_t aLength);
+
+/**
  * Gets the current preference used for published routes in Network Data.
  *
  * The preference is determined as follows:
@@ -483,6 +499,31 @@
 otBorderRoutingDhcp6PdState otBorderRoutingDhcp6PdGetState(otInstance *aInstance);
 
 /**
+ * When the state of a DHCPv6 Prefix Delegation (PD) on the Thread interface changes, this callback notifies processes
+ * in the OS of this changed state.
+ *
+ * @param[in] aState    The state of DHCPv6 Prefix Delegation State.
+ * @param[in] aContext  A pointer to arbitrary context information.
+ *
+ */
+typedef void (*otBorderRoutingRequestDhcp6PdCallback)(otBorderRoutingDhcp6PdState aState, void *aContext);
+
+/**
+ * Sets the callback whenever the DHCPv6 PD state changes on the Thread interface.
+ *
+ * Subsequent calls to this function replace the previously set callback.
+ *
+ * @param[in] aInstance  A pointer to an OpenThread instance.
+ * @param[in] aCallback  A pointer to a function that is called whenever the DHCPv6 PD state changes.
+ * @param[in] aContext   A pointer to arbitrary context information.
+ *
+ *
+ */
+void otBorderRoutingDhcp6PdSetRequestCallback(otInstance                           *aInstance,
+                                              otBorderRoutingRequestDhcp6PdCallback aCallback,
+                                              void                                 *aContext);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/channel_manager.h b/include/openthread/channel_manager.h
index 6afe3a4..e024957 100644
--- a/include/openthread/channel_manager.h
+++ b/include/openthread/channel_manager.h
@@ -47,8 +47,14 @@
  * @brief
  *   This module includes functions for Channel Manager.
  *
- *   The functions in this module are available when Channel Manager feature
- *   (`OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE`) is enabled. Channel Manager is available only on an FTD build.
+ *   The functions in this module are available when Channel Manager features
+ *   `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+ * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE` are enabled. Channel Manager behavior depends on the
+ * device role. It manages the network-wide PAN channel on a Full Thread Device in rx-on-when-idle mode, or with
+ * `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE` set,
+ *   selects CSL channel in synchronized rx-off-when-idle mode. On a Minimal Thread Device
+ *   `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE` selects
+ * the CSL channel.
  *
  * @{
  *
@@ -77,7 +83,9 @@
 uint8_t otChannelManagerGetRequestedChannel(otInstance *aInstance);
 
 /**
- * Gets the delay (in seconds) used by Channel Manager for a channel change.
+ * Gets the delay (in seconds) used by Channel Manager for a network channel change.
+ *
+ * Only available on FTDs.
  *
  * @param[in]  aInstance          A pointer to an OpenThread instance.
  *
@@ -87,10 +95,10 @@
 uint16_t otChannelManagerGetDelay(otInstance *aInstance);
 
 /**
- * Sets the delay (in seconds) used for a channel change.
+ * Sets the delay (in seconds) used for a network channel change.
  *
- * The delay should preferably be longer than the maximum data poll interval used by all sleepy-end-devices within the
- * Thread network.
+ * Only available on FTDs. 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.
  * @param[in]  aDelay             Delay in seconds.
@@ -117,7 +125,7 @@
  *
  * 2) If the first step passes, then `ChannelManager` selects a potentially better channel. It uses the collected
  *    channel quality data by `ChannelMonitor` module. The supported and favored channels are used at this step.
- *    (see otChannelManagerSetSupportedChannels() and otChannelManagerSetFavoredChannels()).
+ *    (see `otChannelManagerSetSupportedChannels()` and `otChannelManagerSetFavoredChannels()`).
  *
  * 3) If the newly selected channel is different from the current channel, `ChannelManager` requests/starts the
  *    channel change process (internally invoking a `RequestChannelChange()`).
@@ -132,10 +140,41 @@
 otError otChannelManagerRequestChannelSelect(otInstance *aInstance, bool aSkipQualityCheck);
 
 /**
- * Enables or disables the auto-channel-selection functionality.
+ * Requests that `ChannelManager` checks and selects a new CSL channel and starts a CSL channel change.
+ *
+ * Only available with `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+ * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`. This function 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:
+ *
+ * 1) `ChannelManager` decides if the CSL channel change would be helpful. This check can be skipped if
+ *    `aSkipQualityCheck` is set to true (forcing a CSL channel selection to happen and skipping the quality check).
+ *    This step uses the collected link quality metrics on the device (such as CCA failure rate, frame and message
+ *    error rates per neighbor, etc.) to determine if the current channel quality is at the level that justifies
+ *    a CSL channel change.
+ *
+ * 2) If the first step passes, then `ChannelManager` selects a potentially better CSL channel. It uses the collected
+ *    channel quality data by `ChannelMonitor` module. The supported and favored channels are used at this step.
+ *    (see `otChannelManagerSetSupportedChannels()` and `otChannelManagerSetFavoredChannels()`).
+ *
+ * 3) If the newly selected CSL channel is different from the current CSL channel, `ChannelManager` starts the
+ *    CSL channel change process.
+ *
+ * @param[in] aInstance                A pointer to an OpenThread instance.
+ * @param[in] aSkipQualityCheck        Indicates whether the quality check (step 1) should be skipped.
+ *
+ * @retval OT_ERROR_NONE               Channel selection finished successfully.
+ * @retval OT_ERROR_NOT_FOUND          Supported channel mask is empty, therefore could not select a channel.
+ *
+ */
+otError otChannelManagerRequestCslChannelSelect(otInstance *aInstance, bool aSkipQualityCheck);
+
+/**
+ * Enables or disables the auto-channel-selection functionality for network channel.
  *
  * When enabled, `ChannelManager` will periodically invoke a `RequestChannelSelect(false)`. The period interval
- * can be set by `SetAutoChannelSelectionInterval()`.
+ * can be set by `otChannelManagerSetAutoChannelSelectionInterval()`.
  *
  * @param[in]  aInstance    A pointer to an OpenThread instance.
  * @param[in]  aEnabled     Indicates whether to enable or disable this functionality.
@@ -144,7 +183,7 @@
 void otChannelManagerSetAutoChannelSelectionEnabled(otInstance *aInstance, bool aEnabled);
 
 /**
- * Indicates whether the auto-channel-selection functionality is enabled or not.
+ * Indicates whether the auto-channel-selection functionality for a network channel is enabled or not.
  *
  * @param[in]  aInstance    A pointer to an OpenThread instance.
  *
@@ -154,6 +193,33 @@
 bool otChannelManagerGetAutoChannelSelectionEnabled(otInstance *aInstance);
 
 /**
+ * Enables or disables the auto-channel-selection functionality for a CSL channel.
+ *
+ * Only available with `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+ * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`. When enabled, `ChannelManager` will periodically invoke
+ * a `otChannelManagerRequestCslChannelSelect()`. The period interval can be set by
+ * `otChannelManagerSetAutoChannelSelectionInterval()`.
+ *
+ * @param[in]  aInstance    A pointer to an OpenThread instance.
+ * @param[in]  aEnabled     Indicates whether to enable or disable this functionality.
+ *
+ */
+void otChannelManagerSetAutoCslChannelSelectionEnabled(otInstance *aInstance, bool aEnabled);
+
+/**
+ * Indicates whether the auto-csl-channel-selection functionality is enabled or not.
+ *
+ * Only available with `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+ * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`.
+ *
+ * @param[in]  aInstance    A pointer to an OpenThread instance.
+ *
+ * @returns TRUE if enabled, FALSE if disabled.
+ *
+ */
+bool otChannelManagerGetAutoCslChannelSelectionEnabled(otInstance *aInstance);
+
+/**
  * Sets the period interval (in seconds) used by auto-channel-selection functionality.
  *
  * @param[in] aInstance   A pointer to an OpenThread instance.
diff --git a/include/openthread/icmp6.h b/include/openthread/icmp6.h
index 32f8c6a..925871c 100644
--- a/include/openthread/icmp6.h
+++ b/include/openthread/icmp6.h
@@ -144,6 +144,7 @@
     OT_ICMP6_ECHO_HANDLER_UNICAST_ONLY   = 1, ///< ICMPv6 Echo processing enabled only for unicast requests only
     OT_ICMP6_ECHO_HANDLER_MULTICAST_ONLY = 2, ///< ICMPv6 Echo processing enabled only for multicast requests only
     OT_ICMP6_ECHO_HANDLER_ALL            = 3, ///< ICMPv6 Echo processing enabled for unicast and multicast requests
+    OT_ICMP6_ECHO_HANDLER_RLOC_ALOC_ONLY = 4, ///< ICMPv6 Echo processing enabled for RLOC/ALOC destinations only
 } otIcmp6EchoMode;
 
 /**
diff --git a/include/openthread/instance.h b/include/openthread/instance.h
index 3351997..ae50bbf 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 (395)
+#define OPENTHREAD_API_VERSION (409)
 
 /**
  * @addtogroup api-instance
diff --git a/include/openthread/ip6.h b/include/openthread/ip6.h
index 7f54b73..2636702 100644
--- a/include/openthread/ip6.h
+++ b/include/openthread/ip6.h
@@ -236,7 +236,6 @@
     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.
     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.
diff --git a/include/openthread/link.h b/include/openthread/link.h
index f4d203e..15bc0d8 100644
--- a/include/openthread/link.h
+++ b/include/openthread/link.h
@@ -55,27 +55,6 @@
 #define OT_US_PER_TEN_SYMBOLS OT_RADIO_TEN_SYMBOLS_TIME ///< Time for 10 symbols in units of microseconds
 
 /**
- * Represents link-specific information for messages received from the Thread radio.
- *
- */
-typedef struct otThreadLinkInfo
-{
-    uint16_t mPanId;                   ///< Source PAN ID
-    uint8_t  mChannel;                 ///< 802.15.4 Channel
-    int8_t   mRss;                     ///< Received Signal Strength in dBm.
-    uint8_t  mLqi;                     ///< Link Quality Indicator for a received message.
-    bool     mLinkSecurity : 1;        ///< Indicates whether or not link security is enabled.
-    bool     mIsDstPanIdBroadcast : 1; ///< Indicates whether or not destination PAN ID is broadcast.
-
-    // Applicable/Required only when time sync feature (`OPENTHREAD_CONFIG_TIME_SYNC_ENABLE`) is enabled.
-    uint8_t mTimeSyncSeq;       ///< The time sync sequence.
-    int64_t mNetworkTimeOffset; ///< The time offset to the Thread network time, in microseconds.
-
-    // Applicable only when OPENTHREAD_CONFIG_MULTI_RADIO feature is enabled.
-    uint8_t mRadioType; ///< Radio link type.
-} otThreadLinkInfo;
-
-/**
  * Used to indicate no fixed received signal strength was set
  *
  */
diff --git a/include/openthread/link_metrics.h b/include/openthread/link_metrics.h
index 3b6a76a..cf79a80 100644
--- a/include/openthread/link_metrics.h
+++ b/include/openthread/link_metrics.h
@@ -248,6 +248,17 @@
                                    uint8_t             aLength);
 
 /**
+ * If Link Metrics Manager is enabled.
+ *
+ * @param[in] aInstance       A pointer to an OpenThread instance.
+ *
+ * @retval TRUE   Link Metrics Manager is enabled.
+ * @retval FALSE  Link Metrics Manager is not enabled.
+ *
+ */
+bool otLinkMetricsManagerIsEnabled(otInstance *aInstance);
+
+/**
  * Enable or disable Link Metrics Manager.
  *
  * @param[in] aInstance       A pointer to an OpenThread instance.
diff --git a/include/openthread/mdns.h b/include/openthread/mdns.h
new file mode 100644
index 0000000..a88c70f
--- /dev/null
+++ b/include/openthread/mdns.h
@@ -0,0 +1,849 @@
+/*
+ *  Copyright (c) 2024, 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 the mDNS related APIs.
+ *
+ */
+
+#ifndef OPENTHREAD_MULTICAST_DNS_H_
+#define OPENTHREAD_MULTICAST_DNS_H_
+
+#include <stdint.h>
+
+#include <openthread/error.h>
+#include <openthread/instance.h>
+#include <openthread/ip6.h>
+#include <openthread/platform/dnssd.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @addtogroup api-mdns
+ *
+ * @brief
+ *   This module includes APIs for Multicast DNS (mDNS).
+ *
+ * @{
+ *
+ * The mDNS APIs are available when the mDNS support `OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE` is enabled and the
+ * `OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE` is also enabled.
+ *
+ */
+
+/**
+ * Represents a request ID (`uint32_t` value) for registering a host, a service, or a key service.
+ *
+ */
+typedef otPlatDnssdRequestId otMdnsRequestId;
+
+/**
+ * Represents the callback function to report the outcome of a host, service, or key registration request.
+ *
+ * The outcome of a registration request is reported back by invoking this callback with one of the following `aError`
+ * inputs:
+ *
+ * - `OT_ERROR_NONE` indicates registration was successful.
+ * - `OT_ERROR_DUPLICATED` indicates a name conflict while probing, i.e., name is claimed by another mDNS responder.
+ *
+ * See `otMdnsRegisterHost()`, `otMdnsRegisterService()`, and `otMdnsRegisterKey()` for more details about when
+ * the callback will be invoked.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aRequestId    The request ID.
+ * @param[in] aError        Error indicating the outcome of request.
+ *
+ */
+typedef otPlatDnssdRegisterCallback otMdnsRegisterCallback;
+
+/**
+ * Represents the callback function to report a detected name conflict after successful registration of an entry.
+ *
+ * If a conflict is detected while registering an entry, it is reported through the provided `otMdnsRegisterCallback`.
+ * The `otMdnsConflictCallback` is used only when a name conflict is detected after an entry has been successfully
+ * registered.
+ *
+ * A non-NULL @p aServiceType indicates that conflict is for a service entry. In this case @p aName specifies the
+ * service instance label (treated as as a single DNS label and can potentially include dot `.` character).
+ *
+ * A NULL @p aServiceType indicates that conflict is for a host entry. In this case @p Name specifies the host name. It
+ * does not include the domain name.
+ *
+ * @param[in] aInstance      The OpenThread instance.
+ * @param[in] aName          The host name or the service instance label.
+ * @param[in] aServiceType   The service type (e.g., `_tst._udp`).
+ *
+ */
+typedef void (*otMdnsConflictCallback)(otInstance *aInstance, const char *aName, const char *aServiceType);
+
+/**
+ * Represents an mDNS host.
+ *
+ * This type is used to register or unregister a host (`otMdnsRegisterHost()` and `otMdnsUnregisterHost()`).
+ *
+ * See the description of each function for more details on how different fields are used in each case.
+ *
+ */
+typedef otPlatDnssdHost otMdnsHost;
+
+/**
+ * Represents an mDNS service.
+ *
+ * This type is used to register or unregister a service (`otMdnsRegisterService()` and `otMdnsUnregisterService()`).
+ *
+ * See the description of each function for more details on how different fields are used in each case.
+ *
+ */
+typedef otPlatDnssdService otMdnsService;
+
+/**
+ * Represents an mDNS key record.
+ *
+ * See `otMdnsRegisterKey()`, `otMdnsUnregisterKey()` for more details about fields in each case.
+ *
+ */
+typedef otPlatDnssdKey otMdnsKey;
+
+/**
+ * Represents an mDNS entry iterator.
+ *
+ */
+typedef struct otMdnsIterator otMdnsIterator;
+
+/**
+ * Represents a host/service/key entry state.
+ *
+ */
+typedef enum otMdnsEntryState
+{
+    OT_MDNS_ENTRY_STATE_PROBING,    ///< Probing to claim the name.
+    OT_MDNS_ENTRY_STATE_REGISTERED, ///< Entry is successfully registered.
+    OT_MDNS_ENTRY_STATE_CONFLICT,   ///< Name conflict was detected.
+    OT_MDNS_ENTRY_STATE_REMOVING,   ///< Entry is being removed (sending "goodbye" announcements).
+} otMdnsEntryState;
+
+/**
+ * Enables or disables the mDNS module.
+ *
+ * The mDNS module should be enabled before registration any host, service, or key entries. Disabling mDNS will
+ * immediately stop all operations and any communication (multicast or unicast tx) and remove any previously registered
+ * entries without sending any "goodbye" announcements or invoking their callback. Once disabled, all currently active
+ * browsers and resolvers are stopped.
+ *
+ * @param[in] aInstance      The OpenThread instance.
+ * @param[in] aEnable        Boolean to indicate whether to enable (on `TRUE`) or disable (on `FALSE`).
+ * @param[in] aInfraIfIndex  The network interface index for mDNS operation. Value is ignored when disabling
+ *
+ * @retval OT_ERROR_NONE     Enabled or disabled the mDNS module successfully.
+ * @retval OT_ERROR_ALREADY  mDNS is already enabled on an enable request or is already disabled on a disable request.
+ *
+ */
+otError otMdnsSetEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex);
+
+/**
+ * Indicates whether the mDNS module is enabled.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ *
+ * @retval TRUE    The mDNS module is enabled
+ * @retval FALSE   The mDNS module is disabled.
+ *
+ */
+bool otMdnsIsEnabled(otInstance *aInstance);
+
+/**
+ * Sets whether the mDNS module is allowed to send questions requesting unicast responses referred to as "QU" questions.
+ *
+ * The "QU" questions request unicast responses, in contrast to "QM" questions which request multicast responses.
+ *
+ * When allowed, the first probe will be sent as a "QU" question. This API can be used to address platform limitation
+ * where platform socket cannot accept unicast response received on mDNS port (due to it being already bound).
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aAllow        Indicates whether or not to allow "QU" questions.
+ *
+ */
+void otMdnsSetQuestionUnicastAllowed(otInstance *aInstance, bool aAllow);
+
+/**
+ * Indicates whether mDNS module is allowed to send "QU" questions requesting unicast response.
+ *
+ * @retval TRUE  The mDNS module is allowed to send "QU" questions.
+ * @retval FALSE The mDNS module is not allowed to send "QU" questions.
+ *
+ */
+bool otMdnsIsQuestionUnicastAllowed(otInstance *aInstance);
+
+/**
+ * Sets the post-registration conflict callback.
+ *
+ * If a conflict is detected while registering an entry, it is reported through the provided `otMdnsRegisterCallback`.
+ * The `otMdnsConflictCallback` is used only when a name conflict is detected after an entry has been successfully
+ * registered.
+ *
+ * @p aCallback can be set to `NULL` if not needed. Subsequent calls will replace any previously set callback.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aCallback     The conflict callback.
+ *
+ */
+void otMdnsSetConflictCallback(otInstance *aInstance, otMdnsConflictCallback aCallback);
+
+/**
+ * Registers or updates a host on mDNS.
+ *
+ * The fields in @p aHost follow these rules:
+ *
+ * - The `mHostName` field specifies the host name to register (e.g., "myhost"). MUST NOT contain the domain name.
+ * - The `mAddresses` is array of IPv6 addresses to register with the host. `mAddressesLength` provides the number of
+ *   entries in `mAddresses` array.
+ * - The `mAddresses` array can be empty with zero `mAddressesLength`. In this case, mDNS will treat it as if host is
+ *   unregistered and stops advertising any addresses for this the host name.
+ * - The `mTtl` specifies the TTL if non-zero. If zero, the mDNS core will choose the default TTL of 120 seconds.
+ * - Other fields in @p aHost structure are ignored in an `otMdnsRegisterHost()` call.
+ *
+ * This function can be called again for the same `mHostName` to update a previously registered host entry, for example,
+ * to change the list of addresses of the host. In this case, the mDNS module will send "goodbye" announcements for any
+ * previously registered and now removed addresses and announce any newly added addresses.
+ *
+ * The outcome of the registration request is reported back by invoking the provided @p aCallback with @p aRequestId
+ * as its input and one of the following `aError` inputs:
+ *
+ * - `OT_ERROR_NONE` indicates registration was successful.
+ * - `OT_ERROR_DULICATED` indicates a name conflict while probing, i.e., name is claimed by another mDNS responder.
+ *
+ * For caller convenience, the OpenThread mDNS module guarantees that the callback will be invoked after this function
+ * returns, even in cases of immediate registration success. The @p aCallback can be `NULL` if caller does not want to
+ * be notified of the outcome.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aHost         Information about the host to register.
+ * @param[in] aRequestId    The ID associated with this request.
+ * @param[in] aCallback     The callback function pointer to report the outcome (can be NULL if not needed).
+ *
+ * @retval OT_ERROR_NONE            Successfully started registration. @p aCallback will report the outcome.
+ * @retval OT_ERROR_INVALID_STATE   mDNS module is not enabled.
+ *
+ */
+otError otMdnsRegisterHost(otInstance            *aInstance,
+                           const otMdnsHost      *aHost,
+                           otMdnsRequestId        aRequestId,
+                           otMdnsRegisterCallback aCallback);
+
+/**
+ * Unregisters a host on mDNS.
+ *
+ * The fields in @p aHost follow these rules:
+ *
+ * - The `mHostName` field specifies the host name to unregister (e.g., "myhost"). MUST NOT contain the domain name.
+ * - Other fields in @p aHost structure are ignored in an `otMdnsUnregisterHost()` call.
+ *
+ * If there is no previously registered host with the same name, no action is performed.
+ *
+ * If there is a previously registered host with the same name, the mDNS module will send "goodbye" announcement for
+ * all previously advertised address records.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aHost         Information about the host to unregister.
+ *
+ * @retval OT_ERROR_NONE            Successfully unregistered host.
+ * @retval OT_ERROR_INVALID_STATE   mDNS module is not enabled.
+ *
+ */
+otError otMdnsUnregisterHost(otInstance *aInstance, const otMdnsHost *aHost);
+
+/**
+ * Registers or updates a service on mDNS.
+ *
+ * The fields in @p aService follow these rules:
+ *
+ * - The `mServiceInstance` specifies the service instance label. It is treated as a single DNS name label. It may
+ *   contain dot `.` character which is allowed in a service instance label.
+ * - The `mServiceType` specifies the service type (e.g., "_tst._udp"). It is treated as multiple dot `.` separated
+ *   labels. It MUST NOT contain the domain name.
+ * - The `mHostName` field specifies the host name of the service. MUST NOT contain the domain name.
+ * - The `mSubTypeLabels` is an array of strings representing sub-types associated with the service. Each array entry
+ *   is a sub-type label. The `mSubTypeLabels can be NULL if there is no sub-type. Otherwise, the array length is
+ *   specified by `mSubTypeLabelsLength`.
+ * - The `mTxtData` and `mTxtDataLength` specify the encoded TXT data. The `mTxtData` can be NULL or `mTxtDataLength`
+ *   can be zero to specify an empty TXT data. In this case mDNS module will use a single zero byte `[ 0 ]` as the
+ *   TXT data.
+ * - The `mPort`, `mWeight`, and `mPriority` specify the service's parameters as specified in DNS SRV record.
+ * - The `mTtl` specifies the TTL if non-zero. If zero, the mDNS module will use the default TTL of 120 seconds.
+ * - Other fields in @p aService structure are ignored in an `otMdnsRegisterService()` call.
+ *
+ * This function can be called again for the same `mServiceInstance` and `mServiceType` to update a previously
+ * registered service entry, for example, to change the sub-types list, or update any parameter such as port, weight,
+ * priority, TTL, or host name. The mDNS module will send announcements for any changed info, e.g., will send "goodbye"
+ * announcements for any removed sub-types and announce any newly added sub-types.
+ *
+ * Regarding the invocation of the @p aCallback, this function behaves in the same way as described in
+ * `otMdnsRegisterHost()`.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aService      Information about the service to register.
+ * @param[in] aRequestId    The ID associated with this request.
+ * @param[in] aCallback     The callback function pointer to report the outcome (can be NULL if not needed).
+ *
+ * @retval OT_ERROR_NONE            Successfully started registration. @p aCallback will report the outcome.
+ * @retval OT_ERROR_INVALID_STATE   mDNS module is not enabled.
+ *
+ */
+otError otMdnsRegisterService(otInstance            *aInstance,
+                              const otMdnsService   *aService,
+                              otMdnsRequestId        aRequestId,
+                              otMdnsRegisterCallback aCallback);
+
+/**
+ * Unregisters a service on mDNS module.
+ *
+ * The fields in @p aService follow these rules:
+
+ * - The `mServiceInstance` specifies the service instance label. It is treated as a single DNS name label. It may
+ *   contain dot `.` character which is allowed in a service instance label.
+ * - The `mServiceType` specifies the service type (e.g., "_tst._udp"). It is treated as multiple dot `.` separated
+ *   labels. It MUST NOT contain the domain name.
+ * - Other fields in @p aService structure are ignored in an `otMdnsUnregisterService()` call.
+ *
+ * If there is no previously registered service with the same name, no action is performed.
+ *
+ * If there is a previously registered service with the same name, the mDNS module will send "goodbye" announcements
+ * for all related records.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aService      Information about the service to unregister.
+ *
+ * @retval OT_ERROR_NONE            Successfully unregistered service.
+ * @retval OT_ERROR_INVALID_STATE   mDNS module is not enabled.
+ *
+ */
+otError otMdnsUnregisterService(otInstance *aInstance, const otMdnsService *aService);
+
+/**
+ * Registers or updates a key record on mDNS module.
+ *
+ * The fields in @p aKey follow these rules:
+ *
+ * - If the key is associated with a host entry, the `mName` field specifies the host name and the `mServiceType` MUST
+ *   be NULL.
+ * - If the key is associated with a service entry, the `mName` filed specifies the service instance label (always
+ *   treated as a single label) and the `mServiceType` filed specifies the service type (e.g., "_tst._udp"). In this
+ *   case the DNS name for key record is `<mName>.<mServiceTye>`.
+ * - The `mKeyData` field contains the key record's data with `mKeyDataLength` as its length in byes.
+ * - The `mTtl` specifies the TTL if non-zero. If zero, the mDNS module will use the default TTL of 120 seconds.
+ * - Other fields in @p aKey structure are ignored in an `otMdnsRegisterKey()` call.
+ *
+ * This function can be called again for the same name to updated a previously registered key entry, for example, to
+ * change the key data or TTL.
+ *
+ * Regarding the invocation of the @p aCallback, this function behaves in the same way as described in
+ * `otMdnsRegisterHost()`.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aKey          Information about the key record to register.
+ * @param[in] aRequestId    The ID associated with this request.
+ * @param[in] aCallback     The callback function pointer to report the outcome (can be NULL if not needed).
+ *
+ * @retval OT_ERROR_NONE            Successfully started registration. @p aCallback will report the outcome.
+ * @retval OT_ERROR_INVALID_STATE   mDNS module is not enabled.
+ *
+ */
+otError otMdnsRegisterKey(otInstance            *aInstance,
+                          const otMdnsKey       *aKey,
+                          otMdnsRequestId        aRequestId,
+                          otMdnsRegisterCallback aCallback);
+
+/**
+ * Unregisters a key record on mDNS.
+ *
+ * The fields in @p aKey follow these rules:
+ *
+ * - If the key is associated with a host entry, the `mName` field specifies the host name and the `mServiceType` MUST
+ *   be NULL.
+ * - If the key is associated with a service entry, the `mName` filed specifies the service instance label (always
+ *   treated as a single label) and the `mServiceType` filed specifies the service type (e.g., "_tst._udp"). In this
+ *   case the DNS name for key record is `<mName>.<mServiceTye>`.
+ * - Other fields in @p aKey structure are ignored in an `otMdnsUnregisterKey()` call.
+ *
+ * If there is no previously registered key with the same name, no action is performed.
+ *
+ * If there is a previously registered key with the same name, the mDNS module will send "goodbye" announcements for
+ * the key record.
+ *
+ * @param[in] aInstance     The OpenThread instance.
+ * @param[in] aKey          Information about the key to unregister.
+ *
+ * @retval OT_ERROR_NONE            Successfully unregistered key
+ * @retval OT_ERROR_INVALID_STATE   mDNS module is not enabled.
+ *
+ */
+otError otMdnsUnregisterKey(otInstance *aInstance, const otMdnsKey *aKey);
+
+/**
+ * Allocates a new iterator.
+ *
+ * An allocated iterator must be freed by the caller using `otMdnsFreeIterator()`.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ *
+ * @returns A pointer to the allocated iterator, or `NULL` if it fails to allocate.
+ *
+ */
+otMdnsIterator *otMdnsAllocateIterator(otInstance *aInstance);
+
+/**
+ * Frees a previously allocated iterator.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aIterator    The iterator to free.
+ *
+ */
+void otMdnsFreeIterator(otInstance *aInstance, otMdnsIterator *aIterator);
+
+/**
+ * Iterates over registered host entries.
+ *
+ * On success, @p aHost is populated with information about the next host. Pointers within the `otMdnsHost` structure
+ * (like `mName`) remain valid until the next call to any OpenThread stack's public or platform API/callback.
+ *
+ * @param[in]  aInstance   The OpenThread instance.
+ * @param[in]  aIterator   Pointer to the iterator.
+ * @param[out] aHost       Pointer to an `otMdnsHost` to return the information about the next host entry.
+ * @param[out] aState      Pointer to an `otMdnsEntryState` to return the entry state.
+ *
+ * @retval OT_ERROR_NONE         @p aHost, @p aState, & @p aIterator are updated successfully.
+ * @retval OT_ERROR_NOT_FOUND    Reached the end of the list.
+ * @retval OT_ERROR_INVALID_ARG  @p aIterator is not valid.
+ *
+ */
+otError otMdnsGetNextHost(otInstance       *aInstance,
+                          otMdnsIterator   *aIterator,
+                          otMdnsHost       *aHost,
+                          otMdnsEntryState *aState);
+
+/**
+ * Iterates over registered service entries.
+ *
+ * On success, @p aService is populated with information about the next service . Pointers within the `otMdnsService`
+ * structure (like `mServiceType`, `mSubTypeLabels`) remain valid until the next call to any OpenThread stack's public
+ * or platform API/callback.
+ *
+ * @param[in]  aInstance    The OpenThread instance.
+ * @param[in]  aIterator    Pointer to the iterator to use.
+ * @param[out] aService     Pointer to an `otMdnsService` to return the information about the next service entry.
+ * @param[out] aState       Pointer to an `otMdnsEntryState` to return the entry state.
+ *
+ * @retval OT_ERROR_NONE         @p aService, @p aState, & @p aIterator are updated successfully.
+ * @retval OT_ERROR_NOT_FOUND    Reached the end of the list.
+ * @retval OT_ERROR_INVALID_ARG  @p aIterator is not valid.
+ *
+ */
+otError otMdnsGetNextService(otInstance       *aInstance,
+                             otMdnsIterator   *aIterator,
+                             otMdnsService    *aService,
+                             otMdnsEntryState *aState);
+
+/**
+ * Iterates over registered key entries.
+ *
+ * On success, @p aKey is populated with information about the next key.  Pointers within the `otMdnsKey` structure
+ * (like `mName`) remain valid until the next call to any OpenThread stack's public or platform API/callback.
+ *
+ * @param[in]  aInstance    The OpenThread instance.
+ * @param[in]  aIterator    Pointer to the iterator to use.
+ * @param[out] aKey         Pointer to an `otMdnsKey` to return the information about the next key entry.
+ * @param[out] aState       Pointer to an `otMdnsEntryState` to return the entry state.
+ *
+ * @retval OT_ERROR_NONE         @p aKey, @p aState, & @p aIterator are updated successfully.
+ * @retval OT_ERROR_NOT_FOUND    Reached the end of the list.
+ * @retval OT_ERROR_INVALID_ARG  Iterator is not valid.
+ *
+ */
+otError otMdnsGetNextKey(otInstance *aInstance, otMdnsIterator *aIterator, otMdnsKey *aKey, otMdnsEntryState *aState);
+
+typedef struct otMdnsBrowseResult  otMdnsBrowseResult;
+typedef struct otMdnsSrvResult     otMdnsSrvResult;
+typedef struct otMdnsTxtResult     otMdnsTxtResult;
+typedef struct otMdnsAddressResult otMdnsAddressResult;
+
+/**
+ * Represents the callback function used to report a browse result.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResult      The browse result.
+ *
+ */
+typedef void (*otMdnsBrowseCallback)(otInstance *aInstance, const otMdnsBrowseResult *aResult);
+
+/**
+ * Represents the callback function used to report an SRV resolve result.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResult      The SRV resolve result.
+ *
+ */
+typedef void (*otMdnsSrvCallback)(otInstance *aInstance, const otMdnsSrvResult *aResult);
+
+/**
+ * Represents the callback function used to report a TXT resolve result.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResult      The TXT resolve result.
+ *
+ */
+typedef void (*otMdnsTxtCallback)(otInstance *aInstance, const otMdnsTxtResult *aResult);
+
+/**
+ * Represents the callback function use to report a IPv6/IPv4 address resolve result.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResult      The address resolve result.
+ *
+ */
+typedef void (*otMdnsAddressCallback)(otInstance *aInstance, const otMdnsAddressResult *aResult);
+
+/**
+ * Represents a service browser.
+ *
+ */
+typedef struct otMdnsBrowser
+{
+    const char          *mServiceType;  ///< The service type (e.g., "_mt._udp"). MUST NOT include domain name.
+    const char          *mSubTypeLabel; ///< The sub-type label if browsing for sub-type, NULL otherwise.
+    uint32_t             mInfraIfIndex; ///< The infrastructure network interface index.
+    otMdnsBrowseCallback mCallback;     ///< The callback to report result.
+} otMdnsBrowser;
+
+/**
+ * Represents a browse result.
+ *
+ */
+struct otMdnsBrowseResult
+{
+    const char *mServiceType;     ///< The service type (e.g., "_mt._udp").
+    const char *mSubTypeLabel;    ///< The sub-type label if browsing for sub-type, NULL otherwise.
+    const char *mServiceInstance; ///< Service instance label.
+    uint32_t    mTtl;             ///< TTL in seconds. Zero TTL indicates that service is removed.
+    uint32_t    mInfraIfIndex;    ///< The infrastructure network interface index.
+};
+
+/**
+ * Represents an SRV service resolver.
+ *
+ */
+typedef struct otMdnsSrvResolver
+{
+    const char       *mServiceInstance; ///< The service instance label.
+    const char       *mServiceType;     ///< The service type.
+    uint32_t          mInfraIfIndex;    ///< The infrastructure network interface index.
+    otMdnsSrvCallback mCallback;        ///< The callback to report result.
+} otMdnsSrvResolver;
+
+/**
+ * Represents an SRV resolver result.
+ *
+ */
+struct otMdnsSrvResult
+{
+    const char *mServiceInstance; ///< The service instance name label.
+    const char *mServiceType;     ///< The service type.
+    const char *mHostName;        ///< The host name (e.g., "myhost"). Can be NULL when `mTtl` is zero.
+    uint16_t    mPort;            ///< The service port number.
+    uint16_t    mPriority;        ///< The service priority.
+    uint16_t    mWeight;          ///< The service weight.
+    uint32_t    mTtl;             ///< The service TTL in seconds. Zero TTL indicates SRV record is removed.
+    uint32_t    mInfraIfIndex;    ///< The infrastructure network interface index.
+};
+
+/**
+ * Represents a TXT service resolver.
+ *
+ */
+typedef struct otMdnsTxtResolver
+{
+    const char       *mServiceInstance; ///< Service instance label.
+    const char       *mServiceType;     ///< Service type.
+    uint32_t          mInfraIfIndex;    ///< The infrastructure network interface index.
+    otMdnsTxtCallback mCallback;
+} otMdnsTxtResolver;
+
+/**
+ * Represents a TXT resolver result.
+ *
+ */
+struct otMdnsTxtResult
+{
+    const char    *mServiceInstance; ///< The service instance name label.
+    const char    *mServiceType;     ///< The service type.
+    const uint8_t *mTxtData;         ///< Encoded TXT data bytes. Can be NULL when `mTtl` is zero.
+    uint16_t       mTxtDataLength;   ///< Length of TXT data.
+    uint32_t       mTtl;             ///< The TXT data TTL in seconds. Zero TTL indicates record is removed.
+    uint32_t       mInfraIfIndex;    ///< The infrastructure network interface index.
+};
+
+/**
+ * Represents an address resolver.
+ *
+ */
+typedef struct otMdnsAddressResolver
+{
+    const char           *mHostName;     ///< The host name (e.g., "myhost"). MUST NOT contain domain name.
+    uint32_t              mInfraIfIndex; ///< The infrastructure network interface index.
+    otMdnsAddressCallback mCallback;     ///< The callback to report result.
+} otMdnsAddressResolver;
+
+/**
+ * Represents a discovered host address and its TTL.
+ *
+ */
+typedef struct otMdnsAddressAndTtl
+{
+    otIp6Address mAddress; ///< The IPv6 address. For IPv4 address the IPv4-mapped IPv6 address format is used.
+    uint32_t     mTtl;     ///< The TTL in seconds.
+} otMdnsAddressAndTtl;
+
+/**
+ * Represents address resolver result.
+ *
+ */
+struct otMdnsAddressResult
+{
+    const char                *mHostName;        ///< The host name.
+    const otMdnsAddressAndTtl *mAddresses;       ///< Array of host addresses and their TTL. Can be NULL if empty.
+    uint16_t                   mAddressesLength; ///< Number of entries in `mAddresses` array.
+    uint32_t                   mInfraIfIndex;    ///< The infrastructure network interface index.
+};
+
+/**
+ * Starts a service browser.
+ *
+ * Initiates a continuous search for the specified `mServiceType` in @p aBrowser. For sub-type services, use
+ * `mSubTypeLabel` to define the sub-type, for base services, set `mSubTypeLabel` to NULL.
+ *
+ * Discovered services are reported through the `mCallback` function in @p aBrowser. Services that have been removed
+ * are reported with a TTL value of zero. The callback may be invoked immediately with cached information (if available)
+ * and potentially before this function returns. When cached results are used, the reported TTL value will reflect
+ * the original TTL from the last received response.
+ *
+ * Multiple browsers can be started for the same service, provided they use different callback functions.
+ *
+ * @param[in] aInstance   The OpenThread instance.
+ * @param[in] aBrowser    The browser to be started.
+ *
+ * @retval OT_ERROR_NONE           Browser started successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ * @retval OT_ERROR_ALREADY        An identical browser (same service and callback) is already active.
+ *
+ */
+otError otMdnsStartBrowser(otInstance *aInstance, const otMdnsBrowser *aBrowser);
+
+/**
+ * Stops a service browser.
+ *
+ * No action is performed if no matching browser with the same service and callback is currently active.
+ *
+ * @param[in] aInstance   The OpenThread instance.
+ * @param[in] aBrowser    The browser to stop.
+ *
+ * @retval OT_ERROR_NONE           Browser stopped successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ *
+ */
+otError otMdnsStopBrowser(otInstance *aInstance, const otMdnsBrowser *aBroswer);
+
+/**
+ * Starts an SRV record resolver.
+ *
+ * Initiates a continuous SRV record resolver for the specified service in @p aResolver.
+ *
+ * Discovered information is reported through the `mCallback` function in @p aResolver. When the service is removed
+ * it is reported with a TTL value of zero. In this case, `mHostName` may be NULL and other result fields (such as
+ * `mPort`) should be ignored.
+ *
+ * The callback may be invoked immediately with cached information (if available) and potentially before this function
+ * returns. When cached result is used, the reported TTL value will reflect the original TTL from the last received
+ * response.
+ *
+ * Multiple resolvers can be started for the same service, provided they use different callback functions.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to be started.
+ *
+ * @retval OT_ERROR_NONE           Resolver started successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ * @retval OT_ERROR_ALREADY        An identical resolver (same service and callback) is already active.
+ *
+ */
+otError otMdnsStartSrvResolver(otInstance *aInstance, const otMdnsSrvResolver *aResolver);
+
+/**
+ * Stops an SRV record resolver.
+ *
+ * No action is performed if no matching resolver with the same service and callback is currently active.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to stop.
+ *
+ * @retval OT_ERROR_NONE           Resolver stopped successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ *
+ */
+otError otMdnsStopSrvResolver(otInstance *aInstance, const otMdnsSrvResolver *aResolver);
+
+/**
+ * Starts a TXT record resolver.
+ *
+ * Initiates a continuous TXT record resolver for the specified service in @p aResolver.
+ *
+ * Discovered information is reported through the `mCallback` function in @p aResolver. When the TXT record is removed
+ * it is reported with a TTL value of zero. In this case, `mTxtData` may be NULL, and other result fields (such as
+ * `mTxtDataLength`) should be ignored.
+ *
+ * The callback may be invoked immediately with cached information (if available) and potentially before this function
+ * returns. When cached result is used, the reported TTL value will reflect the original TTL from the last received
+ * response.
+ *
+ * Multiple resolvers can be started for the same service, provided they use different callback functions.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to be started.
+ *
+ * @retval OT_ERROR_NONE           Resolver started successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ * @retval OT_ERROR_ALREADY        An identical resolver (same service and callback) is already active.
+ *
+ */
+otError otMdnsStartTxtResolver(otInstance *aInstance, const otMdnsTxtResolver *aResolver);
+
+/**
+ * Stops a TXT record resolver.
+ *
+ * No action is performed if no matching resolver with the same service and callback is currently active.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to stop.
+ *
+ * @retval OT_ERROR_NONE           Resolver stopped successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ *
+ */
+otError otMdnsStopTxtResolver(otInstance *aInstance, const otMdnsTxtResolver *aResolver);
+
+/**
+ * Starts an IPv6 address resolver.
+ *
+ * Initiates a continuous IPv6 address resolver for the specified host name in @p aResolver.
+ *
+ * Discovered addresses are reported through the `mCallback` function in @p aResolver. The callback is invoked
+ * whenever addresses are added or removed, providing an updated list. If all addresses are removed, the callback is
+ * invoked with an empty list (`mAddresses` will be NULL, and `mAddressesLength` will be zero).
+ *
+ * The callback may be invoked immediately with cached information (if available) and potentially before this function
+ * returns. When cached result is used, the reported TTL values will reflect the original TTL from the last received
+ * response.
+ *
+ * Multiple resolvers can be started for the same host name, provided they use different callback functions.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to be started.
+ *
+ * @retval OT_ERROR_NONE           Resolver started successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ * @retval OT_ERROR_ALREADY        An identical resolver (same host and callback) is already active.
+ *
+ */
+otError otMdnsStartIp6AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver);
+
+/**
+ * Stops an IPv6 address resolver.
+ *
+ * No action is performed if no matching resolver with the same host name and callback is currently active.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to stop.
+ *
+ * @retval OT_ERROR_NONE           Resolver stopped successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ *
+ */
+otError otMdnsStopIp6AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver);
+
+/**
+ * Starts an IPv4 address resolver.
+ *
+ * Initiates a continuous IPv4 address resolver for the specified host name in @p aResolver.
+ *
+ * Discovered addresses are reported through the `mCallback` function in @p aResolver. The IPv4 addresses are
+ * represented using the IPv4-mapped IPv6 address format in `mAddresses` array.  The callback is invoked  whenever
+ * addresses are added or removed, providing an updated list. If all addresses are removed, the callback is invoked
+ * with an empty list (`mAddresses` will be NULL, and `mAddressesLength` will be zero).
+ *
+ * The callback may be invoked immediately with cached information (if available) and potentially before this function
+ * returns. When cached result is used, the reported TTL values will reflect the original TTL from the last received
+ * response.
+ *
+ * Multiple resolvers can be started for the same host name, provided they use different callback functions.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to be started.
+ *
+ * @retval OT_ERROR_NONE           Resolver started successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ * @retval OT_ERROR_ALREADY        An identical resolver (same host and callback) is already active.
+ *
+ */
+otError otMdnsStartIp4AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver);
+
+/**
+ * Stops an IPv4 address resolver.
+ *
+ * No action is performed if no matching resolver with the same host name and callback is currently active.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aResolver    The resolver to stop.
+ *
+ * @retval OT_ERROR_NONE           Resolver stopped successfully.
+ * @retval OT_ERROR_INVALID_STATE  mDNS module is not enabled.
+ *
+ */
+otError otMdnsStopIp4AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver);
+
+/**
+ * @}
+ *
+ */
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // OPENTHREAD_MULTICAST_DNS_H_
diff --git a/include/openthread/message.h b/include/openthread/message.h
index 3c2938c..39e14d4 100644
--- a/include/openthread/message.h
+++ b/include/openthread/message.h
@@ -91,6 +91,27 @@
 } otMessageSettings;
 
 /**
+ * Represents link-specific information for messages received from the Thread radio.
+ *
+ */
+typedef struct otThreadLinkInfo
+{
+    uint16_t mPanId;                   ///< Source PAN ID
+    uint8_t  mChannel;                 ///< 802.15.4 Channel
+    int8_t   mRss;                     ///< Received Signal Strength in dBm (averaged over fragments)
+    uint8_t  mLqi;                     ///< Average Link Quality Indicator (averaged over fragments)
+    bool     mLinkSecurity : 1;        ///< Indicates whether or not link security is enabled.
+    bool     mIsDstPanIdBroadcast : 1; ///< Indicates whether or not destination PAN ID is broadcast.
+
+    // Applicable/Required only when time sync feature (`OPENTHREAD_CONFIG_TIME_SYNC_ENABLE`) is enabled.
+    uint8_t mTimeSyncSeq;       ///< The time sync sequence.
+    int64_t mNetworkTimeOffset; ///< The time offset to the Thread network time, in microseconds.
+
+    // Applicable only when OPENTHREAD_CONFIG_MULTI_RADIO feature is enabled.
+    uint8_t mRadioType; ///< Radio link type.
+} otThreadLinkInfo;
+
+/**
  * Free an allocated message buffer.
  *
  * @param[in]  aMessage  A pointer to a message buffer.
@@ -266,12 +287,26 @@
 /**
  * Returns the average RSS (received signal strength) associated with the message.
  *
+ * @param[in]  aMessage  A pointer to a message buffer.
+ *
  * @returns The average RSS value (in dBm) or OT_RADIO_RSSI_INVALID if no average RSS is available.
  *
  */
 int8_t otMessageGetRss(const otMessage *aMessage);
 
 /**
+ * Retrieves the link-specific information for a message received over Thread radio.
+ *
+ * @param[in] aMessage    The message from which to retrieve `otThreadLinkInfo`.
+ * @pram[out] aLinkInfo   A pointer to an `otThreadLinkInfo` to populate.
+ *
+ * @retval OT_ERROR_NONE       Successfully retrieved the link info, @p `aLinkInfo` is updated.
+ * @retval OT_ERROR_NOT_FOUND  Message origin is not `OT_MESSAGE_ORIGIN_THREAD_NETIF`.
+ *
+ */
+otError otMessageGetThreadLinkInfo(const otMessage *aMessage, otThreadLinkInfo *aLinkInfo);
+
+/**
  * Append bytes to a message.
  *
  * @param[in]  aMessage  A pointer to a message buffer.
diff --git a/include/openthread/netdiag.h b/include/openthread/netdiag.h
index 9555952..ff99b38 100644
--- a/include/openthread/netdiag.h
+++ b/include/openthread/netdiag.h
@@ -49,59 +49,43 @@
  *
  */
 
-/**
- * Maximum Number of Network Diagnostic TLV Types to Request or Reset.
- */
-#define OT_NETWORK_DIAGNOSTIC_TYPELIST_MAX_ENTRIES 19
-
-/**
- * Size of Network Diagnostic Child Table entry.
- */
-#define OT_NETWORK_DIAGNOSTIC_CHILD_TABLE_ENTRY_SIZE 3
-
-/**
- * Initializer for otNetworkDiagIterator.
- */
-#define OT_NETWORK_DIAGNOSTIC_ITERATOR_INIT 0
-
-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_EUI64             = 23, ///< EUI64 TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_VERSION           = 24, ///< Version TLV (version number for the protocols and features)
-    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 (version identifier as UTF-8 string for Thread stack codebase/commit/version)
-    OT_NETWORK_DIAGNOSTIC_TLV_CHILD               = 29, ///< Child TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_CHILD_IP6_ADDR_LIST = 30, ///< Child IPv6 Address List TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_ROUTER_NEIGHBOR     = 31, ///< Router Neighbor TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_ANSWER              = 32, ///< Answer TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_QUERY_ID            = 33, ///< Query ID TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_MLE_COUNTERS        = 34, ///< MLE Counters TLV
-
-};
+#define OT_NETWORK_DIAGNOSTIC_TLV_EXT_ADDRESS 0           ///< MAC Extended Address TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_SHORT_ADDRESS 1         ///< Address16 TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_MODE 2                  ///< Mode TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_TIMEOUT 3               ///< Timeout TLV (max polling time period for SEDs)
+#define OT_NETWORK_DIAGNOSTIC_TLV_CONNECTIVITY 4          ///< Connectivity TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_ROUTE 5                 ///< Route64 TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_LEADER_DATA 6           ///< Leader Data TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_NETWORK_DATA 7          ///< Network Data TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_IP6_ADDR_LIST 8         ///< IPv6 Address List TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_MAC_COUNTERS 9          ///< MAC Counters TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_BATTERY_LEVEL 14        ///< Battery Level TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_SUPPLY_VOLTAGE 15       ///< Supply Voltage TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_CHILD_TABLE 16          ///< Child Table TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_CHANNEL_PAGES 17        ///< Channel Pages TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_TYPE_LIST 18            ///< Type List TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_MAX_CHILD_TIMEOUT 19    ///< Max Child Timeout TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_EUI64 23                ///< EUI64 TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_VERSION 24              ///< Thread Version TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_NAME 25          ///< Vendor Name TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_MODEL 26         ///< Vendor Model TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_SW_VERSION 27    ///< Vendor SW Version TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_THREAD_STACK_VERSION 28 ///< Thread Stack Version TLV (codebase/commit version)
+#define OT_NETWORK_DIAGNOSTIC_TLV_CHILD 29                ///< Child TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_CHILD_IP6_ADDR_LIST 30  ///< Child IPv6 Address List TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_ROUTER_NEIGHBOR 31      ///< Router Neighbor TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_ANSWER 32               ///< Answer TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_QUERY_ID 33             ///< Query ID TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_MLE_COUNTERS 34         ///< MLE Counters TLV
+#define OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_APP_URL 35       ///< Vendor App URL 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.
+#define OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_APP_URL_TLV_LENGTH 96       ///< Max length of Vendor App URL TLV.
+
+#define OT_NETWORK_DIAGNOSTIC_ITERATOR_INIT 0 ///<  Initializer for `otNetworkDiagIterator`.
 
 typedef uint16_t otNetworkDiagIterator; ///< Used to iterate through Network Diagnostic TLV.
 
@@ -111,50 +95,15 @@
  */
 typedef struct otNetworkDiagConnectivity
 {
-    /**
-     * The priority of the sender as a parent.
-     */
-    int8_t mParentPriority;
-
-    /**
-     * The number of neighboring devices with which the sender shares a link of quality 3.
-     */
-    uint8_t mLinkQuality3;
-
-    /**
-     * The number of neighboring devices with which the sender shares a link of quality 2.
-     */
-    uint8_t mLinkQuality2;
-
-    /**
-     * The number of neighboring devices with which the sender shares a link of quality 1.
-     */
-    uint8_t mLinkQuality1;
-
-    /**
-     * The sender's routing cost to the Leader.
-     */
-    uint8_t mLeaderCost;
-
-    /**
-     * The most recent ID sequence number received by the sender.
-     */
-    uint8_t mIdSequence;
-
-    /**
-     * The number of active Routers in the sender's Thread Network Partition.
-     */
-    uint8_t mActiveRouters;
-
-    /**
-     * The guaranteed buffer capacity in octets for all IPv6 datagrams destined to a given SED. Optional.
-     */
-    uint16_t mSedBufferSize;
-
-    /**
-     * The guaranteed queue capacity in number of IPv6 datagrams destined to a given SED. Optional.
-     */
-    uint8_t mSedDatagramCount;
+    int8_t   mParentPriority;   ///< The priority of the sender as a parent.
+    uint8_t  mLinkQuality3;     ///< Number of neighbors with link of quality 3.
+    uint8_t  mLinkQuality2;     ///< Number of neighbors with link of quality 2.
+    uint8_t  mLinkQuality1;     ///< Number of neighbors with link of quality 1.
+    uint8_t  mLeaderCost;       ///< Cost to the Leader.
+    uint8_t  mIdSequence;       ///< Most recent received ID seq number.
+    uint8_t  mActiveRouters;    ///< Number of active routers.
+    uint16_t mSedBufferSize;    ///< Buffer capacity in bytes for SEDs. Optional.
+    uint8_t  mSedDatagramCount; ///< Queue capacity (number of IPv6 datagrams) per SED. Optional.
 } otNetworkDiagConnectivity;
 
 /**
@@ -170,7 +119,7 @@
 } otNetworkDiagRouteData;
 
 /**
- * Represents a Network Diagnostic Route TLV value.
+ * Represents a Network Diagnostic Route64 TLV value.
  *
  */
 typedef struct otNetworkDiagRoute
@@ -178,17 +127,9 @@
     /**
      * The sequence number associated with the set of Router ID assignments in #mRouteData.
      */
-    uint8_t mIdSequence;
-
-    /**
-     * Number of elements in #mRouteData.
-     */
-    uint8_t mRouteCount;
-
-    /**
-     * Link Quality and Routing Cost data.
-     */
-    otNetworkDiagRouteData mRouteData[OT_NETWORK_MAX_ROUTER_ID + 1];
+    uint8_t                mIdSequence;                              ///< Sequence number for Router ID assignments.
+    uint8_t                mRouteCount;                              ///< Number of routes.
+    otNetworkDiagRouteData mRouteData[OT_NETWORK_MAX_ROUTER_ID + 1]; ///< Link Quality and Routing Cost data.
 } otNetworkDiagRoute;
 
 /**
@@ -239,28 +180,10 @@
  */
 typedef struct otNetworkDiagChildEntry
 {
-    /**
-     * Expected poll time expressed as 2^(Timeout-4) seconds.
-     */
-    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;
-
-    /**
-     * Link mode bits.
-     */
-    otLinkModeConfig mMode;
+    uint16_t mTimeout : 5;     ///< Expected poll timeout expressed as 2^(Timeout-4) seconds.
+    uint8_t  mLinkQuality : 2; ///< Link Quality In value [0,3]. Zero indicate sender cannot provide link quality info.
+    uint16_t mChildId : 9;     ///< Child ID (derived from child RLOC)
+    otLinkModeConfig mMode;    ///< Link mode.
 } otNetworkDiagChildEntry;
 
 /**
@@ -269,10 +192,7 @@
  */
 typedef struct otNetworkDiagTlv
 {
-    /**
-     * The Network Diagnostic TLV type.
-     */
-    uint8_t mType;
+    uint8_t mType; ///< The Network Diagnostic TLV type.
 
     union
     {
@@ -294,6 +214,7 @@
         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];
+        char                      mVendorAppUrl[OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_APP_URL_TLV_LENGTH + 1];
         struct
         {
             uint8_t mCount;
@@ -302,13 +223,12 @@
         struct
         {
             uint8_t      mCount;
-            otIp6Address mList[OT_NETWORK_BASE_TLV_MAX_LENGTH / OT_IP6_ADDRESS_SIZE];
+            otIp6Address mList[OT_NETWORK_BASE_TLV_MAX_LENGTH / sizeof(otIp6Address)];
         } mIp6AddrList;
         struct
         {
-            uint8_t mCount;
-            otNetworkDiagChildEntry
-                mTable[OT_NETWORK_BASE_TLV_MAX_LENGTH / OT_NETWORK_DIAGNOSTIC_CHILD_TABLE_ENTRY_SIZE];
+            uint8_t                 mCount;
+            otNetworkDiagChildEntry mTable[OT_NETWORK_BASE_TLV_MAX_LENGTH / sizeof(otNetworkDiagChildEntry)];
         } mChildTable;
         struct
         {
@@ -419,16 +339,26 @@
 const char *otThreadGetVendorModel(otInstance *aInstance);
 
 /**
- * Get the vendor sw version string.
+ * Get the vendor software version string.
  *
  * @param[in]  aInstance      A pointer to an OpenThread instance.
  *
- * @returns The vendor sw version string.
+ * @returns The vendor software version string.
  *
  */
 const char *otThreadGetVendorSwVersion(otInstance *aInstance);
 
 /**
+ * Get the vendor app URL string.
+ *
+ * @param[in]  aInstance      A pointer to an OpenThread instance.
+ *
+ * @returns The vendor app URL string.
+ *
+ */
+const char *otThreadGetVendorAppUrl(otInstance *aInstance);
+
+/**
  * Set the vendor name string.
  *
  * Requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`.
@@ -480,6 +410,23 @@
 otError otThreadSetVendorSwVersion(otInstance *aInstance, const char *aVendorSwVersion);
 
 /**
+ * Set the vendor app URL string.
+ *
+ * Requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`.
+ *
+ * @p aVendorAppUrl should be UTF8 with max length of 64 chars (`MAX_VENDOR_APPL_URL_TLV_LENGTH`). Maximum length
+ * does not include the null `\0` character.
+ *
+ * @param[in] aInstance          A pointer to an OpenThread instance.
+ * @param[in] aVendorAppUrl      The vendor app URL string.
+ *
+ * @retval OT_ERROR_NONE          Successfully set the vendor app URL string.
+ * @retval OT_ERROR_INVALID_ARGS  @p aVendorAppUrl is not valid (too long or not UTF8).
+ *
+ */
+otError otThreadSetVendorAppUrl(otInstance *aInstance, const char *aVendorAppUrl);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/platform/border_routing.h b/include/openthread/platform/border_routing.h
index eeb825c..aed4bdb 100644
--- a/include/openthread/platform/border_routing.h
+++ b/include/openthread/platform/border_routing.h
@@ -65,6 +65,25 @@
  */
 extern void otPlatBorderRoutingProcessIcmp6Ra(otInstance *aInstance, const uint8_t *aMessage, uint16_t aLength);
 
+/**
+ * Process a prefix received from the DHCPv6 PD Server. The prefix is received on
+ * the DHCPv6 PD client callback and provided to the Routing Manager via this
+ * API.
+ *
+ * The prefix lifetime can be updated by calling the function again with updated time values.
+ * If the preferred lifetime of the prefix is set to 0, the prefix becomes deprecated.
+ * When this function is called multiple times, the smallest prefix is preferred as this rule allows
+ * choosing a GUA instead of a ULA.
+ *
+ * Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE`.
+ *
+ * @param[in] aInstance   A pointer to an OpenThread instance.
+ * @param[in] aPrefixInfo A pointer to the prefix information structure
+ *
+ */
+extern void otPlatBorderRoutingProcessDhcp6PdPrefix(otInstance                            *aInstance,
+                                                    const otBorderRoutingPrefixTableEntry *aPrefixInfo);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/include/openthread/platform/mdns_socket.h b/include/openthread/platform/mdns_socket.h
new file mode 100644
index 0000000..cc2bed9
--- /dev/null
+++ b/include/openthread/platform/mdns_socket.h
@@ -0,0 +1,173 @@
+/*
+ *  Copyright (c) 2024, 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 the platform abstraction for mDNS socket.
+ *
+ */
+
+#ifndef OPENTHREAD_PLATFORM_MULTICAST_DNS_SOCKET_H_
+#define OPENTHREAD_PLATFORM_MULTICAST_DNS_SOCKET_H_
+
+#include <stdint.h>
+
+#include <openthread/instance.h>
+#include <openthread/ip6.h>
+#include <openthread/message.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @addtogroup plat-mdns
+ *
+ * @brief
+ *   This module defines platform APIs for Multicast DNS (mDNS) socket.
+ *
+ * @{
+ *
+ */
+
+/**
+ * Represents a socket address info.
+ *
+ */
+typedef struct otPlatMdnsAddressInfo
+{
+    otIp6Address mAddress;      ///< IP address. IPv4-mapped IPv6 format should be used to represent IPv4 address.
+    uint16_t     mPort;         ///< Port number.
+    uint32_t     mInfraIfIndex; ///< Interface index.
+} otPlatMdnsAddressInfo;
+
+/**
+ * Enables or disables listening for mDNS messages sent to mDNS port 5353.
+ *
+ * When listening is enabled, the platform MUST listen for multicast messages sent to UDP destination port 5353 at the
+ * mDNS link-local multicast address `224.0.0.251` and its IPv6 equivalent `ff02::fb`.
+ *
+ * The platform SHOULD also listen for any unicast messages sent to UDP destination port 5353. If this is not possible,
+ * then OpenThread mDNS module can be configured to not use any "QU" questions requesting unicast response.
+ *
+ * While enabled, all received messages MUST be reported back using `otPlatMdnsHandleReceive()` callback.
+ *
+ * @param[in] aInstance        The OpernThread instance.
+ * @param[in] aEnable          Indicate whether to enable or disable.
+ * @param[in] aInfraInfIndex   The infrastructure network interface index.
+ *
+ * @retval OT_ERROR_NONE     Successfully enabled/disabled listening for mDNS messages.
+ * @retval OT_ERROR_FAILED   Failed to enable/disable listening for mDNS messages.
+ *
+ */
+otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex);
+
+/**
+ * Sends an mDNS message as multicast.
+ *
+ * The platform MUST multicast the prepared mDNS message in @p aMessage as a UDP message using the mDNS well-known port
+ * number 5353 for both source and destination ports. The message MUST be sent to the mDNS link-local multicast
+ * address `224.0.0.251` and/or its IPv6 equivalent `ff02::fb`.
+ *
+ * @p aMessage contains the mDNS message starting with DNS header at offset zero. It does not include IP or UDP headers.
+ * This function passes the ownership of @p aMessage to the platform layer and platform implementation MUST free
+ * @p aMessage once sent and no longer needed.
+ *
+ * The platform MUST allow multicast loopback, i.e., the multicast message @p aMessage MUST also be received and
+ * passed back to OpenThread stack using `otPlatMdnsHandleReceive()` callback. This behavior is essential for the
+ * OpenThread mDNS stack to process and potentially respond to its own queries, while allowing other mDNS receivers
+ * to also receive the query and its response.
+ *
+ * @param[in] aInstance       The OpenThread instance.
+ * @param[in] aMessage        The mDNS message to multicast. Ownership is transferred to the platform layer.
+ * @param[in] aInfraIfIndex   The infrastructure network interface index.
+ *
+ */
+void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex);
+
+/**
+ * Sends an mDNS message as unicast.
+ *
+ * The platform MUST send the prepared mDNS message in @p aMessage as a UDP message using source UDP port 5353 to
+ * the destination address and port number specified by @p aAddress.
+ *
+ * @p aMessage contains the DNS message starting with the DNS header at offset zero. It does not include IP or UDP
+ * headers. This function passes the ownership of @p aMessage to the platform layer and platform implementation
+ * MUST free @p aMessage once sent and no longer needed.
+ *
+ * The @p aAddress fields are as follows:
+ *
+ * - `mAddress` specifies the destination address. IPv4-mapped IPv6 format is used to represent an IPv4 destination.
+ * - `mPort` specifies the destination port.
+ * - `mInfraIndex` specifies the interface index.
+ *
+ * If the @aAddress matches this devices address, the platform MUST ensure to receive and pass the message back to
+ * the OpenThread stack using `otPlatMdnsHandleReceive()` for processing.
+ *
+ * @param[in] aInstance   The OpenThread instance.
+ * @param[in] aMessage    The mDNS message to multicast. Ownership is transferred to platform layer.
+ * @param[in] aAddress    The destination address info.
+ *
+ */
+void otPlatMdnsSendUnicast(otInstance *aInstance, otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress);
+
+/**
+ * Callback to notify OpenThread mDNS module of a received message on UDP port 5353.
+ *
+ * @p aMessage MUST contain DNS message starting with the DNS header at offset zero. This function passes the
+ * ownership of @p aMessage from the platform layer to the OpenThread stack. The OpenThread stack will free the
+ * message once processed.
+ *
+ * The @p aAddress fields are as follows:
+ *
+ * - `mAddress` specifies the sender's address. IPv4-mapped IPv6 format is used to represent an IPv4 destination.
+ * - `mPort` specifies the sender's port.
+ * - `mInfraIndex` specifies the interface index.
+ *
+ * @param[in] aInstance    The OpenThread instance.
+ * @param[in] aMessage     The received mDNS message. Ownership is transferred to the OpenThread stack.
+ * @param[in] aIsUnicast   Indicates whether the received message is unicast or multicast.
+ * @param[in] aAddress     The sender's address info.
+ *
+ */
+extern void otPlatMdnsHandleReceive(otInstance                  *aInstance,
+                                    otMessage                   *aMessage,
+                                    bool                         aIsUnicast,
+                                    const otPlatMdnsAddressInfo *aAddress);
+
+/**
+ * @}
+ *
+ */
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // OPENTHREAD_PLATFORM_MULTICAST_DNS_SOCKET_H_
diff --git a/include/openthread/thread.h b/include/openthread/thread.h
index b077001..4d814c1 100644
--- a/include/openthread/thread.h
+++ b/include/openthread/thread.h
@@ -709,7 +709,7 @@
  * @sa otThreadSetKeySwitchGuardTime
  *
  */
-uint32_t otThreadGetKeySwitchGuardTime(otInstance *aInstance);
+uint16_t otThreadGetKeySwitchGuardTime(otInstance *aInstance);
 
 /**
  * Sets the thrKeySwitchGuardTime (in hours).
@@ -723,7 +723,7 @@
  * @sa otThreadGetKeySwitchGuardTime
  *
  */
-void otThreadSetKeySwitchGuardTime(otInstance *aInstance, uint32_t aKeySwitchGuardTime);
+void otThreadSetKeySwitchGuardTime(otInstance *aInstance, uint16_t aKeySwitchGuardTime);
 
 /**
  * Detach from the Thread network.
diff --git a/include/openthread/verhoeff_checksum.h b/include/openthread/verhoeff_checksum.h
new file mode 100644
index 0000000..bbd53ab
--- /dev/null
+++ b/include/openthread/verhoeff_checksum.h
@@ -0,0 +1,101 @@
+/*
+ *  Copyright (c) 2024, 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 APIs for Verhoeff checksum calculation and validation.
+ */
+
+#ifndef OPENTHREAD_VERHOEFF_CHECKSUM_H_
+#define OPENTHREAD_VERHOEFF_CHECKSUM_H_
+
+#include <openthread/error.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @addtogroup api-verhoeff-checksum
+ *
+ * @brief
+ *   This module includes functions for Verhoeff checksum calculation and validation.
+ *
+ *   The functions in this module are available when `OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE` is enabled.
+ *
+ * @{
+ *
+ */
+
+/**
+ * Specifies the maximum length of decimal string input in `otVerhoeffChecksum` functions.
+ *
+ */
+#define OT_VERHOEFF_CHECKSUM_MAX_STRING_LENGTH 128
+
+/**
+ * Calculates the Verhoeff checksum for a given decimal string.
+ *
+ * Requires `OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE`.
+ *
+ * @param[in]  aDecimalString   The string containing decimal digits.
+ * @param[out] aChecksum        Pointer to a `char` to return the calculated checksum.
+ *
+ * @retval OT_ERROR_NONE            Successfully calculated the checksum, @p aChecksum is updated.
+ * @retval OT_ERROR_INVALID_ARGS    The @p aDecimalString is not valid, i.e. it either contains chars other than
+ *                                  ['0'-'9'], or is longer than `OT_VERHOEFF_CHECKSUM_MAX_STRING_LENGTH`.
+ *
+ */
+otError otVerhoeffChecksumCalculate(const char *aDecimalString, char *aChecksum);
+
+/**
+ * Validates the Verhoeff checksum for a given decimal string.
+ *
+ * Requires `OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE`.
+ *
+ * @param[in] aDecimalString   The string containing decimal digits (last char is treated as checksum).
+ *
+ * @retval OT_ERROR_NONE            Successfully validated the checksum in @p aDecimalString.
+ * @retval OT_ERROR_FAILED          Checksum validation failed.
+ * @retval OT_ERROR_INVALID_ARGS    The @p aDecimalString is not valid, i.e. it either contains chars other than
+ *                                  ['0'-'9'], or is longer than `OT_VERHOEFF_CHECKSUM_MAX_STRING_LENGTH`.
+ *
+ */
+otError otVerhoeffChecksumValidate(const char *aDecimalString);
+
+/**
+ * @}
+ *
+ */
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // OPENTHREAD_VERHOEFF_CHECKSUM_H_
diff --git a/include/simul_utils.c b/include/simul_utils.c
new file mode 100644
index 0000000..36b69af
--- /dev/null
+++ b/include/simul_utils.c
@@ -0,0 +1,228 @@
+/*
+ *  Copyright (c) 2024, 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 "simul_utils.h"
+
+#include <errno.h>
+#include <sys/time.h>
+
+#include "utils/code_utils.h"
+
+#define UTILS_SOCKET_LOCAL_HOST_ADDR "127.0.0.1"
+#define UTILS_SOCKET_GROUP_ADDR "224.0.0.116"
+
+const char *gLocalHost = UTILS_SOCKET_LOCAL_HOST_ADDR;
+
+void utilsAddFdToFdSet(int aFd, fd_set *aFdSet, int *aMaxFd)
+{
+    otEXPECT(aFd >= 0);
+    otEXPECT(aFdSet != NULL);
+
+    FD_SET(aFd, aFdSet);
+
+    otEXPECT(aMaxFd != NULL);
+
+    if (*aMaxFd < aFd)
+    {
+        *aMaxFd = aFd;
+    }
+
+exit:
+    return;
+}
+
+void utilsInitSocket(utilsSocket *aSocket, uint16_t aPortBase)
+{
+    int                fd;
+    int                one = 1;
+    int                rval;
+    struct sockaddr_in sockaddr;
+    struct ip_mreqn    mreq;
+
+    aSocket->mInitialized = false;
+    aSocket->mPortBase    = aPortBase;
+    aSocket->mPort        = (uint16_t)(aSocket->mPortBase + gNodeId);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Prepare `mTxFd`
+
+    fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+    otEXPECT_ACTION(fd != -1, perror("socket(TxFd)"));
+
+    memset(&sockaddr, 0, sizeof(sockaddr));
+    sockaddr.sin_family      = AF_INET;
+    sockaddr.sin_port        = htons(aSocket->mPort);
+    sockaddr.sin_addr.s_addr = inet_addr(gLocalHost);
+
+    rval = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &sockaddr.sin_addr, sizeof(sockaddr.sin_addr));
+    otEXPECT_ACTION(rval != -1, perror("setsockopt(TxFd, IP_MULTICAST_IF)"));
+
+    rval = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &one, sizeof(one));
+    otEXPECT_ACTION(rval != -1, perror("setsockopt(TxFd, IP_MULTICAST_LOOP)"));
+
+    rval = bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
+    otEXPECT_ACTION(rval != -1, perror("bind(TxFd)"));
+
+    aSocket->mTxFd = fd;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Prepare `mRxFd`
+
+    fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+    otEXPECT_ACTION(fd != -1, perror("socket(RxFd)"));
+
+    rval = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+    otEXPECT_ACTION(rval != -1, perror("setsockopt(RxFd, SO_REUSEADDR)"));
+
+    rval = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
+    otEXPECT_ACTION(rval != -1, perror("setsockopt(RxFd, SO_REUSEPORT)"));
+
+    memset(&mreq, 0, sizeof(mreq));
+    inet_pton(AF_INET, UTILS_SOCKET_GROUP_ADDR, &mreq.imr_multiaddr);
+
+    mreq.imr_address.s_addr = inet_addr(gLocalHost);
+
+    rval = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &mreq.imr_address, sizeof(mreq.imr_address));
+    otEXPECT_ACTION(rval != -1, perror("setsockopt(RxFd, IP_MULTICAST_IF)"));
+
+    rval = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
+    otEXPECT_ACTION(rval != -1, perror("setsockopt(RxFd, IP_ADD_MEMBERSHIP)"));
+
+    sockaddr.sin_family      = AF_INET;
+    sockaddr.sin_port        = htons(aSocket->mPortBase);
+    sockaddr.sin_addr.s_addr = inet_addr(UTILS_SOCKET_GROUP_ADDR);
+
+    rval = bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
+    otEXPECT_ACTION(rval != -1, perror("bind(RxFd)"));
+
+    aSocket->mRxFd = fd;
+
+    aSocket->mInitialized = true;
+
+exit:
+    if (!aSocket->mInitialized)
+    {
+        exit(EXIT_FAILURE);
+    }
+}
+
+void utilsDeinitSocket(utilsSocket *aSocket)
+{
+    if (aSocket->mInitialized)
+    {
+        close(aSocket->mRxFd);
+        close(aSocket->mTxFd);
+        aSocket->mInitialized = false;
+    }
+}
+
+void utilsAddSocketRxFd(const utilsSocket *aSocket, fd_set *aFdSet, int *aMaxFd)
+{
+    otEXPECT(aSocket->mInitialized);
+    utilsAddFdToFdSet(aSocket->mRxFd, aFdSet, aMaxFd);
+
+exit:
+    return;
+}
+
+void utilsAddSocketTxFd(const utilsSocket *aSocket, fd_set *aFdSet, int *aMaxFd)
+{
+    otEXPECT(aSocket->mInitialized);
+    utilsAddFdToFdSet(aSocket->mTxFd, aFdSet, aMaxFd);
+
+exit:
+    return;
+}
+
+bool utilsCanSocketReceive(const utilsSocket *aSocket, const fd_set *aReadFdSet)
+{
+    return aSocket->mInitialized && FD_ISSET(aSocket->mRxFd, aReadFdSet);
+}
+
+bool utilsCanSocketSend(const utilsSocket *aSocket, const fd_set *aWriteFdSet)
+{
+    return aSocket->mInitialized && FD_ISSET(aSocket->mTxFd, aWriteFdSet);
+}
+
+uint16_t utilsReceiveFromSocket(const utilsSocket *aSocket,
+                                void              *aBuffer,
+                                uint16_t           aBufferSize,
+                                uint16_t          *aSenderNodeId)
+{
+    struct sockaddr_in sockaddr;
+    socklen_t          socklen = sizeof(sockaddr);
+    ssize_t            rval;
+    uint16_t           len = 0;
+
+    memset(&sockaddr, 0, sizeof(sockaddr));
+
+    rval = recvfrom(aSocket->mRxFd, (char *)aBuffer, aBufferSize, 0, (struct sockaddr *)&sockaddr, &socklen);
+
+    if (rval > 0)
+    {
+        uint16_t senderPort = ntohs(sockaddr.sin_port);
+
+        if (aSenderNodeId != NULL)
+        {
+            *aSenderNodeId = (uint16_t)(senderPort - aSocket->mPortBase);
+        }
+
+        len = (uint16_t)rval;
+    }
+    else if (rval == 0)
+    {
+        assert(false);
+    }
+    else if (errno != EINTR && errno != EAGAIN)
+    {
+        perror("recvfrom(RxFd)");
+        exit(EXIT_FAILURE);
+    }
+
+    return len;
+}
+
+void utilsSendOverSocket(const utilsSocket *aSocket, const void *aBuffer, uint16_t aBufferLength)
+{
+    ssize_t            rval;
+    struct sockaddr_in sockaddr;
+
+    memset(&sockaddr, 0, sizeof(sockaddr));
+    sockaddr.sin_family = AF_INET;
+    sockaddr.sin_port   = htons(aSocket->mPortBase);
+    inet_pton(AF_INET, UTILS_SOCKET_GROUP_ADDR, &sockaddr.sin_addr);
+
+    rval =
+        sendto(aSocket->mTxFd, (const char *)aBuffer, aBufferLength, 0, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
+
+    if (rval < 0)
+    {
+        perror("sendto(sTxFd)");
+        exit(EXIT_FAILURE);
+    }
+}
diff --git a/include/simul_utils.h b/include/simul_utils.h
new file mode 100644
index 0000000..0d70f2d
--- /dev/null
+++ b/include/simul_utils.h
@@ -0,0 +1,151 @@
+/*
+ *  Copyright (c) 2024, 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 PLATFORM_SIMULATION_SOCKET_UTILS_H_
+#define PLATFORM_SIMULATION_SOCKET_UTILS_H_
+
+#include "platform-simulation.h"
+
+/**
+ * Represents a socket for communication with other simulation node.
+ *
+ * This is used for emulation of 15.4 radio or other interfaces.
+ *
+ */
+typedef struct utilsSocket
+{
+    bool     mInitialized; ///< Whether or not initialized.
+    int      mTxFd;        ///< RX file descriptor.
+    int      mRxFd;        ///< TX file descriptor.
+    uint16_t mPortBase;    ///< Base port number value.
+    uint16_t mPort;        ///< The port number used by this node
+} utilsSocket;
+
+extern const char *gLocalHost; ///< Local host address to use for sockets
+
+/**
+ * Adds a file descriptor (FD) to a given FD set.
+ *
+ * @param[in] aFd      The FD to add.
+ * @param[in] aFdSet   The FD set to add to.
+ * @param[in] aMaxFd   A pointer to track maximum FD in @p aFdSet (can be NULL).
+ *
+ */
+void utilsAddFdToFdSet(int aFd, fd_set *aFdSet, int *aMaxFd);
+
+/**
+ * Initializes the socket.
+ *
+ * @param[in] aSocket     The socket to initialize.
+ * @param[in] aPortBase   The base port number value. Nodes will determine their port as `aPortBased + gNodeId`.
+ *
+ */
+void utilsInitSocket(utilsSocket *aSocket, uint16_t aPortBase);
+
+/**
+ * De-initializes the socket.
+ *
+ * @param[in] aSocket   The socket to de-initialize.
+ *
+ */
+void utilsDeinitSocket(utilsSocket *aSocket);
+
+/**
+ * Adds sockets RX FD to a given FD set.
+ *
+ * @param[in] aSocket   The socket.
+ * @param[in] aFdSet    The (read) FD set to add to.
+ * @param[in] aMaxFd    A pointer to track maximum FD in @p aFdSet (can be NULL).
+ *
+ */
+void utilsAddSocketRxFd(const utilsSocket *aSocket, fd_set *aFdSet, int *aMaxFd);
+
+/**
+ * Adds sockets TX FD to a given FD set.
+ *
+ * @param[in] aSocket   The socket.
+ * @param[in] aFdSet    The (write) FD set to add to.
+ * @param[in] aMaxFd    A pointer to track maximum FD in @p aFdSet (can be NULL).
+ *
+ */
+void utilsAddSocketTxFd(const utilsSocket *aSocket, fd_set *aFdSet, int *aMaxFd);
+
+/**
+ * Indicates whether the socket can receive.
+ *
+ * @param[in] aSocket       The socket.
+ * @param[in] aReadFdSet    The read FD set.
+ *
+ * @retval TRUE   The socket RX FD is in @p aReadFdSet, and socket can receive.
+ * @retval FALSE  The socket RX FD is not in @p aReadFdSet. Socket is not ready to receive.
+ *
+ */
+bool utilsCanSocketReceive(const utilsSocket *aSocket, const fd_set *aReadFdSet);
+
+/**
+ * Indicates whether the socket can send.
+ *
+ * @param[in] aSocket   The socket.
+ * @param[in] aFdSet    The write FD set.
+ *
+ * @retval TRUE   The socket TX FD is in @p aWriteFdSet, and socket can send.
+ * @retval FALSE  The socket TX FD is not in @p aWriteFdSet. Socket is not ready to send.
+ *
+ */
+bool utilsCanSocketSend(const utilsSocket *aSocket, const fd_set *aWriteFdSet);
+
+/**
+ * Receives data from socket.
+ *
+ * MUST be used when `utilsCanSocketReceive()` returns `TRUE.
+ *
+ * @param[in] aSocket          The socket.
+ * @param[out] aBuffer         The buffer to output the read content.
+ * @param[in]  aBufferSize     Maximum size of buffer in bytes.
+ * @param[out] aSenderNodeId   A pointer to return the Node ID of the sender (derived from the port number).
+ *                             Can be NULL if not needed.
+ *
+ * @returns The number of received bytes written into @p aBuffer.
+ *
+ */
+uint16_t utilsReceiveFromSocket(const utilsSocket *aSocket,
+                                void              *aBuffer,
+                                uint16_t           aBufferSize,
+                                uint16_t          *aSenderNodeId);
+
+/**
+ * Sends data over the socket.
+ *
+ * @param[in] aSocket         The socket.
+ * @param[in] aBuffer         The buffer containing the bytes to sent.
+ * @param[in]  aBufferSize    Size of data in @p buffer in bytes.
+ *
+ */
+void utilsSendOverSocket(const utilsSocket *aSocket, const void *aBuffer, uint16_t aBufferLength);
+
+#endif // PLATFORM_SIMULATION_SOCKET_UTILS_H_
diff --git a/openthread_upstream_version.gni b/openthread_upstream_version.gni
index d4d9111..c82c4e9 100644
--- a/openthread_upstream_version.gni
+++ b/openthread_upstream_version.gni
@@ -1,3 +1,3 @@
 # This file is added to support soft-transition in Fuchsia.
 
-openthread_upstream_version = "49c59ec519cc8b49dd58978d1bc80b7ae7ba88d0"
+openthread_upstream_version = "93f311376434605e03c796a7543e3f0289fd3acf"
diff --git a/script/check-scan-build b/script/check-scan-build
index 2e1463c..4cedb76 100755
--- a/script/check-scan-build
+++ b/script/check-scan-build
@@ -61,6 +61,7 @@
     "-DOT_JOINER=ON"
     "-DOT_LOG_LEVEL_DYNAMIC=ON"
     "-DOT_MAC_FILTER=ON"
+    "-DOT_MDNS=ON"
     "-DOT_MESH_DIAG=ON"
     "-DOT_NAT64_BORDER_ROUTING=ON"
     "-DOT_NAT64_TRANSLATOR=ON"
diff --git a/script/cmake-build b/script/cmake-build
index 5ed2e4e..f8e227c 100755
--- a/script/cmake-build
+++ b/script/cmake-build
@@ -109,6 +109,7 @@
     "-DOT_SRP_CLIENT=ON"
     "-DOT_SRP_SERVER=ON"
     "-DOT_UPTIME=ON"
+    "-DOT_BLE_TCAT=ON"
 )
 readonly OT_POSIX_SIM_COMMON_OPTIONS
 
diff --git a/script/make-pretty b/script/make-pretty
index 491d4ac..84b6204 100755
--- a/script/make-pretty
+++ b/script/make-pretty
@@ -95,6 +95,7 @@
     '-DOT_BORDER_ROUTING=ON'
     '-DOT_BORDER_ROUTING_DHCP6_PD=ON'
     '-DOT_CHANNEL_MANAGER=ON'
+    '-DOT_CHANNEL_MANAGER_CSL=ON'
     '-DOT_CHANNEL_MONITOR=ON'
     '-DOT_COAP=ON'
     '-DOT_COAP_BLOCK=ON'
@@ -121,6 +122,7 @@
     '-DOT_LINK_METRICS_INITIATOR=ON'
     '-DOT_LINK_METRICS_SUBJECT=ON'
     '-DOT_MAC_FILTER=ON'
+    '-DOT_MDNS=ON'
     '-DOT_MESH_DIAG=ON'
     '-DOT_NAT64_BORDER_ROUTING=ON'
     '-DOT_NAT64_TRANSLATOR=ON'
diff --git a/src/cli/BUILD.gn b/src/cli/BUILD.gn
index d37de23..38d166f 100644
--- a/src/cli/BUILD.gn
+++ b/src/cli/BUILD.gn
@@ -53,10 +53,10 @@
   "cli_link_metrics.hpp",
   "cli_mac_filter.cpp",
   "cli_mac_filter.hpp",
+  "cli_mdns.cpp",
+  "cli_mdns.hpp",
   "cli_network_data.cpp",
   "cli_network_data.hpp",
-  "cli_output.cpp",
-  "cli_output.hpp",
   "cli_ping.cpp",
   "cli_ping.hpp",
   "cli_srp_client.cpp",
@@ -67,6 +67,8 @@
   "cli_tcp.hpp",
   "cli_udp.cpp",
   "cli_udp.hpp",
+  "cli_utils.cpp",
+  "cli_utils.hpp",
   "x509_cert_key.hpp",
 ]
 
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
index f36439f..3d9a32a 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -44,14 +44,15 @@
     cli_joiner.cpp
     cli_link_metrics.cpp
     cli_mac_filter.cpp
+    cli_mdns.cpp
     cli_network_data.cpp
-    cli_output.cpp
     cli_ping.cpp
     cli_srp_client.cpp
     cli_srp_server.cpp
     cli_tcat.cpp
     cli_tcp.cpp
     cli_udp.cpp
+    cli_utils.cpp
 )
 
 set(OT_CLI_VENDOR_EXTENSION "" CACHE STRING "Path to CMake file to define and link cli vendor extension")
diff --git a/src/cli/README.md b/src/cli/README.md
index 262b9f5..2921b19 100644
--- a/src/cli/README.md
+++ b/src/cli/README.md
@@ -86,6 +86,7 @@
 - [networkkey](#networkkey)
 - [networkname](#networkname)
 - [networktime](#networktime)
+- [nexthop](#nexthop)
 - [panid](#panid)
 - [parent](#parent)
 - [parentpriority](#parentpriority)
@@ -129,6 +130,7 @@
 - [unsecureport](#unsecureport-add-port)
 - [uptime](#uptime)
 - [vendor](#vendor-name)
+- [verhoeff](#verhoeff-calculate)
 - [version](#version)
 
 ## OpenThread Command Details
@@ -357,12 +359,99 @@
 
 Print border agent state.
 
+Possible states are
+
+- `Stopped` : Border Agent is stopped.
+- `Started` : Border Agent is running with no active connection with external commissioner.
+- `Active` : Border Agent is running and is connected with an external commissioner.
+
 ```bash
 > ba state
 Started
 Done
 ```
 
+### ba ephemeralkey
+
+Indicates if an ephemeral key is active.
+
+Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE`.
+
+```bash
+> ba ephemeralkey
+inactive
+Done
+
+> ba ephemeralkey set Z10X20g3J15w1000P60m16 1000
+Done
+
+> ba ephemeralkey
+active
+Done
+```
+
+### ba ephemeralkey set \<keystring\> \[timeout\] \[port\]
+
+Sets the ephemeral key for a given timeout duration.
+
+Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE`.
+
+The ephemeral key can be set when Border Agent is already running and is not currently connected to any external commissioner (i.e., `ba state` gives `Started`).
+
+The `keystring` string is directly used as the ephemeral PSK (excluding the trailing null `\0` character). Its length MUST be between 6 and 32, inclusive.
+
+The `timeout` is in milliseconds. If not provided or set to zero, the default value of 2 minutes will be used. If the timeout value is larger than 10 minutes, the 10 minutes timeout value will be used instead.
+
+The `port` specifies the UDP port to use with the ephemeral key. If UDP port is zero or is not provided, an ephemeral port will be used. `ba port` will give the current UDP port in use by the Border Agent.
+
+Setting the ephemeral key again before a previously set one is timed out, will replace the previous one.
+
+While the timeout interval is in effect, the ephemeral key can be used only once by an external commissioner to connect. Once the commissioner disconnects, the ephemeral key is cleared, and Border Agent reverts to using PSKc.
+
+```bash
+> ba ephemeralkey set Z10X20g3J15w1000P60m16 5000 1234
+Done
+```
+
+### ba ephemeralkey clear
+
+Cancels the ephemeral key in use if any.
+
+Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE`.
+
+Can be used to cancel a previously set ephemeral key before it is used or times out. If the Border Agent is not running or there is no ephemeral key in use, calling this function has no effect.
+
+If a commissioner is connected using the ephemeral key and is currently active, calling this method does not change its state. In this case the `ba ephemeralkey` will continue to return `active` until the commissioner disconnects.
+
+```bash
+> ba ephemeralkey clear
+Done
+```
+
+### ba ephemeralkey callback enable
+
+Enables callback from Border Agent for ephemeral key state changes.
+
+```bash
+> ba ephemeralkey callback enable
+Done
+
+> ba ephemeralkey set W10X12 5000 49155
+Done
+
+BorderAgent callback: Ephemeral key active, port:49155
+BorderAgent callback: Ephemeral key inactive
+```
+
+### ba ephemeralkey callback disable
+
+Disables callback from Border Agent for ephemeral key state changes.
+
+```bash
+> ba ephemeralkey callback disable
+Done
+```
+
 ### bufferinfo
 
 Show the current message buffer information.
@@ -1026,30 +1115,6 @@
 Done
 ```
 
-### networktime
-
-Get the Thread network time and the time sync parameters.
-
-```bash
-> networktime
-Network Time:     21084154us (synchronized)
-Time Sync Period: 100s
-XTAL Threshold:   300ppm
-Done
-```
-
-### networktime \<timesyncperiod\> \<xtalthreshold\>
-
-Set 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.
-
-```bash
-> networktime 100 300
-Done
-```
-
 ### debug
 
 Executes a series of CLI commands to gather information about the device and thread network. This is intended for debugging.
@@ -1762,6 +1827,8 @@
 
 Set the Thread Key Sequence Counter.
 
+This command is reserved for testing and demo purposes only. Changing Key Sequence Counter will render a production application non-compliant with the Thread Specification.
+
 ```bash
 > keysequence counter 10
 Done
@@ -1779,7 +1846,9 @@
 
 ### keysequence guardtime \<guardtime\>
 
-Set Thread Key Switch Guard Time (in hours) 0 means Thread Key Switch immediately if key index match
+Set Thread Key Switch Guard Time (in hours).
+
+This command is reserved for testing and demo purposes only. Changing Key Switch Guard Time will render a production application non-compliant with the Thread Specification.
 
 ```bash
 > keysequence guardtime 0
@@ -2696,6 +2765,61 @@
 Done
 ```
 
+### networktime
+
+Get the Thread network time and the time sync parameters.
+
+```bash
+> networktime
+Network Time:     21084154us (synchronized)
+Time Sync Period: 100s
+XTAL Threshold:   300ppm
+Done
+```
+
+### networktime \<timesyncperiod\> \<xtalthreshold\>
+
+Set 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.
+
+```bash
+> networktime 100 300
+Done
+```
+
+### nexthop
+
+Output the table of allocated Router IDs and the current next hop (as Router ID) and path cost for each ID.
+
+```bash
+> nexthop
+| ID   |NxtHop| Cost |
++------+------+------+
+|    9 |    9 |    1 |
+|   25 |   25 |    0 |
+|   30 |   30 |    1 |
+|   46 |    - |    - |
+|   50 |   30 |    3 |
+|   60 |   30 |    2 |
+Done
+```
+
+### nexthop \<rloc16\>
+
+Get the next hop (as RLOC16) and path cost towards a given RLOC16 destination.
+
+```bash
+> nexthop 0xc000
+0xc000 cost:0
+Done
+
+nexthop 0x8001
+0x2000 cost:3
+Done
+```
+
 ### panid
 
 Get the IEEE 802.15.4 PAN ID value.
@@ -3841,6 +3965,35 @@
 Done
 ```
 
+### verhoeff calculate
+
+Calculates the Verhoeff checksum for a given decimal string.
+
+Requires `OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE`.
+
+The input string MUST consist of characters in `['0'-'9']`.
+
+```bash
+> verhoeff calculate 30731842
+1
+Done
+```
+
+### verhoeff validate
+
+Validates the Verhoeff checksum for a given decimal string.
+
+Requires `OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE`.
+
+The input string MUST consist of characters in `['0'-'9']`. The last digit is treated as checksum.
+
+```bash
+> verhoeff validate 307318421
+Done
+> verhoeff validate 307318425
+Error 1: Failed
+```
+
 ### version
 
 Print the build version information.
diff --git a/src/cli/README_BR.md b/src/cli/README_BR.md
index 189de8a..8376290 100644
--- a/src/cli/README_BR.md
+++ b/src/cli/README_BR.md
@@ -36,6 +36,7 @@
 onlinkprefix
 pd
 prefixtable
+raoptions
 rioprf
 routeprf
 routers
@@ -235,6 +236,28 @@
 Done
 ```
 
+### raoptions
+
+Usage: `br raoptions <options>`
+
+Sets additional options to append at the end of emitted Router Advertisement (RA) messages. `<options>` provided as hex bytes.
+
+```bash
+> br raoptions 0400ff00020001
+Done
+```
+
+### raoptions clear
+
+Usage: `br raoptions clear`
+
+Clear any previously set additional options to append at the end of emitted Router Advertisement (RA) messages.
+
+```bash
+> br raoptions clear
+Done
+```
+
 ### rioprf
 
 Usage: `br rioprf`
diff --git a/src/cli/README_COMMISSIONER.md b/src/cli/README_COMMISSIONER.md
index 013b416..5dc1152 100644
--- a/src/cli/README_COMMISSIONER.md
+++ b/src/cli/README_COMMISSIONER.md
@@ -1,5 +1,9 @@
 # OpenThread CLI - Commissioner
 
+## Overview
+
+The Commissioner is an entity that can add new Thread devices securely to a Thread network. It also can manage a Thread network by changing network configuration parameters ([Operational Datasets](README_DATASET.md)) or sending specific management/diagnostic commands to selected Thread devices. Before a Commissioner can do its tasks, it has to petition to the Leader to get permission to become the Commissioner.
+
 ## Quick Start
 
 See [README_COMMISSIONING.md](README_COMMISSIONING.md).
diff --git a/src/cli/README_COMMISSIONING.md b/src/cli/README_COMMISSIONING.md
index 8262a8c..857bb7a 100644
--- a/src/cli/README_COMMISSIONING.md
+++ b/src/cli/README_COMMISSIONING.md
@@ -1,10 +1,14 @@
 # OpenThread CLI - Commissioning
 
+## Overview
+
+Commissioning is the process of adding a new Thread device, called the Joiner, to a Thread network. This process is done under guidance of a [Commissioner](README_COMMISSIONER.md).
+
 ## Quick Start
 
 ### Form Network
 
-Form a network with the device that has Commissioner support.
+Form a network with the Thread device that has Commissioner support.
 
 1. Generate and view new network configuration.
 
diff --git a/src/cli/README_DATASET.md b/src/cli/README_DATASET.md
index 54f4c1e..abed508 100644
--- a/src/cli/README_DATASET.md
+++ b/src/cli/README_DATASET.md
@@ -4,6 +4,15 @@
 
 Thread network configuration parameters are managed using Active and Pending Operational Dataset objects.
 
+### WARNING - Restrictions for production use!
+
+The CLI commands to write/change the Active and Pending Operational Datasets may allow setting invalid parameters, or invalid combinations of parameters, for testing purposes. These CLI commands can only be used:
+
+- To configure network parameters for the first device in a newly created Thread network.
+- For testing (not applicable to production devices).
+
+In production Thread networks, the correct method to write/change Operational Datasets is via a [Commissioner](README_COMMISSIONER.md) that performs [commissioning](README_COMMISSIONING.md). Production devices that are not an active Commissioner and are part of a Thread network MUST NOT modify the Operational Datasets in any way.
+
 ### Active Operational Dataset
 
 The Active Operational Dataset includes parameters that are currently in use across an entire Thread network. The Active Operational Dataset contains:
@@ -107,6 +116,58 @@
    Done
    ```
 
+### Using the Dataset Updater to update Operational Dataset
+
+Dataset Updater can be used for a delayed update of network parameters on all devices of a Thread Network.
+
+1. Clear the dataset buffer and add the Dataset fields to update.
+
+   ```bash
+   > dataset clear
+   Done
+
+   > dataset channel 12
+   Done
+   ```
+
+2. Set the delay timer parameter (example uses 5 minutes or 300000 ms). Check the resulting dataset. There is no need to specify active or pending timestamps because the Dataset Updater will handle this. If specified the `dataset updater start` will issue an error.
+
+   ```bash
+   > dataset delay 300000
+
+   > dataset
+   Channel: 12
+   Delay: 30000
+   Done
+   ```
+
+3. Start the Dataset Updater, which will prepare a Pending Operation Dataset and inform the Leader to distribute it to other devices.
+
+   ```bash
+   > dataset updater start
+   Done
+
+   > dataset updater
+   Enabled
+   ```
+
+4. After about 5 minutes, the changes are applied to the Active Operational Dataset on the Leader. This can also be checked at other devices on the network: these should have applied the new Dataset too, at approximately the same time as the Leader has done this.
+
+   ```bash
+   > dataset active
+   Active Timestamp: 10
+   Channel: 12
+   Channel Mask: 0x07fff800
+   Ext PAN ID: 324a71d90cdc8345
+   Mesh Local Prefix: fd7d:da74:df5e:80c::/64
+   Network Key: be768535bac1b8d228960038311d6ca2
+   Network Name: OpenThread-bcaf
+   PAN ID: 0xbcaf
+   PSKc: e79b274ab22414a814ed5cce6a30be67
+   Security Policy: 672 onrc 0
+   Done
+   ```
+
 ### Using the Pending Operational Dataset for Delayed Dataset Updates
 
 The Pending Operational Dataset can be used for a delayed update of network parameters on all devices of a Thread Network. If certain Active Operational Dataset parameters need to be changed, but the change would impact the connectivity of the network, delaying the update helps to let all devices receive the new parameters before the update is applied. Examples of such parameters are the channel, PAN ID, certain Security Policy bits, or Network Key.
@@ -241,6 +302,7 @@
 - [pskc](#pskc)
 - [securitypolicy](#securitypolicy)
 - [tlvs](#tlvs)
+- [updater](#updater)
 
 ## Command Details
 
@@ -695,3 +757,74 @@
 0e080000000000010000000300001635060004001fffe00208d196fa2040e973b60708fdbbc310c48f3a3905109929154dbc363218bcd22f907caf5c15030f4f70656e5468726561642d646532620102de2b041015b2c16f7ba92ed4bc7b1ee054f1553f0c0402a0f7f8
 Done
 ```
+
+### updater
+
+Usage: `dataset updater`
+
+Requires `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE`.
+
+Indicate whether there is an ongoing Operation Dataset update request.
+
+```bash
+> dataset updater
+Enabled
+```
+
+### updater start
+
+Usage: `dataset updater start`
+
+Requires `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE`.
+
+Request network to update its Operation Dataset to the current operational dataset buffer.
+
+The current operational dataset buffer should contain the fields to be updated with their new values. It must not contain Active or Pending Timestamp fields. The Delay field is optional. If not provided, a default value (1000 ms) is used.
+
+```bash
+> channel
+19
+Done
+
+> dataset clear
+Done
+> dataset channel 15
+Done
+> dataset
+Channel: 15
+Done
+
+> dataset updater start
+Done
+> dataset updater
+Enabled
+Done
+
+Dataset update complete: OK
+
+> channel
+15
+Done
+```
+
+### updater cancel
+
+Usage: `dataset updater cancel`
+
+Requires `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE`.
+
+Cancels an ongoing (if any) Operational Dataset update request.
+
+```bash
+> dataset updater start
+Done
+> dataset updater
+Enabled
+Done
+>
+> dataset updater cancel
+Done
+> dataset updater
+Disabled
+Done
+```
diff --git a/src/cli/README_NETDATA.md b/src/cli/README_NETDATA.md
index ad33bb9..9402885 100644
--- a/src/cli/README_NETDATA.md
+++ b/src/cli/README_NETDATA.md
@@ -334,10 +334,12 @@
 
 ### show
 
-Usage: `netdata show [local] [-x]`
+Usage: `netdata show [local] [-x] [\<rloc16\>]`
 
 Print entries in Network Data, on-mesh prefixes, external routes, services, and 6LoWPAN context information.
 
+If the optional `rloc16` input is specified, prints the entries associated with the given RLOC16 only. The RLOC16 filtering can be used when `-x` or `local` are not used.
+
 On-mesh prefixes are listed under `Prefixes` header:
 
 - The on-mesh prefix
@@ -406,6 +408,19 @@
 Done
 ```
 
+Print Network Data entries from the Leader associated with `0xa00` RLOC16.
+
+```bash
+> netdata show 0xa00
+Prefixes:
+fd00:dead:beef:cafe::/64 paros med a000
+Routes:
+fd00:1234:0:0::/64 s med a000
+Services:
+44970 5d fddead00beef00007bad0069ce45948504d2 s a000
+Done
+```
+
 Print Network Data received from the Leader as hex-encoded TLVs.
 
 ```bash
diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp
index 4540334..79a2969 100644
--- a/src/cli/cli.cpp
+++ b/src/cli/cli.cpp
@@ -45,6 +45,7 @@
 #include <openthread/logging.h>
 #include <openthread/ncp.h>
 #include <openthread/thread.h>
+#include <openthread/verhoeff_checksum.h>
 #include "common/num_utils.hpp"
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 #include <openthread/network_time.h>
@@ -68,7 +69,9 @@
 #include <openthread/backbone_router_ftd.h>
 #endif
 #endif
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
 #include <openthread/channel_manager.h>
 #endif
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
@@ -99,7 +102,7 @@
 
 Interpreter::Interpreter(Instance *aInstance, otCliOutputCallback aCallback, void *aContext)
     : OutputImplementer(aCallback, aContext)
-    , Output(aInstance, *this)
+    , Utils(aInstance, *this)
     , mCommandIsPending(false)
     , mInternalDebugCommand(false)
     , mTimer(*aInstance, HandleTimer, this)
@@ -116,6 +119,9 @@
 #if OPENTHREAD_CLI_DNS_ENABLE
     , mDns(aInstance, *this)
 #endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+    , mMdns(aInstance, *this)
+#endif
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
     , mBbr(aInstance, *this)
 #endif
@@ -337,7 +343,7 @@
         VerifyOrExit(StringLength(aBuf, kMaxLineLength) <= kMaxLineLength - 1, error = OT_ERROR_PARSE);
     }
 
-    SuccessOrExit(error = Utils::CmdLineParser::ParseCmd(aBuf, args, kMaxArgs));
+    SuccessOrExit(error = ot::Utils::CmdLineParser::ParseCmd(aBuf, args, kMaxArgs));
     VerifyOrExit(!args[0].IsEmpty(), mCommandIsPending = false);
 
     if (!mInternalDebugCommand)
@@ -408,26 +414,6 @@
     return error;
 }
 
-otError Interpreter::ParseEnableOrDisable(const Arg &aArg, bool &aEnable)
-{
-    otError error = OT_ERROR_NONE;
-
-    if (aArg == "enable")
-    {
-        aEnable = true;
-    }
-    else if (aArg == "disable")
-    {
-        aEnable = false;
-    }
-    else
-    {
-        error = OT_ERROR_INVALID_COMMAND;
-    }
-
-    return error;
-}
-
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
 
 otError Interpreter::ParseJoinerDiscerner(Arg &aArg, otJoinerDiscerner &aDiscerner)
@@ -441,7 +427,7 @@
 
     VerifyOrExit(separator != nullptr, error = OT_ERROR_NOT_FOUND);
 
-    SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(separator + 1, aDiscerner.mLength));
+    SuccessOrExit(error = ot::Utils::CmdLineParser::ParseAsUint8(separator + 1, aDiscerner.mLength));
     VerifyOrExit(aDiscerner.mLength > 0 && aDiscerner.mLength <= 64, error = OT_ERROR_INVALID_ARGS);
     *separator = '\0';
     error      = aArg.ParseAsUint64(aDiscerner.mValue);
@@ -504,12 +490,13 @@
                                        otIp6Address &aAddress,
                                        bool         &aSynthesized)
 {
-    Error error = kErrorNone;
+    Error error = OT_ERROR_NONE;
 
     VerifyOrExit(!aArg.IsEmpty(), error = OT_ERROR_INVALID_ARGS);
     error        = aArg.ParseAsIp6Address(aAddress);
     aSynthesized = false;
-    if (error != kErrorNone)
+
+    if (error != OT_ERROR_NONE)
     {
         // It might be an IPv4 address, let's have a try.
         otIp4Address ip4Address;
@@ -609,6 +596,98 @@
         }
     }
 #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    else if (aArgs[0] == "ephemeralkey")
+    {
+        /**
+         * @cli ba ephemeralkey
+         * @code
+         * ba ephemeralkey
+         * active
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otBorderAgentIsEphemeralKeyActive
+         */
+        if (aArgs[1].IsEmpty())
+        {
+            OutputLine("%sactive", otBorderAgentIsEphemeralKeyActive(GetInstancePtr()) ? "" : "in");
+        }
+        /**
+         * @cli ba ephemeralkey set <keystring> [timeout-in-msec] [port]
+         * @code
+         * ba ephemeralkey set Z10X20g3J15w1000P60m16 5000 1234
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otBorderAgentSetEphemeralKey
+         */
+        else if (aArgs[1] == "set")
+        {
+            uint32_t timeout = 0;
+            uint16_t port    = 0;
+
+            VerifyOrExit(!aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+            if (!aArgs[3].IsEmpty())
+            {
+                SuccessOrExit(error = aArgs[3].ParseAsUint32(timeout));
+            }
+
+            if (!aArgs[4].IsEmpty())
+            {
+                SuccessOrExit(error = aArgs[4].ParseAsUint16(port));
+            }
+
+            error = otBorderAgentSetEphemeralKey(GetInstancePtr(), aArgs[2].GetCString(), timeout, port);
+        }
+        /**
+         * @cli ba ephemeralkey clear
+         * @code
+         * ba ephemeralkey clear
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otBorderAgentClearEphemeralKey
+         */
+        else if (aArgs[1] == "clear")
+        {
+            otBorderAgentClearEphemeralKey(GetInstancePtr());
+        }
+        /**
+         * @cli ba ephemeralkey callback (enable, disable)
+         * @code
+         * ba ephemeralkey callback enable
+         * Done
+         * ba ephemeralkey set W10X1 5000 49155
+         * Done
+         * BorderAgent callback: Ephemeral key active, port:49155
+         * BorderAgent callback: Ephemeral key inactive
+         * @endcode
+         * @par api_copy
+         * #otBorderAgentSetEphemeralKeyCallback
+         */
+        else if (aArgs[1] == "callback")
+        {
+            bool enable;
+
+            SuccessOrExit(error = ParseEnableOrDisable(aArgs[2], enable));
+
+            if (enable)
+            {
+                otBorderAgentSetEphemeralKeyCallback(GetInstancePtr(), HandleBorderAgentEphemeralKeyStateChange, this);
+            }
+            else
+            {
+                otBorderAgentSetEphemeralKeyCallback(GetInstancePtr(), nullptr, nullptr);
+            }
+        }
+        else
+        {
+            error = OT_ERROR_INVALID_ARGS;
+        }
+    }
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
     else
     {
         ExitNow(error = OT_ERROR_INVALID_COMMAND);
@@ -617,6 +696,28 @@
 exit:
     return error;
 }
+
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+void Interpreter::HandleBorderAgentEphemeralKeyStateChange(void *aContext)
+{
+    reinterpret_cast<Interpreter *>(aContext)->HandleBorderAgentEphemeralKeyStateChange();
+}
+
+void Interpreter::HandleBorderAgentEphemeralKeyStateChange(void)
+{
+    bool active = otBorderAgentIsEphemeralKeyActive(GetInstancePtr());
+
+    OutputFormat("BorderAgent callback: Ephemeral key %sactive", active ? "" : "in");
+
+    if (active)
+    {
+        OutputFormat(", port:%u", otBorderAgentGetUdpPort(GetInstancePtr()));
+    }
+
+    OutputNewLine();
+}
+#endif
+
 #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
@@ -758,9 +859,6 @@
      */
     else if (aArgs[0] == "mappings")
     {
-        otNat64AddressMappingIterator iterator;
-        otNat64AddressMapping         mapping;
-
         static const char *const kNat64StatusLevel1Title[] = {"", "Address", "", "4 to 6", "6 to 4"};
 
         static const uint8_t kNat64StatusLevel1ColumnWidths[] = {
@@ -775,6 +873,9 @@
             18, 42, 18, 8, 10, 14, 10, 14,
         };
 
+        otNat64AddressMappingIterator iterator;
+        otNat64AddressMapping         mapping;
+
         OutputTableHeader(kNat64StatusLevel1Title, kNat64StatusLevel1ColumnWidths);
         OutputTableHeader(kNat64StatusTableHeader, kNat64StatusTableColumnWidths);
 
@@ -1329,7 +1430,9 @@
         }
     }
 #endif // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
     else if (aArgs[0] == "manager")
     {
         /**
@@ -1346,26 +1449,42 @@
          * @endcode
          * @par
          * Get the channel manager state.
-         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` is required.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE` is required.
          * @sa otChannelManagerGetRequestedChannel
          */
         if (aArgs[1].IsEmpty())
         {
             OutputLine("channel: %u", otChannelManagerGetRequestedChannel(GetInstancePtr()));
+#if OPENTHREAD_FTD
             OutputLine("auto: %d", otChannelManagerGetAutoChannelSelectionEnabled(GetInstancePtr()));
+#endif
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+            OutputLine("autocsl: %u", otChannelManagerGetAutoCslChannelSelectionEnabled(GetInstancePtr()));
+#endif
 
+#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && \
+     OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+            if (otChannelManagerGetAutoChannelSelectionEnabled(GetInstancePtr()) ||
+                otChannelManagerGetAutoCslChannelSelectionEnabled(GetInstancePtr()))
+#elif OPENTHREAD_FTD
             if (otChannelManagerGetAutoChannelSelectionEnabled(GetInstancePtr()))
+#elif (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+            if (otChannelManagerGetAutoCslChannelSelectionEnabled(GetInstancePtr()))
+#endif
             {
                 Mac::ChannelMask supportedMask(otChannelManagerGetSupportedChannels(GetInstancePtr()));
                 Mac::ChannelMask favoredMask(otChannelManagerGetFavoredChannels(GetInstancePtr()));
-
+#if OPENTHREAD_FTD
                 OutputLine("delay: %u", otChannelManagerGetDelay(GetInstancePtr()));
+#endif
                 OutputLine("interval: %lu", ToUlong(otChannelManagerGetAutoChannelSelectionInterval(GetInstancePtr())));
                 OutputLine("cca threshold: 0x%04x", otChannelManagerGetCcaFailureRateThreshold(GetInstancePtr()));
                 OutputLine("supported: %s", supportedMask.ToString().AsCString());
                 OutputLine("favored: %s", favoredMask.ToString().AsCString());
             }
         }
+#if OPENTHREAD_FTD
         /**
          * @cli channel manager change
          * @code
@@ -1394,7 +1513,9 @@
          * @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.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`, and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE`
+         * are required.
          * @par api_copy
          * #otChannelManagerRequestChannelSelect
          */
@@ -1405,7 +1526,7 @@
             SuccessOrExit(error = aArgs[2].ParseAsBool(enable));
             error = otChannelManagerRequestChannelSelect(GetInstancePtr(), enable);
         }
-#endif
+#endif // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
         /**
          * @cli channel manager auto
          * @code
@@ -1416,7 +1537,9 @@
          * @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.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`, and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE`
+         * are required.
          * @par api_copy
          * #otChannelManagerSetAutoChannelSelectionEnabled
          */
@@ -1427,6 +1550,32 @@
             SuccessOrExit(error = aArgs[2].ParseAsBool(enable));
             otChannelManagerSetAutoChannelSelectionEnabled(GetInstancePtr(), enable);
         }
+#endif // OPENTHREAD_FTD
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+        /**
+         * @cli channel manager autocsl
+         * @code
+         * channel manager autocsl 1
+         * Done
+         * @endcode
+         * @cparam channel manager autocsl @ca{enable}
+         * `1` is a boolean to `enable`.
+         * @par
+         * Enables or disables the auto channel selection functionality for a CSL channel.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`, and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE`
+         * are required.
+         * @sa otChannelManagerSetAutoCslChannelSelectionEnabled
+         */
+        else if (aArgs[1] == "autocsl")
+        {
+            bool enable;
+
+            SuccessOrExit(error = aArgs[2].ParseAsBool(enable));
+            otChannelManagerSetAutoCslChannelSelectionEnabled(GetInstancePtr(), enable);
+        }
+#endif // (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+#if OPENTHREAD_FTD
         /**
          * @cli channel manager delay
          * @code
@@ -1444,6 +1593,7 @@
         {
             error = ProcessGetSet(aArgs + 2, otChannelManagerGetDelay, otChannelManagerSetDelay);
         }
+#endif
         /**
          * @cli channel manager interval
          * @code
@@ -1453,7 +1603,9 @@
          * @endcode
          * @cparam channel manager interval @ca{interval-seconds}
          * @par
-         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`, and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE`
+         * are required.
          * @par api_copy
          * #otChannelManagerSetAutoChannelSelectionInterval
          */
@@ -1470,7 +1622,9 @@
          * @endcode
          * @cparam channel manager supported @ca{mask}
          * @par
-         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`, and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE`
+         * are required.
          * @par api_copy
          * #otChannelManagerSetSupportedChannels
          */
@@ -1487,7 +1641,9 @@
          * @endcode
          * @cparam channel manager favored @ca{mask}
          * @par
-         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`, and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE`
+         * are required.
          * @par api_copy
          * #otChannelManagerSetFavoredChannels
          */
@@ -1505,7 +1661,9 @@
          * @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.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` or `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE &&
+         * OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE`, and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE`
+         * are required.
          * @par api_copy
          * #otChannelManagerSetCcaFailureRateThreshold
          */
@@ -2370,9 +2528,9 @@
      */
     if (aArgs[0].IsEmpty())
     {
-        OutputLine("Channel: %u", otLinkGetCslChannel(GetInstancePtr()));
-        OutputLine("Period: %luus", ToUlong(otLinkGetCslPeriod(GetInstancePtr())));
-        OutputLine("Timeout: %lus", ToUlong(otLinkGetCslTimeout(GetInstancePtr())));
+        OutputLine("channel: %u", otLinkGetCslChannel(GetInstancePtr()));
+        OutputLine("period: %luus", ToUlong(otLinkGetCslPeriod(GetInstancePtr())));
+        OutputLine("timeout: %lus", ToUlong(otLinkGetCslTimeout(GetInstancePtr())));
     }
     /**
      * @cli csl channel
@@ -2608,6 +2766,10 @@
 template <> otError Interpreter::Process<Cmd("dns")>(Arg aArgs[]) { return mDns.Process(aArgs); }
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+template <> otError Interpreter::Process<Cmd("mdns")>(Arg aArgs[]) { return mMdns.Process(aArgs); }
+#endif
+
 #if OPENTHREAD_FTD
 void Interpreter::OutputEidCacheEntry(const otCacheEntryInfo &aEntry)
 {
@@ -3650,8 +3812,6 @@
 {
     otError error = OT_ERROR_NONE;
 
-    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-
     /**
      * @cli linkmetricsmgr (enable,disable)
      * @code
@@ -3667,7 +3827,7 @@
      * #otLinkMetricsManagerSetEnabled
      *
      */
-    if (ProcessEnableDisable(aArgs, otLinkMetricsManagerSetEnabled) == OT_ERROR_NONE)
+    if (ProcessEnableDisable(aArgs, otLinkMetricsManagerIsEnabled, otLinkMetricsManagerSetEnabled) == OT_ERROR_NONE)
     {
     }
     /**
@@ -3706,7 +3866,6 @@
         error = OT_ERROR_INVALID_COMMAND;
     }
 
-exit:
     return error;
 }
 #endif // OPENTHREAD_CONFIG_LINK_METRICS_MANAGER_ENABLE
@@ -7563,7 +7722,7 @@
         }
         else
         {
-            VerifyOrExit(aArgs[1].IsEmpty(), error = kErrorInvalidArgs);
+            VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
         }
 
         if (isTable)
@@ -7682,7 +7841,7 @@
         error = ProcessGet(aArgs, otThreadGetVendorName);
 #else
         /**
-         * @cli vendor name (name)
+         * @cli vendor name (set)
          * @code
          * vendor name nest
          * Done
@@ -7712,7 +7871,7 @@
         error = ProcessGet(aArgs, otThreadGetVendorModel);
 #else
         /**
-         * @cli vendor model (name)
+         * @cli vendor model (set)
          * @code
          * vendor model Hub\ Max
          * Done
@@ -7742,7 +7901,7 @@
         error = ProcessGet(aArgs, otThreadGetVendorSwVersion);
 #else
         /**
-         * @cli vendor swversion (version)
+         * @cli vendor swversion (set)
          * @code
          * vendor swversion Marble3.5.1
          * Done
@@ -7754,6 +7913,36 @@
         error = ProcessGetSet(aArgs, otThreadGetVendorSwVersion, otThreadSetVendorSwVersion);
 #endif
     }
+    /**
+     * @cli vendor appurl
+     * @code
+     * vendor appurl
+     * http://www.example.com
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetVendorAppUrl
+     */
+    else if (aArgs[0] == "appurl")
+    {
+        aArgs++;
+
+#if !OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+        error = ProcessGet(aArgs, otThreadGetVendorAppUrl);
+#else
+        /**
+         * @cli vendor appurl (set)
+         * @code
+         * vendor appurl http://www.example.com
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otThreadSetVendorAppUrl
+         * @cparam vendor appurl @ca{url}
+         */
+        error = ProcessGetSet(aArgs, otThreadGetVendorAppUrl, otThreadSetVendorAppUrl);
+#endif
+    }
 
     return error;
 }
@@ -7762,9 +7951,11 @@
 
 template <> otError Interpreter::Process<Cmd("networkdiagnostic")>(Arg aArgs[])
 {
+    static constexpr uint16_t kMaxTlvs = 35;
+
     otError      error = OT_ERROR_NONE;
     otIp6Address address;
-    uint8_t      tlvTypes[OT_NETWORK_DIAGNOSTIC_TYPELIST_MAX_ENTRIES];
+    uint8_t      tlvTypes[kMaxTlvs];
     uint8_t      count = 0;
 
     SuccessOrExit(error = aArgs[1].ParseAsIp6Address(address));
@@ -7829,6 +8020,7 @@
      * - `28`: Thread Stack Version TLV (version identifier as UTF-8 string for Thread stack codebase/commit/version)
      * - `29`: Child TLV
      * - `34`: MLE Counters TLV
+     * - `35`: Vendor App URL TLV
      * @par
      * Sends a network diagnostic request to retrieve specified Type Length Values (TLVs)
      * for the specified addresses(es).
@@ -7996,6 +8188,9 @@
         case OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_SW_VERSION:
             OutputLine("Vendor SW Version: %s", diagTlv.mData.mVendorSwVersion);
             break;
+        case OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_APP_URL:
+            OutputLine("Vendor App URL: %s", diagTlv.mData.mVendorAppUrl);
+            break;
         case OT_NETWORK_DIAGNOSTIC_TLV_THREAD_STACK_VERSION:
             OutputLine("Thread Stack Version: %s", diagTlv.mData.mThreadStackVersion);
             break;
@@ -8165,6 +8360,57 @@
 }
 #endif
 
+#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
+template <> otError Interpreter::Process<Cmd("verhoeff")>(Arg aArgs[])
+{
+    otError error;
+
+    /**
+     * @cli verhoeff calculate
+     * @code
+     * verhoeff calculate 30731842
+     * 1
+     * Done
+     * @endcode
+     * @cparam verhoeff calculate @ca{decimalstring}
+     * @par api_copy
+     * #otVerhoeffChecksumCalculate
+     */
+    if (aArgs[0] == "calculate")
+    {
+        char checksum;
+
+        VerifyOrExit(!aArgs[1].IsEmpty() && aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+        SuccessOrExit(error = otVerhoeffChecksumCalculate(aArgs[1].GetCString(), &checksum));
+        OutputLine("%c", checksum);
+    }
+    /**
+     * @cli verhoeff validate
+     * @code
+     * verhoeff validate 307318421
+     * Done
+     * @endcode
+     * @cparam verhoeff validate @ca{decimalstring}
+     * @par api_copy
+     * #otVerhoeffChecksumValidate
+     */
+    else if (aArgs[0] == "validate")
+    {
+        VerifyOrExit(!aArgs[1].IsEmpty() && aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+        error = otVerhoeffChecksumValidate(aArgs[1].GetCString());
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
 
 void Interpreter::Initialize(otInstance *aInstance, otCliOutputCallback aCallback, void *aContext)
@@ -8174,76 +8420,6 @@
     Interpreter::sInterpreter = new (&sInterpreterRaw) Interpreter(instance, aCallback, aContext);
 }
 
-otError Interpreter::ProcessEnableDisable(Arg aArgs[], SetEnabledHandler aSetEnabledHandler)
-{
-    otError error = OT_ERROR_NONE;
-    bool    enable;
-
-    if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE)
-    {
-        aSetEnabledHandler(GetInstancePtr(), enable);
-    }
-    else
-    {
-        error = OT_ERROR_INVALID_COMMAND;
-    }
-
-    return error;
-}
-
-otError Interpreter::ProcessEnableDisable(Arg aArgs[], SetEnabledHandlerFailable aSetEnabledHandler)
-{
-    otError error = OT_ERROR_NONE;
-    bool    enable;
-
-    if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE)
-    {
-        error = aSetEnabledHandler(GetInstancePtr(), enable);
-    }
-    else
-    {
-        error = OT_ERROR_INVALID_COMMAND;
-    }
-
-    return error;
-}
-
-otError Interpreter::ProcessEnableDisable(Arg               aArgs[],
-                                          IsEnabledHandler  aIsEnabledHandler,
-                                          SetEnabledHandler aSetEnabledHandler)
-{
-    otError error = OT_ERROR_NONE;
-
-    if (aArgs[0].IsEmpty())
-    {
-        OutputEnabledDisabledStatus(aIsEnabledHandler(GetInstancePtr()));
-    }
-    else
-    {
-        error = ProcessEnableDisable(aArgs, aSetEnabledHandler);
-    }
-
-    return error;
-}
-
-otError Interpreter::ProcessEnableDisable(Arg                       aArgs[],
-                                          IsEnabledHandler          aIsEnabledHandler,
-                                          SetEnabledHandlerFailable aSetEnabledHandler)
-{
-    otError error = OT_ERROR_NONE;
-
-    if (aArgs[0].IsEmpty())
-    {
-        OutputEnabledDisabledStatus(aIsEnabledHandler(GetInstancePtr()));
-    }
-    else
-    {
-        error = ProcessEnableDisable(aArgs, aSetEnabledHandler);
-    }
-
-    return error;
-}
-
 void Interpreter::OutputPrompt(void)
 {
 #if OPENTHREAD_CONFIG_CLI_PROMPT_ENABLE
@@ -8407,6 +8583,9 @@
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
         CmdEntry("macfilter"),
 #endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+        CmdEntry("mdns"),
+#endif
 #if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
         CmdEntry("meshdiag"),
 #endif
@@ -8533,6 +8712,9 @@
         CmdEntry("uptime"),
 #endif
         CmdEntry("vendor"),
+#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+        CmdEntry("verhoeff"),
+#endif
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
         CmdEntry("version"),
     };
diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp
index f698071..b808f74 100644
--- a/src/cli/cli.hpp
+++ b/src/cli/cli.hpp
@@ -61,20 +61,22 @@
 #include "cli/cli_bbr.hpp"
 #include "cli/cli_br.hpp"
 #include "cli/cli_commissioner.hpp"
+#include "cli/cli_config.h"
 #include "cli/cli_dataset.hpp"
 #include "cli/cli_dns.hpp"
 #include "cli/cli_history.hpp"
 #include "cli/cli_joiner.hpp"
 #include "cli/cli_link_metrics.hpp"
 #include "cli/cli_mac_filter.hpp"
+#include "cli/cli_mdns.hpp"
 #include "cli/cli_network_data.hpp"
-#include "cli/cli_output.hpp"
 #include "cli/cli_ping.hpp"
 #include "cli/cli_srp_client.hpp"
 #include "cli/cli_srp_server.hpp"
 #include "cli/cli_tcat.hpp"
 #include "cli/cli_tcp.hpp"
 #include "cli/cli_udp.hpp"
+#include "cli/cli_utils.hpp"
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
 #include "cli/cli_coap.hpp"
 #endif
@@ -108,7 +110,7 @@
  * Implements the CLI interpreter.
  *
  */
-class Interpreter : public OutputImplementer, public Output
+class Interpreter : public OutputImplementer, public Utils
 {
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
     friend class Br;
@@ -117,6 +119,7 @@
     friend class Dns;
     friend class Joiner;
     friend class LinkMetrics;
+    friend class Mdns;
     friend class NetworkData;
     friend class PingSender;
     friend class SrpClient;
@@ -128,8 +131,6 @@
     friend void otCliOutputFormat(const char *aFmt, ...);
 
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -179,19 +180,6 @@
     void ProcessLine(char *aBuf);
 
     /**
-     * Checks a given argument string against "enable" or "disable" commands.
-     *
-     * @param[in]  aArg     The argument string to parse.
-     * @param[out] aEnable  Boolean variable to return outcome on success.
-     *                      Set to TRUE for "enable" command, and FALSE for "disable" command.
-     *
-     * @retval OT_ERROR_NONE             Successfully parsed the @p aString and updated @p aEnable.
-     * @retval OT_ERROR_INVALID_COMMAND  The @p aString is not "enable" or "disable" command.
-     *
-     */
-    static otError ParseEnableOrDisable(const Arg &aArg, bool &aEnable);
-
-    /**
      * Adds commands to the user command table.
      *
      * @param[in]  aCommands  A pointer to an array with user commands.
@@ -258,14 +246,14 @@
      * 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]  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.
+     * @retval OT_ERROR_NONE           The argument was parsed successfully.
+     * @retval OT_ERROR_INVALID_ARGS   The argument is empty or does not contain a valid IP address.
+     * @retval OT_ERROR_INVALID_STATE  No valid NAT64 prefix in the network data.
      *
      */
     static otError ParseToIp6Address(otInstance   *aInstance,
@@ -289,94 +277,6 @@
 
     using Command = CommandEntry<Interpreter>;
 
-    template <typename ValueType> using GetHandler         = ValueType (&)(otInstance *);
-    template <typename ValueType> using SetHandler         = void (&)(otInstance *, ValueType);
-    template <typename ValueType> using SetHandlerFailable = otError (&)(otInstance *, ValueType);
-    using IsEnabledHandler                                 = bool (&)(otInstance *);
-    using SetEnabledHandler                                = void (&)(otInstance *, bool);
-    using SetEnabledHandlerFailable                        = otError (&)(otInstance *, bool);
-
-    // 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, 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;
-
-        VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        OutputLine(FormatStringFor<ValueType>(), aGetHandler(GetInstancePtr()));
-
-    exit:
-        return error;
-    }
-
-    template <typename ValueType> otError ProcessSet(Arg aArgs[], SetHandler<ValueType> aSetHandler)
-    {
-        otError   error;
-        ValueType value;
-
-        SuccessOrExit(error = aArgs[0].ParseAs<ValueType>(value));
-        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-
-        aSetHandler(GetInstancePtr(), value);
-
-    exit:
-        return error;
-    }
-
-    template <typename ValueType> otError ProcessSet(Arg aArgs[], SetHandlerFailable<ValueType> aSetHandler)
-    {
-        otError   error;
-        ValueType value;
-
-        SuccessOrExit(error = aArgs[0].ParseAs<ValueType>(value));
-        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-
-        error = aSetHandler(GetInstancePtr(), value);
-
-    exit:
-        return error;
-    }
-
-    template <typename ValueType>
-    otError ProcessGetSet(Arg aArgs[], GetHandler<ValueType> aGetHandler, SetHandler<ValueType> aSetHandler)
-    {
-        otError error = ProcessGet(aArgs, aGetHandler);
-
-        VerifyOrExit(error != OT_ERROR_NONE);
-        error = ProcessSet(aArgs, aSetHandler);
-
-    exit:
-        return error;
-    }
-
-    template <typename ValueType>
-    otError ProcessGetSet(Arg aArgs[], GetHandler<ValueType> aGetHandler, SetHandlerFailable<ValueType> aSetHandler)
-    {
-        otError error = ProcessGet(aArgs, aGetHandler);
-
-        VerifyOrExit(error != OT_ERROR_NONE);
-        error = ProcessSet(aArgs, aSetHandler);
-
-    exit:
-        return error;
-    }
-
-    otError ProcessEnableDisable(Arg aArgs[], SetEnabledHandler aSetEnabledHandler);
-    otError ProcessEnableDisable(Arg aArgs[], SetEnabledHandlerFailable aSetEnabledHandler);
-    otError ProcessEnableDisable(Arg aArgs[], IsEnabledHandler aIsEnabledHandler, SetEnabledHandler aSetEnabledHandler);
-    otError ProcessEnableDisable(Arg                       aArgs[],
-                                 IsEnabledHandler          aIsEnabledHandler,
-                                 SetEnabledHandlerFailable aSetEnabledHandler);
-
     void OutputPrompt(void);
     void OutputResult(otError aError);
 
@@ -495,6 +395,11 @@
     void HandleSntpResponse(uint64_t aTime, otError aResult);
 #endif
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    static void HandleBorderAgentEphemeralKeyStateChange(void *aContext);
+    void        HandleBorderAgentEphemeralKeyStateChange(void);
+#endif
+
     static void HandleDetachGracefullyResult(void *aContext);
     void        HandleDetachGracefullyResult(void);
 
@@ -544,6 +449,10 @@
     Dns mDns;
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+    Mdns mMdns;
+#endif
+
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
     Bbr mBbr;
 #endif
@@ -599,46 +508,6 @@
 #endif
 };
 
-// Specializations of `FormatStringFor<ValueType>()`
-
-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)
-{
-    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 otError Interpreter::ProcessGet<int32_t>(Arg aArgs[], GetHandler<int32_t> aGetHandler)
-{
-    otError error = OT_ERROR_NONE;
-
-    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-    OutputLine(FormatStringFor<int32_t>(), static_cast<long int>(aGetHandler(GetInstancePtr())));
-
-exit:
-    return error;
-}
-
 } // namespace Cli
 } // namespace ot
 
diff --git a/src/cli/cli_bbr.cpp b/src/cli/cli_bbr.cpp
index d37d3df..0f86b24 100644
--- a/src/cli/cli_bbr.cpp
+++ b/src/cli/cli_bbr.cpp
@@ -290,8 +290,7 @@
  */
 template <> otError Bbr::Process<Cmd("jitter")>(Arg aArgs[])
 {
-    return Interpreter::GetInterpreter().ProcessGetSet(aArgs, otBackboneRouterGetRegistrationJitter,
-                                                       otBackboneRouterSetRegistrationJitter);
+    return ProcessGetSet(aArgs, otBackboneRouterGetRegistrationJitter, otBackboneRouterSetRegistrationJitter);
 }
 
 /**
diff --git a/src/cli/cli_bbr.hpp b/src/cli/cli_bbr.hpp
index 40ed979..120518a 100644
--- a/src/cli/cli_bbr.hpp
+++ b/src/cli/cli_bbr.hpp
@@ -42,7 +42,7 @@
 #include <openthread/backbone_router_ftd.h>
 
 #include "cli/cli_config.h"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 namespace ot {
 namespace Cli {
@@ -51,11 +51,9 @@
  * Implements the BBR CLI interpreter.
  *
  */
-class Bbr : private Output
+class Bbr : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor.
      *
@@ -64,7 +62,7 @@
      *
      */
     Bbr(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_br.cpp b/src/cli/cli_br.cpp
index 9c77c7d..53f2b32 100644
--- a/src/cli/cli_br.cpp
+++ b/src/cli/cli_br.cpp
@@ -455,7 +455,7 @@
      * #otBorderRoutingDhcp6PdSetEnabled
      *
      */
-    if (Interpreter::GetInterpreter().ProcessEnableDisable(aArgs, otBorderRoutingDhcp6PdSetEnabled) == OT_ERROR_NONE)
+    if (ProcessEnableDisable(aArgs, otBorderRoutingDhcp6PdSetEnabled) == OT_ERROR_NONE)
     {
     }
     /**
@@ -538,6 +538,46 @@
                aEntry.mStubRouterFlag);
 }
 
+template <> otError Br::Process<Cmd("raoptions")>(Arg aArgs[])
+{
+    static constexpr uint16_t kMaxExtraOptions = 800;
+
+    otError  error = OT_ERROR_NONE;
+    uint8_t  options[kMaxExtraOptions];
+    uint16_t length;
+
+    /**
+     * @cli br raoptions (set,clear)
+     * @code
+     * br raoptions 0400ff00020001
+     * Done
+     * @endcode
+     * @code
+     * br raoptions clear
+     * Done
+     * @endcode
+     * @cparam br raoptions @ca{options|clear}
+     * `br raoptions clear` passes a `nullptr` to #otBorderRoutingSetExtraRouterAdvertOptions.
+     * Otherwise, you can pass the `options` byte as hex data.
+     * @par api_copy
+     * #otBorderRoutingSetExtraRouterAdvertOptions
+     */
+    if (aArgs[0] == "clear")
+    {
+        length = 0;
+    }
+    else
+    {
+        length = sizeof(options);
+        SuccessOrExit(error = aArgs[0].ParseAsHexString(length, options));
+    }
+
+    error = otBorderRoutingSetExtraRouterAdvertOptions(GetInstancePtr(), length > 0 ? options : nullptr, length);
+
+exit:
+    return error;
+}
+
 template <> otError Br::Process<Cmd("rioprf")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
@@ -702,6 +742,7 @@
         CmdEntry("pd"),
 #endif
         CmdEntry("prefixtable"),
+        CmdEntry("raoptions"),
         CmdEntry("rioprf"),
         CmdEntry("routeprf"),
         CmdEntry("routers"),
diff --git a/src/cli/cli_br.hpp b/src/cli/cli_br.hpp
index 01fcdcf..5a877a8 100644
--- a/src/cli/cli_br.hpp
+++ b/src/cli/cli_br.hpp
@@ -39,7 +39,7 @@
 #include <openthread/border_routing.h>
 
 #include "cli/cli_config.h"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
@@ -50,11 +50,9 @@
  * Implements the Border Router CLI interpreter.
  *
  */
-class Br : private Output
+class Br : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -63,7 +61,7 @@
      *
      */
     Br(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_coap.cpp b/src/cli/cli_coap.cpp
index f3b966f..5edbe00 100644
--- a/src/cli/cli_coap.cpp
+++ b/src/cli/cli_coap.cpp
@@ -45,7 +45,7 @@
 namespace Cli {
 
 Coap::Coap(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
     , mUseDefaultRequestTxParameters(true)
     , mUseDefaultResponseTxParameters(true)
 #if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
@@ -298,8 +298,9 @@
  * coap start
  * Done
  * @endcode
- * @par api_copy
- * #otCoapStart
+ * @par
+ * Starts the CoAP server. @moreinfo{@coap}.
+ * @sa otCoapStart
  */
 template <> otError Coap::Process<Cmd("start")>(Arg aArgs[])
 {
@@ -491,7 +492,7 @@
  *     the number of blocks to send. The `block-` type requires
  *     `OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE` to be set.
  * @par
- * Creates the specified CoAP resource.
+ * Creates the specified CoAP resource. @moreinfo{@coap}.
  */
 template <> otError Coap::Process<Cmd("post")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_POST); }
 
@@ -526,7 +527,7 @@
  *     the number of blocks to send. The `block-` type requires
  *     `OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE` to be set.
  * @par
- * Modifies the specified CoAP resource.
+ * Modifies the specified CoAP resource. @moreinfo{@coap}.
  */
 template <> otError Coap::Process<Cmd("put")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_PUT); }
 
@@ -948,7 +949,7 @@
         }
         else
         {
-            responseCode = OT_COAP_CODE_VALID;
+            responseCode = OT_COAP_CODE_CHANGED;
         }
 
         responseMessage = otCoapNewMessage(GetInstancePtr(), nullptr);
diff --git a/src/cli/cli_coap.hpp b/src/cli/cli_coap.hpp
index 2b4184a..95ce7b9 100644
--- a/src/cli/cli_coap.hpp
+++ b/src/cli/cli_coap.hpp
@@ -40,7 +40,7 @@
 
 #include <openthread/coap.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 namespace ot {
 namespace Cli {
@@ -49,11 +49,9 @@
  * Implements the CLI CoAP server and client.
  *
  */
-class Coap : private Output
+class Coap : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
diff --git a/src/cli/cli_coap_secure.cpp b/src/cli/cli_coap_secure.cpp
index c1bacdb..2ff4f68 100644
--- a/src/cli/cli_coap_secure.cpp
+++ b/src/cli/cli_coap_secure.cpp
@@ -47,7 +47,7 @@
 namespace Cli {
 
 CoapSecure::CoapSecure(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
     , mShutdownFlag(false)
     , mUseCertificate(false)
     , mPskLength(0)
@@ -103,7 +103,7 @@
  * @endcode
  * @cparam coaps resource [@ca{uri-path}]
  * @par
- * Gets or sets the URI path of the CoAPS server resource.
+ * Gets or sets the URI path of the CoAPS server resource. @moreinfo{@coaps}.
  * @sa otCoapSecureAddBlockWiseResource
  */
 template <> otError CoapSecure::Process<Cmd("resource")>(Arg aArgs[])
@@ -152,7 +152,7 @@
  * @endcode
  * @cparam coaps set @ca{new-content}
  * @par
- * Sets the content sent by the resource on the CoAPS server.
+ * Sets the content sent by the resource on the CoAPS server. @moreinfo{@coaps}.
  */
 template <> otError CoapSecure::Process<Cmd("set")>(Arg aArgs[])
 {
@@ -207,7 +207,7 @@
  *     `check-peer-cert` is `true`, and the `max-conn-attempts` value is the
  *     number specified in the argument.
  * @par
- * Starts the CoAP Secure service.
+ * Starts the CoAP Secure service. @moreinfo{@coaps}.
  * @sa otCoapSecureStart
  * @sa otCoapSecureSetSslAuthMode
  * @sa otCoapSecureSetClientConnectedCallback
@@ -255,7 +255,7 @@
  * Done
  * @endcode
  * @par
- * Stops the CoAP Secure service.
+ * Stops the CoAP Secure service. @moreinfo{@coaps}.
  * @sa otCoapSecureStop
  */
 template <> otError CoapSecure::Process<Cmd("stop")>(Arg aArgs[])
@@ -289,7 +289,7 @@
  * Done
  * @endcode
  * @par
- * Indicates if the CoAP Secure service is closed.
+ * Indicates if the CoAP Secure service is closed. @moreinfo{@coaps}.
  * @sa otCoapSecureIsClosed
  */
 template <> otError CoapSecure::Process<Cmd("isclosed")>(Arg aArgs[])
@@ -305,7 +305,7 @@
  * Done
  * @endcode
  * @par
- * Indicates if the CoAP Secure service is connected.
+ * Indicates if the CoAP Secure service is connected. @moreinfo{@coaps}.
  * @sa otCoapSecureIsConnected
  */
 template <> otError CoapSecure::Process<Cmd("isconnected")>(Arg aArgs[])
@@ -323,6 +323,7 @@
  * @par
  * Indicates if the CoAP Secure service connection is active
  * (either already connected or in the process of establishing a connection).
+ * @moreinfo{@coaps}.
  * @sa otCoapSecureIsConnectionActive
  */
 template <> otError CoapSecure::Process<Cmd("isconnactive")>(Arg aArgs[])
@@ -362,6 +363,7 @@
  *         `block-256`, `block-512`, or `block-1024`.
  * @par
  * Gets information about the specified CoAPS resource on the CoAPS server.
+ * @moreinfo{@coaps}.
  */
 template <> otError CoapSecure::Process<Cmd("get")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_GET); }
 
@@ -393,7 +395,7 @@
  *	   integer that specifies the number of blocks to send. The `block-` type
  *	   requires `OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE` to be set.
  * @par
- * Creates the specified CoAPS resource.
+ * Creates the specified CoAPS resource. @moreinfo{@coaps}.
  */
 template <> otError CoapSecure::Process<Cmd("post")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_POST); }
 
@@ -425,7 +427,7 @@
  *	   integer that specifies the number of blocks to send. The `block-` type
  *	   requires `OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE` to be set.
  * @par
- * Modifies the specified CoAPS resource.
+ * Modifies the specified CoAPS resource. @moreinfo{@coaps}.
  */
 template <> otError CoapSecure::Process<Cmd("put")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_PUT); }
 
@@ -610,6 +612,7 @@
  * The `address` parameter is the IPv6 address of the peer.
  * @par
  * Initializes a Datagram Transport Layer Security (DTLS) session with a peer.
+ * @moreinfo{@coaps}.
  * @sa otCoapSecureConnect
  */
 template <> otError CoapSecure::Process<Cmd("connect")>(Arg aArgs[])
diff --git a/src/cli/cli_coap_secure.hpp b/src/cli/cli_coap_secure.hpp
index a09dfed..92a7a95 100644
--- a/src/cli/cli_coap_secure.hpp
+++ b/src/cli/cli_coap_secure.hpp
@@ -42,7 +42,7 @@
 
 #include <openthread/coap_secure.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #ifndef CLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER
 #define CLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER 0
@@ -55,11 +55,9 @@
  * Implements the CLI CoAP Secure server and client.
  *
  */
-class CoapSecure : private Output
+class CoapSecure : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
diff --git a/src/cli/cli_commissioner.hpp b/src/cli/cli_commissioner.hpp
index 162c6da..a5feac4 100644
--- a/src/cli/cli_commissioner.hpp
+++ b/src/cli/cli_commissioner.hpp
@@ -38,7 +38,7 @@
 
 #include <openthread/commissioner.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
 
@@ -49,11 +49,9 @@
  * Implements the Commissioner CLI interpreter.
  *
  */
-class Commissioner : private Output
+class Commissioner : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -62,7 +60,7 @@
      *
      */
     Commissioner(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_dataset.cpp b/src/cli/cli_dataset.cpp
index cfd47a1..63e3203 100644
--- a/src/cli/cli_dataset.cpp
+++ b/src/cli/cli_dataset.cpp
@@ -1154,10 +1154,46 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli dataset updater
+     * @code
+     * dataset updater
+     * Enabled
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otDatasetUpdaterIsUpdateOngoing
+     */
     if (aArgs[0].IsEmpty())
     {
         OutputEnabledDisabledStatus(otDatasetUpdaterIsUpdateOngoing(GetInstancePtr()));
     }
+    /**
+     * @cli dataset updater start
+     * @code
+     * channel
+     * 19
+     * Done
+     * dataset clear
+     * Done
+     * dataset channel 15
+     * Done
+     * dataset
+     * Channel: 15
+     * Done
+     * dataset updater start
+     * Done
+     * dataset updater
+     * Enabled
+     * Done
+     * Dataset update complete: OK
+     * channel
+     * 15
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otDatasetUpdaterRequestUpdate
+     */
     else if (aArgs[0] == "start")
     {
         otOperationalDataset dataset;
@@ -1166,6 +1202,15 @@
         SuccessOrExit(
             error = otDatasetUpdaterRequestUpdate(GetInstancePtr(), &dataset, &Dataset::HandleDatasetUpdater, this));
     }
+    /**
+     * @cli dataset updater cancel
+     * @code
+     * @dataset updater cancel
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otDatasetUpdaterCancelUpdate
+     */
     else if (aArgs[0] == "cancel")
     {
         otDatasetUpdaterCancelUpdate(GetInstancePtr());
diff --git a/src/cli/cli_dataset.hpp b/src/cli/cli_dataset.hpp
index 8587862..06055e3 100644
--- a/src/cli/cli_dataset.hpp
+++ b/src/cli/cli_dataset.hpp
@@ -40,7 +40,7 @@
 
 #include <openthread/dataset.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 namespace ot {
 namespace Cli {
@@ -49,13 +49,11 @@
  * Implements the Dataset CLI interpreter.
  *
  */
-class Dataset : private Output
+class Dataset : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     Dataset(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_dns.cpp b/src/cli/cli_dns.cpp
index 7aef479..5148bb3 100644
--- a/src/cli/cli_dns.cpp
+++ b/src/cli/cli_dns.cpp
@@ -416,15 +416,15 @@
 
     otError error = OT_ERROR_NONE;
     bool    recursionDesired;
-    bool    nat64SynthesizedAddress;
+    bool    nat64Synth;
 
     ClearAllBytes(*aConfig);
 
     VerifyOrExit(!aArgs[0].IsEmpty(), aConfig = nullptr);
 
     SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], aConfig->mServerSockAddr.mAddress,
-                                                         nat64SynthesizedAddress));
-    if (nat64SynthesizedAddress)
+                                                         nat64Synth));
+    if (nat64Synth)
     {
         OutputFormat("Synthesized IPv6 DNS server address: ");
         OutputIp6AddressLine(aConfig->mServerSockAddr.mAddress);
@@ -679,8 +679,7 @@
          * @par api_copy
          * #otDnssdUpstreamQuerySetEnabled
          */
-        error = Interpreter::GetInterpreter().ProcessEnableDisable(aArgs + 1, otDnssdUpstreamQueryIsEnabled,
-                                                                   otDnssdUpstreamQuerySetEnabled);
+        error = ProcessEnableDisable(aArgs + 1, otDnssdUpstreamQueryIsEnabled, otDnssdUpstreamQuerySetEnabled);
     }
 #endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
     else
diff --git a/src/cli/cli_dns.hpp b/src/cli/cli_dns.hpp
index b9f1679..bf31ed7 100644
--- a/src/cli/cli_dns.hpp
+++ b/src/cli/cli_dns.hpp
@@ -54,7 +54,7 @@
 #include <openthread/dnssd_server.h>
 
 #include "cli/cli_config.h"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 namespace ot {
 namespace Cli {
@@ -63,11 +63,9 @@
  * Implements the DNS CLI interpreter.
  *
  */
-class Dns : private Output
+class Dns : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor.
      *
@@ -76,7 +74,7 @@
      *
      */
     Dns(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_history.hpp b/src/cli/cli_history.hpp
index 90f832f..6958b04 100644
--- a/src/cli/cli_history.hpp
+++ b/src/cli/cli_history.hpp
@@ -39,7 +39,7 @@
 #include <openthread/history_tracker.h>
 
 #include "cli/cli_config.h"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
 
@@ -50,11 +50,9 @@
  * Implements the History Tracker CLI interpreter.
  *
  */
-class History : private Output
+class History : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -63,7 +61,7 @@
      *
      */
     History(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_joiner.hpp b/src/cli/cli_joiner.hpp
index e45dbf8..297525c 100644
--- a/src/cli/cli_joiner.hpp
+++ b/src/cli/cli_joiner.hpp
@@ -38,7 +38,7 @@
 
 #include <openthread/joiner.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_JOINER_ENABLE
 
@@ -49,11 +49,9 @@
  * Implements the Joiner CLI interpreter.
  *
  */
-class Joiner : private Output
+class Joiner : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -62,7 +60,7 @@
      *
      */
     Joiner(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_link_metrics.cpp b/src/cli/cli_link_metrics.cpp
index 93cc8c5..a12515d 100644
--- a/src/cli/cli_link_metrics.cpp
+++ b/src/cli/cli_link_metrics.cpp
@@ -36,7 +36,7 @@
 #include <openthread/link_metrics.h>
 
 #include "cli/cli.hpp"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 #include "common/code_utils.hpp"
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
@@ -45,7 +45,7 @@
 namespace Cli {
 
 LinkMetrics::LinkMetrics(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
     , mLinkMetricsQueryInProgress(false)
 {
 }
diff --git a/src/cli/cli_link_metrics.hpp b/src/cli/cli_link_metrics.hpp
index d62be78..40b67c6 100644
--- a/src/cli/cli_link_metrics.hpp
+++ b/src/cli/cli_link_metrics.hpp
@@ -38,7 +38,7 @@
 
 #include <openthread/link_metrics.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
 
@@ -50,11 +50,9 @@
  *
  */
 
-class LinkMetrics : private Output
+class LinkMetrics : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
diff --git a/src/cli/cli_mac_filter.hpp b/src/cli/cli_mac_filter.hpp
index 1394d4f..0f03b1b 100644
--- a/src/cli/cli_mac_filter.hpp
+++ b/src/cli/cli_mac_filter.hpp
@@ -41,7 +41,7 @@
 #include <openthread/link.h>
 
 #include "cli/cli_config.h"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 namespace ot {
 namespace Cli {
@@ -50,11 +50,9 @@
  * Implements the MAC Filter CLI interpreter.
  *
  */
-class MacFilter : private Output
+class MacFilter : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor.
      *
@@ -63,7 +61,7 @@
      *
      */
     MacFilter(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_mdns.cpp b/src/cli/cli_mdns.cpp
new file mode 100644
index 0000000..977bb7f
--- /dev/null
+++ b/src/cli/cli_mdns.cpp
@@ -0,0 +1,911 @@
+/*
+ *  Copyright (c) 2024, 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 mDNS.
+ */
+
+#include <string.h>
+
+#include "cli_mdns.hpp"
+
+#include <openthread/nat64.h>
+#include "cli/cli.hpp"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+
+namespace ot {
+namespace Cli {
+
+template <> otError Mdns::Process<Cmd("enable")>(Arg aArgs[])
+{
+    otError  error;
+    uint32_t infraIfIndex;
+
+    SuccessOrExit(error = aArgs[0].ParseAsUint32(infraIfIndex));
+    VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    SuccessOrExit(error = otMdnsSetEnabled(GetInstancePtr(), true, infraIfIndex));
+
+    mInfraIfIndex = infraIfIndex;
+
+exit:
+    return error;
+}
+
+template <> otError Mdns::Process<Cmd("disable")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    error = otMdnsSetEnabled(GetInstancePtr(), false, /* aInfraIfIndex */ 0);
+
+exit:
+    return error;
+}
+
+template <> otError Mdns::Process<Cmd("state")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    OutputEnabledDisabledStatus(otMdnsIsEnabled(GetInstancePtr()));
+
+exit:
+    return error;
+}
+
+template <> otError Mdns::Process<Cmd("unicastquestion")>(Arg aArgs[])
+{
+    return ProcessEnableDisable(aArgs, otMdnsIsQuestionUnicastAllowed, otMdnsSetQuestionUnicastAllowed);
+}
+
+void Mdns::OutputHost(const otMdnsHost &aHost)
+{
+    OutputLine("Host %s", aHost.mHostName);
+    OutputLine(kIndentSize, "%u address:", aHost.mAddressesLength);
+
+    for (uint16_t index = 0; index < aHost.mAddressesLength; index++)
+    {
+        OutputFormat(kIndentSize, "  ");
+        OutputIp6AddressLine(aHost.mAddresses[index]);
+    }
+
+    OutputLine(kIndentSize, "ttl: %lu", ToUlong(aHost.mTtl));
+}
+
+void Mdns::OutputService(const otMdnsService &aService)
+{
+    OutputLine("Service %s for %s", aService.mServiceInstance, aService.mServiceType);
+    OutputLine(kIndentSize, "host: %s", aService.mHostName);
+
+    if (aService.mSubTypeLabelsLength > 0)
+    {
+        OutputLine(kIndentSize, "%u sub-type:", aService.mSubTypeLabelsLength);
+
+        for (uint16_t index = 0; index < aService.mSubTypeLabelsLength; index++)
+        {
+            OutputLine(kIndentSize * 2, "%s", aService.mSubTypeLabels[index]);
+        }
+    }
+
+    OutputLine(kIndentSize, "port: %u", aService.mPort);
+    OutputLine(kIndentSize, "priority: %u", aService.mPriority);
+    OutputLine(kIndentSize, "weight: %u", aService.mWeight);
+    OutputLine(kIndentSize, "ttl: %lu", ToUlong(aService.mTtl));
+
+    if ((aService.mTxtData == nullptr) || (aService.mTxtDataLength == 0))
+    {
+        OutputLine(kIndentSize, "txt-data: (empty)");
+    }
+    else
+    {
+        OutputFormat(kIndentSize, "txt-data: ");
+        OutputBytesLine(aService.mTxtData, aService.mTxtDataLength);
+    }
+}
+
+void Mdns::OutputKey(const otMdnsKey &aKey)
+{
+    if (aKey.mServiceType != nullptr)
+    {
+        OutputLine("Key %s for %s (service)", aKey.mName, aKey.mServiceType);
+    }
+    else
+    {
+        OutputLine("Key %s (host)", aKey.mName);
+    }
+
+    OutputFormat(kIndentSize, "key-data: ");
+    OutputBytesLine(aKey.mKeyData, aKey.mKeyDataLength);
+
+    OutputLine(kIndentSize, "ttl: %lu", ToUlong(aKey.mTtl));
+}
+
+void Mdns::OutputState(otMdnsEntryState aState)
+{
+    const char *stateString = "";
+
+    switch (aState)
+    {
+    case OT_MDNS_ENTRY_STATE_PROBING:
+        stateString = "probing";
+        break;
+    case OT_MDNS_ENTRY_STATE_REGISTERED:
+        stateString = "registered";
+        break;
+    case OT_MDNS_ENTRY_STATE_CONFLICT:
+        stateString = "conflict";
+        break;
+    case OT_MDNS_ENTRY_STATE_REMOVING:
+        stateString = "removing";
+        break;
+    }
+
+    OutputLine(kIndentSize, "state: %s", stateString);
+}
+
+template <> otError Mdns::Process<Cmd("register")>(Arg aArgs[])
+{
+    // mdns [async] [host|service|key] <entry specific args>
+
+    otError error   = OT_ERROR_NONE;
+    bool    isAsync = false;
+
+    if (aArgs[0] == "async")
+    {
+        isAsync = true;
+        aArgs++;
+    }
+
+    if (aArgs[0] == "host")
+    {
+        SuccessOrExit(error = ProcessRegisterHost(aArgs + 1));
+    }
+    else if (aArgs[0] == "service")
+    {
+        SuccessOrExit(error = ProcessRegisterService(aArgs + 1));
+    }
+    else if (aArgs[0] == "key")
+    {
+        SuccessOrExit(error = ProcessRegisterKey(aArgs + 1));
+    }
+    else
+    {
+        ExitNow(error = OT_ERROR_INVALID_ARGS);
+    }
+
+    if (isAsync)
+    {
+        OutputLine("mDNS request id: %lu", ToUlong(mRequestId));
+    }
+    else
+    {
+        error               = OT_ERROR_PENDING;
+        mWaitingForCallback = true;
+    }
+
+exit:
+    return error;
+}
+
+otError Mdns::ProcessRegisterHost(Arg aArgs[])
+{
+    // register host <name> [<zero or more addresses>] [<ttl>]
+
+    otError      error = OT_ERROR_NONE;
+    otMdnsHost   host;
+    otIp6Address addresses[kMaxAddresses];
+
+    memset(&host, 0, sizeof(host));
+
+    VerifyOrExit(!aArgs->IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    host.mHostName = aArgs->GetCString();
+    aArgs++;
+
+    host.mAddresses = addresses;
+
+    for (; !aArgs->IsEmpty(); aArgs++)
+    {
+        otIp6Address address;
+        uint32_t     ttl;
+
+        if (aArgs->ParseAsIp6Address(address) == OT_ERROR_NONE)
+        {
+            VerifyOrExit(host.mAddressesLength < kMaxAddresses, error = OT_ERROR_NO_BUFS);
+            addresses[host.mAddressesLength] = address;
+            host.mAddressesLength++;
+        }
+        else if (aArgs->ParseAsUint32(ttl) == OT_ERROR_NONE)
+        {
+            host.mTtl = ttl;
+            VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+        }
+        else
+        {
+            ExitNow(error = OT_ERROR_INVALID_ARGS);
+        }
+    }
+
+    OutputHost(host);
+
+    mRequestId++;
+    error = otMdnsRegisterHost(GetInstancePtr(), &host, mRequestId, HandleRegisterationDone);
+
+exit:
+    return error;
+}
+
+otError Mdns::ProcessRegisterService(Arg aArgs[])
+{
+    otError       error;
+    otMdnsService service;
+    Buffers       buffers;
+
+    SuccessOrExit(error = ParseServiceArgs(aArgs, service, buffers));
+
+    OutputService(service);
+
+    mRequestId++;
+    error = otMdnsRegisterService(GetInstancePtr(), &service, mRequestId, HandleRegisterationDone);
+
+exit:
+    return error;
+}
+
+otError Mdns::ParseServiceArgs(Arg aArgs[], otMdnsService &aService, Buffers &aBuffers)
+{
+    // mdns register service <instance-label> <service-type,sub_types> <host-name> <port> [<prio>] [<weight>] [<ttl>]
+    // [<txtdata>]
+
+    otError  error = OT_ERROR_INVALID_ARGS;
+    char    *label;
+    uint16_t len;
+
+    memset(&aService, 0, sizeof(aService));
+
+    VerifyOrExit(!aArgs->IsEmpty());
+    aService.mServiceInstance = aArgs->GetCString();
+    aArgs++;
+
+    // Copy service type into `aBuffer.mString`, then search for
+    // `,` in the string to parse the list of sub-types (if any).
+
+    VerifyOrExit(!aArgs->IsEmpty());
+    len = aArgs->GetLength();
+    VerifyOrExit(len + 1 < kStringSize, error = OT_ERROR_NO_BUFS);
+    memcpy(aBuffers.mString, aArgs->GetCString(), len + 1);
+
+    aService.mServiceType   = aBuffers.mString;
+    aService.mSubTypeLabels = aBuffers.mSubTypeLabels;
+
+    label = strchr(aBuffers.mString, ',');
+
+    if (label != nullptr)
+    {
+        while (true)
+        {
+            *label++ = '\0';
+
+            VerifyOrExit(aService.mSubTypeLabelsLength < kMaxSubTypes, error = OT_ERROR_NO_BUFS);
+            aBuffers.mSubTypeLabels[aService.mSubTypeLabelsLength] = label;
+            aService.mSubTypeLabelsLength++;
+
+            label = strchr(label, ',');
+
+            if (label == nullptr)
+            {
+                break;
+            }
+        }
+    }
+
+    aArgs++;
+    VerifyOrExit(!aArgs->IsEmpty());
+    aService.mHostName = aArgs->GetCString();
+
+    aArgs++;
+    SuccessOrExit(aArgs->ParseAsUint16(aService.mPort));
+
+    // The rest of `Args` are optional.
+
+    error = OT_ERROR_NONE;
+
+    aArgs++;
+    VerifyOrExit(!aArgs->IsEmpty());
+    SuccessOrExit(error = aArgs->ParseAsUint16(aService.mPriority));
+
+    aArgs++;
+    VerifyOrExit(!aArgs->IsEmpty());
+    SuccessOrExit(error = aArgs->ParseAsUint16(aService.mWeight));
+
+    aArgs++;
+    VerifyOrExit(!aArgs->IsEmpty());
+    SuccessOrExit(error = aArgs->ParseAsUint32(aService.mTtl));
+
+    aArgs++;
+    VerifyOrExit(!aArgs->IsEmpty());
+    len = kMaxTxtDataSize;
+    SuccessOrExit(error = aArgs->ParseAsHexString(len, aBuffers.mTxtData));
+    aService.mTxtData       = aBuffers.mTxtData;
+    aService.mTxtDataLength = len;
+
+    aArgs++;
+    VerifyOrExit(aArgs->IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+exit:
+    return error;
+}
+
+otError Mdns::ProcessRegisterKey(Arg aArgs[])
+{
+    otError   error = OT_ERROR_INVALID_ARGS;
+    otMdnsKey key;
+    uint16_t  len;
+    uint8_t   data[kMaxKeyDataSize];
+
+    memset(&key, 0, sizeof(key));
+
+    VerifyOrExit(!aArgs->IsEmpty());
+    key.mName = aArgs->GetCString();
+
+    aArgs++;
+    VerifyOrExit(!aArgs->IsEmpty());
+
+    if (aArgs->GetCString()[0] == '_')
+    {
+        key.mServiceType = aArgs->GetCString();
+        aArgs++;
+        VerifyOrExit(!aArgs->IsEmpty());
+    }
+
+    len = kMaxKeyDataSize;
+    SuccessOrExit(error = aArgs->ParseAsHexString(len, data));
+
+    key.mKeyData       = data;
+    key.mKeyDataLength = len;
+
+    // ttl is optional
+
+    aArgs++;
+
+    if (!aArgs->IsEmpty())
+    {
+        SuccessOrExit(error = aArgs->ParseAsUint32(key.mTtl));
+        aArgs++;
+        VerifyOrExit(aArgs->IsEmpty(), error = kErrorInvalidArgs);
+    }
+
+    OutputKey(key);
+
+    mRequestId++;
+    error = otMdnsRegisterKey(GetInstancePtr(), &key, mRequestId, HandleRegisterationDone);
+
+exit:
+    return error;
+}
+
+void Mdns::HandleRegisterationDone(otInstance *aInstance, otMdnsRequestId aRequestId, otError aError)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    Interpreter::GetInterpreter().mMdns.HandleRegisterationDone(aRequestId, aError);
+}
+
+void Mdns::HandleRegisterationDone(otMdnsRequestId aRequestId, otError aError)
+{
+    if (mWaitingForCallback && (aRequestId == mRequestId))
+    {
+        mWaitingForCallback = false;
+        Interpreter::GetInterpreter().OutputResult(aError);
+    }
+    else
+    {
+        OutputLine("mDNS registration for request id %lu outcome: %s", ToUlong(aRequestId),
+                   otThreadErrorToString(aError));
+    }
+}
+
+template <> otError Mdns::Process<Cmd("unregister")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_INVALID_ARGS;
+
+    if (aArgs[0] == "host")
+    {
+        otMdnsHost host;
+
+        memset(&host, 0, sizeof(host));
+        VerifyOrExit(!aArgs[1].IsEmpty());
+        host.mHostName = aArgs[1].GetCString();
+        VerifyOrExit(aArgs[2].IsEmpty());
+
+        error = otMdnsUnregisterHost(GetInstancePtr(), &host);
+    }
+    else if (aArgs[0] == "service")
+    {
+        otMdnsService service;
+
+        memset(&service, 0, sizeof(service));
+        VerifyOrExit(!aArgs[1].IsEmpty());
+        service.mServiceInstance = aArgs[1].GetCString();
+        VerifyOrExit(!aArgs[2].IsEmpty());
+        service.mServiceType = aArgs[2].GetCString();
+        VerifyOrExit(aArgs[3].IsEmpty());
+
+        error = otMdnsUnregisterService(GetInstancePtr(), &service);
+    }
+    else if (aArgs[0] == "key")
+    {
+        otMdnsKey key;
+
+        memset(&key, 0, sizeof(key));
+        VerifyOrExit(!aArgs[1].IsEmpty());
+        key.mName = aArgs[1].GetCString();
+
+        if (!aArgs[2].IsEmpty())
+        {
+            key.mServiceType = aArgs[2].GetCString();
+            VerifyOrExit(aArgs[3].IsEmpty());
+        }
+
+        error = otMdnsUnregisterKey(GetInstancePtr(), &key);
+    }
+
+exit:
+    return error;
+}
+
+template <> otError Mdns::Process<Cmd("hosts")>(Arg aArgs[])
+{
+    otError          error    = OT_ERROR_NONE;
+    otMdnsIterator  *iterator = nullptr;
+    otMdnsHost       host;
+    otMdnsEntryState state;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    iterator = otMdnsAllocateIterator(GetInstancePtr());
+    VerifyOrExit(iterator != nullptr, error = OT_ERROR_NO_BUFS);
+
+    while (true)
+    {
+        error = otMdnsGetNextHost(GetInstancePtr(), iterator, &host, &state);
+
+        if (error == OT_ERROR_NOT_FOUND)
+        {
+            error = OT_ERROR_NONE;
+            ExitNow();
+        }
+
+        SuccessOrExit(error);
+
+        OutputHost(host);
+        OutputState(state);
+    }
+
+exit:
+    if (iterator != nullptr)
+    {
+        otMdnsFreeIterator(GetInstancePtr(), iterator);
+    }
+
+    return error;
+}
+
+template <> otError Mdns::Process<Cmd("services")>(Arg aArgs[])
+{
+    otError          error    = OT_ERROR_NONE;
+    otMdnsIterator  *iterator = nullptr;
+    otMdnsService    service;
+    otMdnsEntryState state;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    iterator = otMdnsAllocateIterator(GetInstancePtr());
+    VerifyOrExit(iterator != nullptr, error = OT_ERROR_NO_BUFS);
+
+    while (true)
+    {
+        error = otMdnsGetNextService(GetInstancePtr(), iterator, &service, &state);
+
+        if (error == OT_ERROR_NOT_FOUND)
+        {
+            error = OT_ERROR_NONE;
+            ExitNow();
+        }
+
+        SuccessOrExit(error);
+
+        OutputService(service);
+        OutputState(state);
+    }
+
+exit:
+    if (iterator != nullptr)
+    {
+        otMdnsFreeIterator(GetInstancePtr(), iterator);
+    }
+
+    return error;
+}
+
+template <> otError Mdns::Process<Cmd("keys")>(Arg aArgs[])
+{
+    otError          error    = OT_ERROR_NONE;
+    otMdnsIterator  *iterator = nullptr;
+    otMdnsKey        key;
+    otMdnsEntryState state;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    iterator = otMdnsAllocateIterator(GetInstancePtr());
+    VerifyOrExit(iterator != nullptr, error = OT_ERROR_NO_BUFS);
+
+    while (true)
+    {
+        error = otMdnsGetNextKey(GetInstancePtr(), iterator, &key, &state);
+
+        if (error == OT_ERROR_NOT_FOUND)
+        {
+            error = OT_ERROR_NONE;
+            ExitNow();
+        }
+
+        SuccessOrExit(error);
+
+        OutputKey(key);
+        OutputState(state);
+    }
+
+exit:
+    if (iterator != nullptr)
+    {
+        otMdnsFreeIterator(GetInstancePtr(), iterator);
+    }
+
+    return error;
+}
+
+otError Mdns::ParseStartOrStop(const Arg &aArg, bool &aIsStart)
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArg == "start")
+    {
+        aIsStart = true;
+    }
+    else if (aArg == "stop")
+    {
+        aIsStart = false;
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
+
+    return error;
+}
+
+template <> otError Mdns::Process<Cmd("browser")>(Arg aArgs[])
+{
+    // mdns browser start|stop <service-type> [<sub-type>]
+
+    otError       error;
+    otMdnsBrowser browser;
+    bool          isStart;
+
+    ClearAllBytes(browser);
+
+    SuccessOrExit(error = ParseStartOrStop(aArgs[0], isStart));
+    VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    browser.mServiceType = aArgs[1].GetCString();
+
+    if (!aArgs[2].IsEmpty())
+    {
+        browser.mSubTypeLabel = aArgs[2].GetCString();
+        VerifyOrExit(aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    }
+
+    browser.mInfraIfIndex = mInfraIfIndex;
+    browser.mCallback     = HandleBrowseResult;
+
+    if (isStart)
+    {
+        error = otMdnsStartBrowser(GetInstancePtr(), &browser);
+    }
+    else
+    {
+        error = otMdnsStopBrowser(GetInstancePtr(), &browser);
+    }
+
+exit:
+    return error;
+}
+
+void Mdns::HandleBrowseResult(otInstance *aInstance, const otMdnsBrowseResult *aResult)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    Interpreter::GetInterpreter().mMdns.HandleBrowseResult(*aResult);
+}
+
+void Mdns::HandleBrowseResult(const otMdnsBrowseResult &aResult)
+{
+    OutputFormat("mDNS browse result for %s", aResult.mServiceType);
+
+    if (aResult.mSubTypeLabel)
+    {
+        OutputLine(" sub-type %s", aResult.mSubTypeLabel);
+    }
+    else
+    {
+        OutputNewLine();
+    }
+
+    OutputLine(kIndentSize, "instance: %s", aResult.mServiceInstance);
+    OutputLine(kIndentSize, "ttl: %lu", ToUlong(aResult.mTtl));
+    OutputLine(kIndentSize, "if-index: %lu", ToUlong(aResult.mInfraIfIndex));
+}
+
+template <> otError Mdns::Process<Cmd("srvresolver")>(Arg aArgs[])
+{
+    // mdns srvresolver start|stop <service-instance> <service-type>
+
+    otError           error;
+    otMdnsSrvResolver resolver;
+    bool              isStart;
+
+    ClearAllBytes(resolver);
+
+    SuccessOrExit(error = ParseStartOrStop(aArgs[0], isStart));
+    VerifyOrExit(!aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    resolver.mServiceInstance = aArgs[1].GetCString();
+    resolver.mServiceType     = aArgs[2].GetCString();
+    resolver.mInfraIfIndex    = mInfraIfIndex;
+    resolver.mCallback        = HandleSrvResult;
+
+    if (isStart)
+    {
+        error = otMdnsStartSrvResolver(GetInstancePtr(), &resolver);
+    }
+    else
+    {
+        error = otMdnsStopSrvResolver(GetInstancePtr(), &resolver);
+    }
+
+exit:
+    return error;
+}
+
+void Mdns::HandleSrvResult(otInstance *aInstance, const otMdnsSrvResult *aResult)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    Interpreter::GetInterpreter().mMdns.HandleSrvResult(*aResult);
+}
+
+void Mdns::HandleSrvResult(const otMdnsSrvResult &aResult)
+{
+    OutputLine("mDNS SRV result for %s for %s", aResult.mServiceInstance, aResult.mServiceType);
+
+    if (aResult.mTtl != 0)
+    {
+        OutputLine(kIndentSize, "host: %s", aResult.mHostName);
+        OutputLine(kIndentSize, "port: %u", aResult.mPort);
+        OutputLine(kIndentSize, "priority: %u", aResult.mPriority);
+        OutputLine(kIndentSize, "weight: %u", aResult.mWeight);
+    }
+
+    OutputLine(kIndentSize, "ttl: %lu", ToUlong(aResult.mTtl));
+    OutputLine(kIndentSize, "if-index: %lu", ToUlong(aResult.mInfraIfIndex));
+}
+
+template <> otError Mdns::Process<Cmd("txtresolver")>(Arg aArgs[])
+{
+    // mdns txtresolver start|stop <service-instance> <service-type>
+
+    otError           error;
+    otMdnsTxtResolver resolver;
+    bool              isStart;
+
+    ClearAllBytes(resolver);
+
+    SuccessOrExit(error = ParseStartOrStop(aArgs[0], isStart));
+    VerifyOrExit(!aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    resolver.mServiceInstance = aArgs[1].GetCString();
+    resolver.mServiceType     = aArgs[2].GetCString();
+    resolver.mInfraIfIndex    = mInfraIfIndex;
+    resolver.mCallback        = HandleTxtResult;
+
+    if (isStart)
+    {
+        error = otMdnsStartTxtResolver(GetInstancePtr(), &resolver);
+    }
+    else
+    {
+        error = otMdnsStopTxtResolver(GetInstancePtr(), &resolver);
+    }
+
+exit:
+    return error;
+}
+
+void Mdns::HandleTxtResult(otInstance *aInstance, const otMdnsTxtResult *aResult)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    Interpreter::GetInterpreter().mMdns.HandleTxtResult(*aResult);
+}
+
+void Mdns::HandleTxtResult(const otMdnsTxtResult &aResult)
+{
+    OutputLine("mDNS TXT result for %s for %s", aResult.mServiceInstance, aResult.mServiceType);
+
+    if (aResult.mTtl != 0)
+    {
+        OutputFormat(kIndentSize, "txt-data: ");
+        OutputBytesLine(aResult.mTxtData, aResult.mTxtDataLength);
+    }
+
+    OutputLine(kIndentSize, "ttl: %lu", ToUlong(aResult.mTtl));
+    OutputLine(kIndentSize, "if-index: %lu", ToUlong(aResult.mInfraIfIndex));
+}
+template <> otError Mdns::Process<Cmd("ip6resolver")>(Arg aArgs[])
+{
+    // mdns ip6resolver start|stop <host-name>
+
+    otError               error;
+    otMdnsAddressResolver resolver;
+    bool                  isStart;
+
+    ClearAllBytes(resolver);
+
+    SuccessOrExit(error = ParseStartOrStop(aArgs[0], isStart));
+    VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    resolver.mHostName     = aArgs[1].GetCString();
+    resolver.mInfraIfIndex = mInfraIfIndex;
+    resolver.mCallback     = HandleIp6AddressResult;
+
+    if (isStart)
+    {
+        error = otMdnsStartIp6AddressResolver(GetInstancePtr(), &resolver);
+    }
+    else
+    {
+        error = otMdnsStopIp6AddressResolver(GetInstancePtr(), &resolver);
+    }
+
+exit:
+    return error;
+}
+
+void Mdns::HandleIp6AddressResult(otInstance *aInstance, const otMdnsAddressResult *aResult)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    Interpreter::GetInterpreter().mMdns.HandleAddressResult(*aResult, kIp6Address);
+}
+
+void Mdns::HandleAddressResult(const otMdnsAddressResult &aResult, IpAddressType aType)
+{
+    OutputLine("mDNS %s address result for %s", aType == kIp6Address ? "IPv6" : "IPv4", aResult.mHostName);
+
+    OutputLine(kIndentSize, "%u address:", aResult.mAddressesLength);
+
+    for (uint16_t index = 0; index < aResult.mAddressesLength; index++)
+    {
+        OutputFormat(kIndentSize, "  ");
+        OutputIp6Address(aResult.mAddresses[index].mAddress);
+        OutputLine(" ttl:%lu", ToUlong(aResult.mAddresses[index].mTtl));
+    }
+
+    OutputLine(kIndentSize, "if-index: %lu", ToUlong(aResult.mInfraIfIndex));
+}
+
+template <> otError Mdns::Process<Cmd("ip4resolver")>(Arg aArgs[])
+{
+    // mdns ip4resolver start|stop <host-name>
+
+    otError               error;
+    otMdnsAddressResolver resolver;
+    bool                  isStart;
+
+    ClearAllBytes(resolver);
+
+    SuccessOrExit(error = ParseStartOrStop(aArgs[0], isStart));
+    VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    resolver.mHostName     = aArgs[1].GetCString();
+    resolver.mInfraIfIndex = mInfraIfIndex;
+    resolver.mCallback     = HandleIp4AddressResult;
+
+    if (isStart)
+    {
+        error = otMdnsStartIp4AddressResolver(GetInstancePtr(), &resolver);
+    }
+    else
+    {
+        error = otMdnsStopIp4AddressResolver(GetInstancePtr(), &resolver);
+    }
+
+exit:
+    return error;
+}
+
+void Mdns::HandleIp4AddressResult(otInstance *aInstance, const otMdnsAddressResult *aResult)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    Interpreter::GetInterpreter().mMdns.HandleAddressResult(*aResult, kIp4Address);
+}
+
+otError Mdns::Process(Arg aArgs[])
+{
+#define CmdEntry(aCommandString)                            \
+    {                                                       \
+        aCommandString, &Mdns::Process<Cmd(aCommandString)> \
+    }
+
+    static constexpr Command kCommands[] = {
+        CmdEntry("browser"),         CmdEntry("disable"),     CmdEntry("enable"), CmdEntry("hosts"),
+        CmdEntry("ip4resolver"),     CmdEntry("ip6resolver"), CmdEntry("keys"),   CmdEntry("register"),
+        CmdEntry("services"),        CmdEntry("srvresolver"), CmdEntry("state"),  CmdEntry("txtresolver"),
+        CmdEntry("unicastquestion"), CmdEntry("unregister"),
+    };
+
+#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_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
diff --git a/src/cli/cli_mdns.hpp b/src/cli/cli_mdns.hpp
new file mode 100644
index 0000000..242b6d9
--- /dev/null
+++ b/src/cli/cli_mdns.hpp
@@ -0,0 +1,143 @@
+/*
+ *  Copyright (c) 2024, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file contains definitions for CLI to DNS (client and resolver).
+ */
+
+#ifndef CLI_MDNS_HPP_
+#define CLI_MDNS_HPP_
+
+#include "openthread-core-config.h"
+
+#include <openthread/mdns.h>
+
+#include "cli/cli_config.h"
+#include "cli/cli_utils.hpp"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+
+namespace ot {
+namespace Cli {
+
+/**
+ * Implements the mDNS CLI interpreter.
+ *
+ */
+class Mdns : private Utils
+{
+public:
+    /**
+     * Constructor.
+     *
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
+     *
+     */
+    Mdns(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
+        , mInfraIfIndex(0)
+        , mRequestId(0)
+        , mWaitingForCallback(false)
+    {
+    }
+
+    /**
+     * Processes a CLI sub-command.
+     *
+     * @param[in]  aArgs     An array of command line arguments.
+     *
+     * @retval OT_ERROR_NONE              Successfully executed the CLI command.
+     * @retval OT_ERROR_PENDING           The CLI command was successfully started but final result is pending.
+     * @retval OT_ERROR_INVALID_COMMAND   Invalid or unknown CLI command.
+     * @retval OT_ERROR_INVALID_ARGS      Invalid arguments.
+     * @retval ...                        Error during execution of the CLI command.
+     *
+     */
+    otError Process(Arg aArgs[]);
+
+private:
+    using Command = CommandEntry<Mdns>;
+
+    static constexpr uint8_t  kIndentSize     = 4;
+    static constexpr uint16_t kMaxAddresses   = 16;
+    static constexpr uint16_t kStringSize     = 400;
+    static constexpr uint16_t kMaxSubTypes    = 8;
+    static constexpr uint16_t kMaxTxtDataSize = 200;
+    static constexpr uint16_t kMaxKeyDataSize = 200;
+
+    enum IpAddressType : uint8_t
+    {
+        kIp6Address,
+        kIp4Address,
+    };
+
+    struct Buffers // Used to populate `otMdnsService` field
+    {
+        char        mString[kStringSize];
+        const char *mSubTypeLabels[kMaxSubTypes];
+        uint8_t     mTxtData[kMaxTxtDataSize];
+    };
+
+    template <CommandId kCommandId> otError Process(Arg aArgs[]);
+
+    void    OutputHost(const otMdnsHost &aHost);
+    void    OutputService(const otMdnsService &aService);
+    void    OutputKey(const otMdnsKey &aKey);
+    void    OutputState(otMdnsEntryState aState);
+    otError ProcessRegisterHost(Arg aArgs[]);
+    otError ProcessRegisterService(Arg aArgs[]);
+    otError ProcessRegisterKey(Arg aArgs[]);
+    void    HandleRegisterationDone(otMdnsRequestId aRequestId, otError aError);
+    void    HandleBrowseResult(const otMdnsBrowseResult &aResult);
+    void    HandleSrvResult(const otMdnsSrvResult &aResult);
+    void    HandleTxtResult(const otMdnsTxtResult &aResult);
+    void    HandleAddressResult(const otMdnsAddressResult &aResult, IpAddressType aType);
+
+    static otError ParseStartOrStop(const Arg &aArg, bool &aIsStart);
+    static void    HandleRegisterationDone(otInstance *aInstance, otMdnsRequestId aRequestId, otError aError);
+    static void    HandleBrowseResult(otInstance *aInstance, const otMdnsBrowseResult *aResult);
+    static void    HandleSrvResult(otInstance *aInstance, const otMdnsSrvResult *aResult);
+    static void    HandleTxtResult(otInstance *aInstance, const otMdnsTxtResult *aResult);
+    static void    HandleIp6AddressResult(otInstance *aInstance, const otMdnsAddressResult *aResult);
+    static void    HandleIp4AddressResult(otInstance *aInstance, const otMdnsAddressResult *aResult);
+
+    static otError ParseServiceArgs(Arg aArgs[], otMdnsService &aService, Buffers &aBuffers);
+
+    uint32_t        mInfraIfIndex;
+    otMdnsRequestId mRequestId;
+    bool            mWaitingForCallback;
+};
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+
+#endif // CLI_MDNS_HPP_
diff --git a/src/cli/cli_network_data.cpp b/src/cli/cli_network_data.cpp
index 85c3e26..7aa0dd5 100644
--- a/src/cli/cli_network_data.cpp
+++ b/src/cli/cli_network_data.cpp
@@ -44,7 +44,7 @@
 namespace Cli {
 
 NetworkData::NetworkData(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
 {
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_SIGNAL_NETWORK_DATA_FULL
     mFullCallbackWasCalled = false;
@@ -564,17 +564,78 @@
     return error;
 }
 
-void NetworkData::OutputPrefixes(bool aLocal)
+void NetworkData::OutputNetworkData(bool aLocal, uint16_t aRloc16)
 {
-    otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
-    otBorderRouterConfig  config;
+    otNetworkDataIterator  iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+    otBorderRouterConfig   prefix;
+    otExternalRouteConfig  route;
+    otServiceConfig        service;
+    otLowpanContextInfo    context;
+    otCommissioningDataset dataset;
 
     OutputLine("Prefixes:");
 
-    while (GetNextPrefix(&iterator, &config, aLocal) == OT_ERROR_NONE)
+    while (GetNextPrefix(&iterator, &prefix, aLocal) == OT_ERROR_NONE)
     {
-        OutputPrefix(config);
+        if ((aRloc16 == kAnyRloc16) || (aRloc16 == prefix.mRloc16))
+        {
+            OutputPrefix(prefix);
+        }
     }
+
+    OutputLine("Routes:");
+    iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+
+    while (GetNextRoute(&iterator, &route, aLocal) == OT_ERROR_NONE)
+    {
+        if ((aRloc16 == kAnyRloc16) || (aRloc16 == route.mRloc16))
+        {
+            OutputRoute(route);
+        }
+    }
+
+    OutputLine("Services:");
+    iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+
+    while (GetNextService(&iterator, &service, aLocal) == OT_ERROR_NONE)
+    {
+        if ((aRloc16 == kAnyRloc16) || (aRloc16 == service.mServerConfig.mRloc16))
+        {
+            OutputService(service);
+        }
+    }
+
+    VerifyOrExit(!aLocal);
+    VerifyOrExit(aRloc16 == kAnyRloc16);
+
+    OutputLine("Contexts:");
+    iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+
+    while (otNetDataGetNextLowpanContextInfo(GetInstancePtr(), &iterator, &context) == OT_ERROR_NONE)
+    {
+        OutputIp6Prefix(context.mPrefix);
+        OutputLine(" %u %c", context.mContextId, context.mCompressFlag ? 'c' : '-');
+    }
+
+    otNetDataGetCommissioningDataset(GetInstancePtr(), &dataset);
+
+    OutputLine("Commissioning:");
+
+    dataset.mIsSessionIdSet ? OutputFormat("%u ", dataset.mSessionId) : OutputFormat("- ");
+    dataset.mIsLocatorSet ? OutputFormat("%04x ", dataset.mLocator) : OutputFormat("- ");
+    dataset.mIsJoinerUdpPortSet ? OutputFormat("%u ", dataset.mJoinerUdpPort) : OutputFormat("- ");
+    dataset.mIsSteeringDataSet ? OutputBytes(dataset.mSteeringData.m8, dataset.mSteeringData.mLength)
+                               : OutputFormat("-");
+
+    if (dataset.mHasExtraTlv)
+    {
+        OutputFormat(" e");
+    }
+
+    OutputNewLine();
+
+exit:
+    return;
 }
 
 otError NetworkData::GetNextRoute(otNetworkDataIterator *aIterator, otExternalRouteConfig *aConfig, bool aLocal)
@@ -597,19 +658,6 @@
     return error;
 }
 
-void NetworkData::OutputRoutes(bool aLocal)
-{
-    otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
-    otExternalRouteConfig config;
-
-    OutputLine("Routes:");
-
-    while (GetNextRoute(&iterator, &config, aLocal) == OT_ERROR_NONE)
-    {
-        OutputRoute(config);
-    }
-}
-
 otError NetworkData::GetNextService(otNetworkDataIterator *aIterator, otServiceConfig *aConfig, bool aLocal)
 {
     otError error;
@@ -630,65 +678,6 @@
     return error;
 }
 
-void NetworkData::OutputServices(bool aLocal)
-{
-    otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
-    otServiceConfig       config;
-
-    OutputLine("Services:");
-
-    while (GetNextService(&iterator, &config, aLocal) == OT_ERROR_NONE)
-    {
-        OutputService(config);
-    }
-}
-
-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;
-}
-
-void NetworkData::OutputCommissioningDataset(bool aLocal)
-{
-    otCommissioningDataset dataset;
-
-    VerifyOrExit(!aLocal);
-
-    otNetDataGetCommissioningDataset(GetInstancePtr(), &dataset);
-
-    OutputLine("Commissioning:");
-
-    dataset.mIsSessionIdSet ? OutputFormat("%u ", dataset.mSessionId) : OutputFormat("- ");
-    dataset.mIsLocatorSet ? OutputFormat("%04x ", dataset.mLocator) : OutputFormat("- ");
-    dataset.mIsJoinerUdpPortSet ? OutputFormat("%u ", dataset.mJoinerUdpPort) : OutputFormat("- ");
-    dataset.mIsSteeringDataSet ? OutputBytes(dataset.mSteeringData.m8, dataset.mSteeringData.mLength)
-                               : OutputFormat("-");
-
-    if (dataset.mHasExtraTlv)
-    {
-        OutputFormat(" e");
-    }
-
-    OutputNewLine();
-
-exit:
-    return;
-}
-
 otError NetworkData::OutputBinary(bool aLocal)
 {
     otError error;
@@ -737,8 +726,16 @@
  * 08040b02174703140040fd00deadbeefcafe0504dc00330007021140
  * Done
  * @endcode
- * @cparam netdata show [@ca{-x}]
+ * @code
+ * netdata show 0xdc00
+ * Prefixes:
+ * fd00:dead:beef:cafe::/64 paros med dc00
+ * Routes:
+ * Services:
+ * Done
+ * @cparam netdata show [@ca{-x}|@ca{rloc16}]
  * *   The optional `-x` argument gets Network Data as hex-encoded TLVs.
+ * *   The optional `rloc16` argument gets all prefix/route/service entries associated with a given RLOC16.
  * @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,
@@ -795,9 +792,10 @@
  */
 template <> otError NetworkData::Process<Cmd("show")>(Arg aArgs[])
 {
-    otError error  = OT_ERROR_INVALID_ARGS;
-    bool    local  = false;
-    bool    binary = false;
+    otError  error  = OT_ERROR_INVALID_ARGS;
+    uint16_t rloc16 = kAnyRloc16;
+    bool     local  = false;
+    bool     binary = false;
 
     for (uint8_t i = 0; !aArgs[i].IsEmpty(); i++)
     {
@@ -832,21 +830,22 @@
         }
         else
         {
-            ExitNow(error = OT_ERROR_INVALID_ARGS);
+            SuccessOrExit(error = aArgs[i].ParseAsUint16(rloc16));
         }
     }
 
+    if (local || binary)
+    {
+        VerifyOrExit(rloc16 == kAnyRloc16, error = OT_ERROR_INVALID_ARGS);
+    }
+
     if (binary)
     {
         error = OutputBinary(local);
     }
     else
     {
-        OutputPrefixes(local);
-        OutputRoutes(local);
-        OutputServices(local);
-        OutputLowpanContexts(local);
-        OutputCommissioningDataset(local);
+        OutputNetworkData(local, rloc16);
         error = OT_ERROR_NONE;
     }
 
diff --git a/src/cli/cli_network_data.hpp b/src/cli/cli_network_data.hpp
index 15af650..ba8f281 100644
--- a/src/cli/cli_network_data.hpp
+++ b/src/cli/cli_network_data.hpp
@@ -38,7 +38,7 @@
 
 #include <openthread/netdata.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 namespace ot {
 namespace Cli {
@@ -47,11 +47,9 @@
  * Implements the Network Data CLI.
  *
  */
-class NetworkData : private Output
+class NetworkData : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * This constant specifies the string size for representing Network Data prefix/route entry flags.
      *
@@ -132,6 +130,8 @@
 private:
     using Command = CommandEntry<NetworkData>;
 
+    static constexpr uint16_t kAnyRloc16 = 0xffff;
+
     template <CommandId kCommandId> otError Process(Arg aArgs[]);
 
     otError GetNextPrefix(otNetworkDataIterator *aIterator, otBorderRouterConfig *aConfig, bool aLocal);
@@ -139,11 +139,7 @@
     otError GetNextService(otNetworkDataIterator *aIterator, otServiceConfig *aConfig, bool aLocal);
 
     otError OutputBinary(bool aLocal);
-    void    OutputPrefixes(bool aLocal);
-    void    OutputRoutes(bool aLocal);
-    void    OutputServices(bool aLocal);
-    void    OutputLowpanContexts(bool aLocal);
-    void    OutputCommissioningDataset(bool aLocal);
+    void    OutputNetworkData(bool aLocal, uint16_t aRloc16);
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_SIGNAL_NETWORK_DATA_FULL
     static void HandleNetdataFull(void *aContext) { static_cast<NetworkData *>(aContext)->HandleNetdataFull(); }
diff --git a/src/cli/cli_ping.cpp b/src/cli/cli_ping.cpp
index e8e7621..1111e3d 100644
--- a/src/cli/cli_ping.cpp
+++ b/src/cli/cli_ping.cpp
@@ -36,7 +36,7 @@
 #include <openthread/ping_sender.h>
 
 #include "cli/cli.hpp"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 #include "common/code_utils.hpp"
 
 #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE
@@ -45,7 +45,7 @@
 namespace Cli {
 
 PingSender::PingSender(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
     , mPingIsAsync(false)
 {
 }
@@ -55,7 +55,7 @@
     otError            error = OT_ERROR_NONE;
     otPingSenderConfig config;
     bool               async = false;
-    bool               nat64SynthesizedAddress;
+    bool               nat64Synth;
 
     /**
      * @cli ping stop
@@ -96,9 +96,9 @@
         aArgs++;
     }
 
-    SuccessOrExit(error = Interpreter::GetInterpreter().ParseToIp6Address(
-                      GetInstancePtr(), aArgs[0], config.mDestination, nat64SynthesizedAddress));
-    if (nat64SynthesizedAddress)
+    SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], config.mDestination, nat64Synth));
+
+    if (nat64Synth)
     {
         OutputFormat("Pinging synthesized IPv6 address: ");
         OutputIp6AddressLine(config.mDestination);
diff --git a/src/cli/cli_ping.hpp b/src/cli/cli_ping.hpp
index e517506..6504412 100644
--- a/src/cli/cli_ping.hpp
+++ b/src/cli/cli_ping.hpp
@@ -38,7 +38,7 @@
 
 #include <openthread/ping_sender.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE
 
@@ -50,11 +50,9 @@
  *
  */
 
-class PingSender : private Output
+class PingSender : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
diff --git a/src/cli/cli_srp_client.cpp b/src/cli/cli_srp_client.cpp
index 30ed8d9..a5301f3 100644
--- a/src/cli/cli_srp_client.cpp
+++ b/src/cli/cli_srp_client.cpp
@@ -58,7 +58,7 @@
 }
 
 SrpClient::SrpClient(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
     , mCallbackEnabled(false)
 {
     otSrpClientSetCallback(GetInstancePtr(), SrpClient::HandleCallback, this);
@@ -80,8 +80,8 @@
      * @endcode
      * @par
      * Indicates the current state of auto-start mode (enabled or disabled).
+     * @moreinfo{@srp}.
      * @sa otSrpClientIsAutoStartModeEnabled
-     * @sa @srp
      */
     if (aArgs[0].IsEmpty())
     {
@@ -160,8 +160,8 @@
  * @cparam srp client callback [@ca{enable}|@ca{disable}]
  * @par
  * Gets or enables/disables printing callback events from the SRP client.
+ * @moreinfo{@srp}.
  * @sa otSrpClientSetCallback
- * @sa @srp
  */
 template <> otError SrpClient::Process<Cmd("callback")>(Arg aArgs[])
 {
@@ -212,9 +212,8 @@
      * To set the client host name when the host has either been removed or not yet
      * registered with the server, use the `name` parameter.
      * @par
-     * Gets or sets the host name of the SRP client.
+     * Gets or sets the host name of the SRP client. @moreinfo{@srp}.
      * @sa otSrpClientSetHostName
-     * @sa @srp
      */
     else if (aArgs[0] == "name")
     {
@@ -288,8 +287,8 @@
          * @par
          * Indicates whether auto address mode is enabled. If auto address mode is not
          * enabled, then the list of SRP client host addresses is returned.
+         * @moreinfo{@srp}.
          * @sa otSrpClientGetHostInfo
-         * @sa @srp
          */
         if (aArgs[1].IsEmpty())
         {
@@ -330,10 +329,9 @@
          *     running. This will also disable auto host address mode.
          * @par
          * Enable auto host address mode or explicitly set the list of host
-         * addresses.
+         * addresses. @moreinfo{@srp}.
          * @sa otSrpClientEnableAutoHostAddress
          * @sa otSrpClientSetHostAddresses
-         * @sa @srp
          */
         else if (aArgs[1] == "auto")
         {
@@ -388,9 +386,9 @@
      *     `removekeylease` parameter is specified first in the command.
      * @par
      * Removes SRP client host information and all services from the SRP server.
+     * @moreinfo{@srp}.
      * @sa otSrpClientRemoveHostAndServices
      * @sa otSrpClientSetHostName
-     * @sa @srp
      */
     else if (aArgs[0] == "remove")
     {
@@ -455,7 +453,7 @@
  */
 template <> otError SrpClient::Process<Cmd("leaseinterval")>(Arg aArgs[])
 {
-    return Interpreter::GetInterpreter().ProcessGetSet(aArgs, otSrpClientGetLeaseInterval, otSrpClientSetLeaseInterval);
+    return ProcessGetSet(aArgs, otSrpClientGetLeaseInterval, otSrpClientSetLeaseInterval);
 }
 
 /**
@@ -477,8 +475,7 @@
  */
 template <> otError SrpClient::Process<Cmd("keyleaseinterval")>(Arg aArgs[])
 {
-    return Interpreter::GetInterpreter().ProcessGetSet(aArgs, otSrpClientGetKeyLeaseInterval,
-                                                       otSrpClientSetKeyLeaseInterval);
+    return ProcessGetSet(aArgs, otSrpClientGetKeyLeaseInterval, otSrpClientSetKeyLeaseInterval);
 }
 
 template <> otError SrpClient::Process<Cmd("server")>(Arg aArgs[])
@@ -496,9 +493,8 @@
      * @par
      * Gets the socket address (IPv6 address and port number) of the SRP server
      * that is being used by the SRP client. If the client is not running, the address
-     * is unspecified (all zeros) with a port number of 0.
+     * is unspecified (all zeros) with a port number of 0. @moreinfo{@srp}.
      * @sa otSrpClientGetServerAddress
-     * @sa @srp
      */
     if (aArgs[0].IsEmpty())
     {
@@ -580,13 +576,13 @@
      * * -->                          [@ca{weight}] [@ca{txt}]
      * The `servicename` parameter can optionally include a list of service subtype labels that are
      * separated by commas. The examples here use generic naming. The `priority` and `weight` (both are `uint16_t`
-     * values) parameters are optional, and if not provided zero is used. The optional `txt` parameter sets the TXT data
-     * associated with the service. The `txt` value must be in hex-string format and is treated as an already encoded
-     * TXT data byte sequence.
+     * values) parameters are optional, and if not provided zero is used. The optional `txt` parameter sets the TXT
+     * data associated with the service. The `txt` value must be in hex-string format and is treated as an already
+     * encoded TXT data byte sequence.
      * @par
      * Adds a service with a given instance name, service name, and port number.
+     * @moreinfo{@srp}.
      * @sa otSrpClientAddService
-     * @sa @srp
      */
     else if (aArgs[0] == "add")
     {
@@ -660,16 +656,15 @@
      * @par
      * Gets or sets the service key record inclusion mode in the SRP client.
      * This command is intended for testing only, and requires that
-     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` be enabled.
+     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` be enabled. @moreinfo{@srp}.
      * @sa otSrpClientIsServiceKeyRecordEnabled
-     * @sa @srp
      */
     else if (aArgs[0] == "key")
     {
         // `key [enable/disable]`
 
-        error = Interpreter::GetInterpreter().ProcessEnableDisable(aArgs + 1, otSrpClientIsServiceKeyRecordEnabled,
-                                                                   otSrpClientSetServiceKeyRecordEnabled);
+        error = ProcessEnableDisable(aArgs + 1, otSrpClientIsServiceKeyRecordEnabled,
+                                     otSrpClientSetServiceKeyRecordEnabled);
     }
 #endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     else
@@ -853,9 +848,8 @@
  * @endcode
  * @cparam srp client start @ca{serveraddr} @ca{serverport}
  * @par
- * Starts the SRP client operation.
+ * Starts the SRP client operation. @moreinfo{@srp}.
  * @sa otSrpClientStart
- * @sa @srp
  */
 template <> otError SrpClient::Process<Cmd("start")>(Arg aArgs[])
 {
@@ -881,7 +875,7 @@
  * @endcode
  * @par api_copy
  * #otSrpClientIsRunning
- * @sa @srp
+ * @moreinfo{@srp}.
  */
 template <> otError SrpClient::Process<Cmd("state")>(Arg aArgs[])
 {
@@ -934,7 +928,7 @@
  */
 template <> otError SrpClient::Process<Cmd("ttl")>(Arg aArgs[])
 {
-    return Interpreter::GetInterpreter().ProcessGetSet(aArgs, otSrpClientGetTtl, otSrpClientSetTtl);
+    return ProcessGetSet(aArgs, otSrpClientGetTtl, otSrpClientSetTtl);
 }
 
 void SrpClient::HandleCallback(otError                    aError,
diff --git a/src/cli/cli_srp_client.hpp b/src/cli/cli_srp_client.hpp
index 93e7fe3..85564cd 100644
--- a/src/cli/cli_srp_client.hpp
+++ b/src/cli/cli_srp_client.hpp
@@ -40,7 +40,7 @@
 #include <openthread/srp_client_buffers.h>
 
 #include "cli/cli_config.h"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
 
@@ -51,11 +51,9 @@
  * Implements the SRP Client CLI interpreter.
  *
  */
-class SrpClient : private Output
+class SrpClient : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
diff --git a/src/cli/cli_srp_server.cpp b/src/cli/cli_srp_server.cpp
index 1dc6d43..5e71a4a 100644
--- a/src/cli/cli_srp_server.cpp
+++ b/src/cli/cli_srp_server.cpp
@@ -115,14 +115,13 @@
  * to enable or disable the SRP server.
  * @par
  * This command requires that `OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE` be enabled.
+ * @moreinfo{@srp}.
  * @sa otSrpServerIsAutoEnableMode
  * @sa otSrpServerSetAutoEnableMode
- * @sa @srp
  */
 template <> otError SrpServer::Process<Cmd("auto")>(Arg aArgs[])
 {
-    return Interpreter::GetInterpreter().ProcessEnableDisable(aArgs, otSrpServerIsAutoEnableMode,
-                                                              otSrpServerSetAutoEnableMode);
+    return ProcessEnableDisable(aArgs, otSrpServerIsAutoEnableMode, otSrpServerSetAutoEnableMode);
 }
 #endif
 
@@ -174,8 +173,9 @@
  *               The SRP server may become active when the existing
  *               SRP servers are no longer active within the Thread network.
  *  * `running`: The SRP server is active and can handle service registrations.
+ * @par
+ * @moreinfo{@srp}.
  * @sa otSrpServerGetState
- * @sa @srp
  */
 template <> otError SrpServer::Process<Cmd("state")>(Arg aArgs[])
 {
@@ -213,9 +213,8 @@
  * @endcode
  * @cparam srp server [@ca{enable}|@ca{disable}]
  * @par
- * Enables or disables the SRP server.
+ * Enables or disables the SRP server. @moreinfo{@srp}.
  * @sa otSrpServerSetEnabled
- * @sa @srp
  */
 template <> otError SrpServer::Process<Cmd("disable")>(Arg aArgs[])
 {
@@ -311,11 +310,10 @@
  * Done
  * @endcode
  * @par
- * Returns information about all registered hosts.
+ * Returns information about all registered hosts. @moreinfo{@srp}.
  * @sa otSrpServerGetNextHost
  * @sa otSrpServerHostGetAddresses
  * @sa otSrpServerHostGetFullName
- * @sa @srp
  */
 template <> otError SrpServer::Process<Cmd("host")>(Arg aArgs[])
 {
@@ -415,10 +413,10 @@
  * The `TXT` record is displayed
  * as an array of entries. If an entry contains a key, the key is printed in
  * ASCII format. The value portion is printed in hexadecimal bytes.
+ * @moreinfo{@srp}.
  * @sa otSrpServerServiceGetInstanceName
  * @sa otSrpServerServiceGetServiceName
  * @sa otSrpServerServiceGetSubTypeServiceNameAt
- * @sa @srp
  */
 template <> otError SrpServer::Process<Cmd("service")>(Arg aArgs[])
 {
diff --git a/src/cli/cli_srp_server.hpp b/src/cli/cli_srp_server.hpp
index 344d3c2..b110eb8 100644
--- a/src/cli/cli_srp_server.hpp
+++ b/src/cli/cli_srp_server.hpp
@@ -38,7 +38,7 @@
 
 #include <openthread/srp_server.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
 
@@ -49,11 +49,9 @@
  * Implements the SRP Server CLI interpreter.
  *
  */
-class SrpServer : private Output
+class SrpServer : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -62,7 +60,7 @@
      *
      */
     SrpServer(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_tcat.cpp b/src/cli/cli_tcat.cpp
index e754bca..1ef73a5 100644
--- a/src/cli/cli_tcat.cpp
+++ b/src/cli/cli_tcat.cpp
@@ -28,7 +28,7 @@
 
 #include "openthread-core-config.h"
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #include "cli/cli_tcat.hpp"
 
@@ -79,9 +79,8 @@
 
 namespace Cli {
 
-otTcatVendorInfo sVendorInfo;
-const char       kPskdVendor[] = "J01NM3";
-const char       kUrl[]        = "dummy_url";
+const char kPskdVendor[] = "J01NM3";
+const char kUrl[]        = "dummy_url";
 
 static void HandleBleSecureReceive(otInstance               *aInstance,
                                    const otMessage          *aMessage,
@@ -93,10 +92,12 @@
     OT_UNUSED_VARIABLE(aContext);
     OT_UNUSED_VARIABLE(aTcatApplicationProtocol);
     OT_UNUSED_VARIABLE(aServiceName);
+
     static constexpr int     kTextMaxLen   = 100;
     static constexpr uint8_t kBufPrefixLen = 5;
-    uint16_t                 nLen;
-    uint8_t                  buf[kTextMaxLen];
+
+    uint16_t nLen;
+    uint8_t  buf[kTextMaxLen];
 
     nLen = otMessageRead(aMessage, (uint16_t)aOffset, buf + kBufPrefixLen, sizeof(buf) - kBufPrefixLen - 1);
 
@@ -114,8 +115,9 @@
 
     otError error = OT_ERROR_NONE;
 
-    sVendorInfo.mPskdString      = kPskdVendor;
-    sVendorInfo.mProvisioningUrl = kUrl;
+    ClearAllBytes(mVendorInfo);
+    mVendorInfo.mPskdString      = kPskdVendor;
+    mVendorInfo.mProvisioningUrl = kUrl;
 
     otBleSecureSetCertificate(GetInstancePtr(), reinterpret_cast<const uint8_t *>(OT_CLI_TCAT_X509_CERT),
                               sizeof(OT_CLI_TCAT_X509_CERT), reinterpret_cast<const uint8_t *>(OT_CLI_TCAT_PRIV_KEY),
@@ -128,7 +130,7 @@
     otBleSecureSetSslAuthMode(GetInstancePtr(), true);
 
     SuccessOrExit(error = otBleSecureStart(GetInstancePtr(), nullptr, HandleBleSecureReceive, true, nullptr));
-    SuccessOrExit(error = otBleSecureTcatStart(GetInstancePtr(), &sVendorInfo, nullptr));
+    SuccessOrExit(error = otBleSecureTcatStart(GetInstancePtr(), &mVendorInfo, nullptr));
 
 exit:
     return error;
@@ -137,11 +139,10 @@
 template <> otError Tcat::Process<Cmd("stop")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
-    otError error = OT_ERROR_NONE;
 
     otBleSecureStop(GetInstancePtr());
 
-    return error;
+    return OT_ERROR_NONE;
 }
 
 otError Tcat::Process(Arg aArgs[])
diff --git a/src/cli/cli_tcat.hpp b/src/cli/cli_tcat.hpp
index 3f1d0be..697cba1 100644
--- a/src/cli/cli_tcat.hpp
+++ b/src/cli/cli_tcat.hpp
@@ -31,7 +31,9 @@
 
 #include "openthread-core-config.h"
 
-#include "cli/cli_output.hpp"
+#include <openthread/tcat.h>
+
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE && OPENTHREAD_CONFIG_CLI_BLE_SECURE_ENABLE
 
@@ -43,11 +45,9 @@
  * Implements the Tcat CLI interpreter.
  *
  */
-class Tcat : private Output
+class Tcat : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -56,7 +56,7 @@
      *
      */
     Tcat(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
@@ -78,6 +78,8 @@
     using Command = CommandEntry<Tcat>;
 
     template <CommandId kCommandId> otError Process(Arg aArgs[]);
+
+    otTcatVendorInfo mVendorInfo;
 };
 
 } // namespace Cli
diff --git a/src/cli/cli_tcp.cpp b/src/cli/cli_tcp.cpp
index eadb27b..b891c2d 100644
--- a/src/cli/cli_tcp.cpp
+++ b/src/cli/cli_tcp.cpp
@@ -61,7 +61,7 @@
 #endif
 
 TcpExample::TcpExample(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
     , mInitialized(false)
     , mEndpointConnected(false)
     , mEndpointConnectedFastOpen(false)
@@ -81,9 +81,10 @@
 void TcpExample::MbedTlsDebugOutput(void *ctx, int level, const char *file, int line, const char *str)
 {
     TcpExample &tcpExample = *static_cast<TcpExample *>(ctx);
+
     tcpExample.OutputLine("%s:%d:%d: %s", file, line, level, str);
 }
-#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+#endif
 
 /**
  * @cli tcp init
@@ -221,6 +222,7 @@
 
         ClearAllBytes(endpointArgs);
         endpointArgs.mEstablishedCallback = HandleTcpEstablishedCallback;
+
         if (mUseCircularSendBuffer)
         {
             endpointArgs.mForwardProgressCallback = HandleTcpForwardProgressCallback;
@@ -229,6 +231,7 @@
         {
             endpointArgs.mSendDoneCallback = HandleTcpSendDoneCallback;
         }
+
         endpointArgs.mReceiveAvailableCallback = HandleTcpReceiveAvailableCallback;
         endpointArgs.mDisconnectedCallback     = HandleTcpDisconnectedCallback;
         endpointArgs.mContext                  = this;
@@ -247,6 +250,7 @@
         listenerArgs.mContext             = this;
 
         error = otTcpListenerInitialize(GetInstancePtr(), &mListener, &listenerArgs);
+
         if (error != OT_ERROR_NONE)
         {
             IgnoreReturnValue(otTcpEndpointDeinitialize(&mEndpoint));
@@ -289,7 +293,7 @@
         mbedtls_pk_free(&mPKey);
         mbedtls_x509_crt_free(&mSrvCert);
     }
-#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+#endif
 
     endpointError = otTcpEndpointDeinitialize(&mEndpoint);
     mSendBusy     = false;
@@ -322,9 +326,8 @@
  * Associates an IPv6 address and a port to the example TCP endpoint provided by
  * the `tcp` CLI. Associating the TCP endpoint to an IPv6
  * address and port is referred to as "naming the TCP endpoint." This binds the
- * endpoint for communication.
+ * endpoint for communication. @moreinfo{@tcp}.
  * @sa otTcpBind
- * @sa @tcp
  */
 template <> otError TcpExample::Process<Cmd("bind")>(Arg aArgs[])
 {
@@ -368,28 +371,28 @@
  * Establishes a connection with the specified peer.
  * @par
  * If the connection establishment is successful, the resulting TCP connection
- * is associated with the example TCP endpoint.
+ * is associated with the example TCP endpoint. @moreinfo{@tcp}.
  * @sa otTcpConnect
- * @sa @tcp
  */
 template <> otError TcpExample::Process<Cmd("connect")>(Arg aArgs[])
 {
     otError    error;
     otSockAddr sockaddr;
-    bool       nat64SynthesizedAddress;
+    bool       nat64Synth;
     uint32_t   flags;
 
     VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);
 
-    SuccessOrExit(
-        error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], sockaddr.mAddress, nat64SynthesizedAddress));
-    if (nat64SynthesizedAddress)
+    SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], sockaddr.mAddress, nat64Synth));
+
+    if (nat64Synth)
     {
         OutputFormat("Connecting to synthesized IPv6 address: ");
         OutputIp6AddressLine(sockaddr.mAddress);
     }
 
     SuccessOrExit(error = aArgs[1].ParseAsUint16(sockaddr.mPort));
+
     if (aArgs[2].IsEmpty())
     {
         flags = OT_TCP_CONNECT_NO_FAST_OPEN;
@@ -408,6 +411,7 @@
         {
             ExitNow(error = OT_ERROR_INVALID_ARGS);
         }
+
         VerifyOrExit(aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
     }
 
@@ -421,7 +425,7 @@
             OutputLine("mbedtls_ssl_config_defaults returned %d", rv);
         }
     }
-#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+#endif
 
     SuccessOrExit(error = otTcpConnect(&mEndpoint, &sockaddr, flags));
     mEndpointConnected         = true;
@@ -450,8 +454,7 @@
  * remote TCP endpoint.
  * @par
  * Sends data over the TCP connection associated with the example TCP endpoint
- * that is provided with the `tcp` CLI.
- * @sa @tcp
+ * that is provided with the `tcp` CLI. @moreinfo{@tcp}.
  */
 template <> otError TcpExample::Process<Cmd("send")>(Arg aArgs[])
 {
@@ -469,16 +472,19 @@
         {
             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);
+                ExitNow(error = OT_ERROR_FAILED);
             }
-            error = kErrorNone;
+
+            error = OT_ERROR_NONE;
         }
         else
-#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+#endif
         {
             size_t written;
+
             SuccessOrExit(error = otTcpCircularSendBufferWrite(&mEndpoint, &mSendBuffer, aArgs[0].GetCString(),
                                                                aArgs[0].GetLength(), &written, 0));
         }
@@ -529,6 +535,7 @@
     if (aArgs[0] == "result")
     {
         OutputFormat("TCP Benchmark Status: ");
+
         if (mBenchmarkBytesTotal != 0)
         {
             OutputLine("Ongoing");
@@ -574,6 +581,7 @@
             SuccessOrExit(error = aArgs[1].ParseAsUint32(mBenchmarkBytesTotal));
             VerifyOrExit(mBenchmarkBytesTotal != 0, error = OT_ERROR_INVALID_ARGS);
         }
+
         VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
 
         mBenchmarkStart       = TimerMilli::GetNow();
@@ -597,10 +605,12 @@
                 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);
@@ -684,9 +694,8 @@
  *   and are associated with the example TCP endpoint.
  * @par
  * Uses the example TCP listener to listen for incoming connections on the
- * specified IPv6 address and port.
+ * specified IPv6 address and port. @moreinfo{@tcp}.
  * @sa otTcpListen
- * @sa @tcp
  */
 template <> otError TcpExample::Process<Cmd("listen")>(Arg aArgs[])
 {
@@ -816,7 +825,7 @@
         PrepareTlsHandshake();
         ContinueTlsHandshake();
     }
-#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+#endif
 }
 
 void TcpExample::HandleTcpSendDone(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData)
@@ -838,10 +847,13 @@
     {
         OT_ASSERT(aData != &mSendLink);
         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(mSendBufferBytes);
+
             if (otTcpSendByReference(&mEndpoint, aData, 0) != OT_ERROR_NONE)
             {
                 OutputLine("TCP Benchmark Failed");
@@ -911,7 +923,7 @@
 #if OPENTHREAD_CONFIG_TLS_ENABLE
     if (mUseTls && ContinueTlsHandshake())
     {
-        return;
+        ExitNow();
     }
 #endif
 
@@ -921,15 +933,18 @@
         if (mUseTls)
         {
             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
@@ -945,13 +960,16 @@
         {
             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));
         }
@@ -961,6 +979,11 @@
     {
         OutputLine("TCP: Reached end of stream");
     }
+
+    ExitNow();
+
+exit:
+    return;
 }
 
 void TcpExample::HandleTcpDisconnected(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason)
@@ -1039,8 +1062,10 @@
         {
             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);
@@ -1080,6 +1105,7 @@
         {
             int rv = mbedtls_ssl_write(&mSslContext, reinterpret_cast<const unsigned char *>(sBenchmarkData),
                                        toSendThisIteration);
+
             if (rv > 0)
             {
                 written = static_cast<size_t>(rv);
@@ -1087,12 +1113,13 @@
             }
             else if (rv != MBEDTLS_ERR_SSL_WANT_WRITE && rv != MBEDTLS_ERR_SSL_WANT_READ)
             {
-                ExitNow(error = kErrorFailed);
+                ExitNow(error = OT_ERROR_FAILED);
             }
-            error = kErrorNone;
+
+            error = OT_ERROR_NONE;
         }
         else
-#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+#endif
         {
             SuccessOrExit(error = otTcpCircularSendBufferWrite(&mEndpoint, &mSendBuffer, sBenchmarkData,
                                                                toSendThisIteration, &written, flag));
@@ -1136,17 +1163,21 @@
 void TcpExample::PrepareTlsHandshake(void)
 {
     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;
@@ -1160,6 +1191,7 @@
     if (!mTlsHandshakeComplete)
     {
         rv = mbedtls_ssl_handshake(&mSslContext);
+
         if (rv == 0)
         {
             OutputLine("TLS Handshake Complete");
@@ -1169,6 +1201,7 @@
         {
             OutputLine("TLS Handshake Failed: %d", rv);
         }
+
         wasNotAlreadyDone = true;
     }
 
diff --git a/src/cli/cli_tcp.hpp b/src/cli/cli_tcp.hpp
index 50e7280..e658ede 100644
--- a/src/cli/cli_tcp.hpp
+++ b/src/cli/cli_tcp.hpp
@@ -49,7 +49,7 @@
 #endif
 
 #include "cli/cli_config.h"
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 #include "common/time.hpp"
 
 namespace ot {
@@ -59,11 +59,9 @@
  * Implements a CLI-based TCP example.
  *
  */
-class TcpExample : private Output
+class TcpExample : private Utils
 {
 public:
-    using Arg = Utils::CmdLineParser::Arg;
-
     /**
      * Constructor
      *
diff --git a/src/cli/cli_udp.cpp b/src/cli/cli_udp.cpp
index 7114564..299be12 100644
--- a/src/cli/cli_udp.cpp
+++ b/src/cli/cli_udp.cpp
@@ -44,7 +44,7 @@
 namespace Cli {
 
 UdpExample::UdpExample(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-    : Output(aInstance, aOutputImplementer)
+    : Utils(aInstance, aOutputImplementer)
     , mLinkSecurityEnabled(true)
 {
     ClearAllBytes(mSocket);
@@ -76,9 +76,8 @@
  * - `port`: UDP port number to bind to. Each of the examples is using port number 1234.
  * @par
  * Assigns an IPv6 address and a port to an open socket, which binds the socket for communication.
- * Assigning the IPv6 address and port is referred to as naming the socket.
+ * Assigning the IPv6 address and port is referred to as naming the socket. @moreinfo{@udp}.
  * @sa otUdpBind
- * @sa @udp
  */
 template <> otError UdpExample::Process<Cmd("bind")>(Arg aArgs[])
 {
@@ -127,17 +126,17 @@
  * `InvalidState` when the preferred NAT64 prefix is unavailable.
  * @par api_copy
  * #otUdpConnect
- * @sa @udp
+ * @moreinfo{@udp}.
  */
 template <> otError UdpExample::Process<Cmd("connect")>(Arg aArgs[])
 {
     otError    error;
     otSockAddr sockaddr;
-    bool       nat64SynthesizedAddress;
+    bool       nat64Synth;
 
-    SuccessOrExit(
-        error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], sockaddr.mAddress, nat64SynthesizedAddress));
-    if (nat64SynthesizedAddress)
+    SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], sockaddr.mAddress, nat64Synth));
+
+    if (nat64Synth)
     {
         OutputFormat("Connecting to synthesized IPv6 address: ");
         OutputIp6AddressLine(sockaddr.mAddress);
@@ -244,12 +243,11 @@
  *   - `-s`: Auto-generated payload with the specified length given in the `value` parameter.
  *   - `-x`: Binary data in hexadecimal representation given in the `value` parameter.
  * @par
- * Sends a UDP message using the socket.
+ * Sends a UDP message using the socket. @moreinfo{@udp}.
  * @csa{udp open}
  * @csa{udp bind}
  * @csa{udp connect}
  * @sa otUdpSend
- * @sa @udp
  */
 template <> otError UdpExample::Process<Cmd("send")>(Arg aArgs[])
 {
@@ -269,11 +267,12 @@
 
     if (!aArgs[2].IsEmpty())
     {
-        bool nat64SynthesizedAddress;
+        bool nat64Synth;
 
-        SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], messageInfo.mPeerAddr,
-                                                             nat64SynthesizedAddress));
-        if (nat64SynthesizedAddress)
+        SuccessOrExit(
+            error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], messageInfo.mPeerAddr, nat64Synth));
+
+        if (nat64Synth)
         {
             OutputFormat("Sending to synthesized IPv6 address: ");
             OutputIp6AddressLine(messageInfo.mPeerAddr);
@@ -409,7 +408,7 @@
     while (!done)
     {
         length = sizeof(buf);
-        error  = Utils::CmdLineParser::ParseAsHexStringSegment(aHexString, length, buf);
+        error  = ot::Utils::CmdLineParser::ParseAsHexStringSegment(aHexString, length, buf);
 
         VerifyOrExit((error == OT_ERROR_NONE) || (error == OT_ERROR_PENDING));
         done = (error == OT_ERROR_NONE);
diff --git a/src/cli/cli_udp.hpp b/src/cli/cli_udp.hpp
index 0c15c80..d1c7050 100644
--- a/src/cli/cli_udp.hpp
+++ b/src/cli/cli_udp.hpp
@@ -38,7 +38,7 @@
 
 #include <openthread/udp.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 namespace ot {
 namespace Cli {
@@ -47,11 +47,9 @@
  * Implements a CLI-based UDP example.
  *
  */
-class UdpExample : private Output
+class UdpExample : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
diff --git a/src/cli/cli_output.cpp b/src/cli/cli_utils.cpp
similarity index 72%
rename from src/cli/cli_output.cpp
rename to src/cli/cli_utils.cpp
index 679f045..4a146a8 100644
--- a/src/cli/cli_output.cpp
+++ b/src/cli/cli_utils.cpp
@@ -31,7 +31,7 @@
  *   This file contains implementation of the CLI output module.
  */
 
-#include "cli_output.hpp"
+#include "cli_utils.hpp"
 
 #include <stdio.h>
 #include <stdlib.h>
@@ -48,7 +48,7 @@
 namespace ot {
 namespace Cli {
 
-const char Output::kUnknownString[] = "unknown";
+const char Utils::kUnknownString[] = "unknown";
 
 OutputImplementer::OutputImplementer(otCliOutputCallback aCallback, void *aCallbackContext)
     : mCallback(aCallback)
@@ -60,7 +60,7 @@
 {
 }
 
-void Output::OutputFormat(const char *aFormat, ...)
+void Utils::OutputFormat(const char *aFormat, ...)
 {
     va_list args;
 
@@ -69,7 +69,7 @@
     va_end(args);
 }
 
-void Output::OutputFormat(uint8_t aIndentSize, const char *aFormat, ...)
+void Utils::OutputFormat(uint8_t aIndentSize, const char *aFormat, ...)
 {
     va_list args;
 
@@ -80,7 +80,7 @@
     va_end(args);
 }
 
-void Output::OutputLine(const char *aFormat, ...)
+void Utils::OutputLine(const char *aFormat, ...)
 {
     va_list args;
 
@@ -91,7 +91,7 @@
     OutputNewLine();
 }
 
-void Output::OutputLine(uint8_t aIndentSize, const char *aFormat, ...)
+void Utils::OutputLine(uint8_t aIndentSize, const char *aFormat, ...)
 {
     va_list args;
 
@@ -104,11 +104,11 @@
     OutputNewLine();
 }
 
-void Output::OutputNewLine(void) { OutputFormat("\r\n"); }
+void Utils::OutputNewLine(void) { OutputFormat("\r\n"); }
 
-void Output::OutputSpaces(uint8_t aCount) { OutputFormat("%*s", aCount, ""); }
+void Utils::OutputSpaces(uint8_t aCount) { OutputFormat("%*s", aCount, ""); }
 
-void Output::OutputBytes(const uint8_t *aBytes, uint16_t aLength)
+void Utils::OutputBytes(const uint8_t *aBytes, uint16_t aLength)
 {
     for (uint16_t i = 0; i < aLength; i++)
     {
@@ -116,13 +116,13 @@
     }
 }
 
-void Output::OutputBytesLine(const uint8_t *aBytes, uint16_t aLength)
+void Utils::OutputBytesLine(const uint8_t *aBytes, uint16_t aLength)
 {
     OutputBytes(aBytes, aLength);
     OutputNewLine();
 }
 
-const char *Output::Uint64ToString(uint64_t aUint64, Uint64StringBuffer &aBuffer)
+const char *Utils::Uint64ToString(uint64_t aUint64, Uint64StringBuffer &aBuffer)
 {
     char *cur = &aBuffer.mChars[Uint64StringBuffer::kSize - 1];
 
@@ -143,24 +143,24 @@
     return cur;
 }
 
-void Output::OutputUint64(uint64_t aUint64)
+void Utils::OutputUint64(uint64_t aUint64)
 {
     Uint64StringBuffer buffer;
 
     OutputFormat("%s", Uint64ToString(aUint64, buffer));
 }
 
-void Output::OutputUint64Line(uint64_t aUint64)
+void Utils::OutputUint64Line(uint64_t aUint64)
 {
     OutputUint64(aUint64);
     OutputNewLine();
 }
 
-void Output::OutputEnabledDisabledStatus(bool aEnabled) { OutputLine(aEnabled ? "Enabled" : "Disabled"); }
+void Utils::OutputEnabledDisabledStatus(bool aEnabled) { OutputLine(aEnabled ? "Enabled" : "Disabled"); }
 
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
 
-void Output::OutputIp6Address(const otIp6Address &aAddress)
+void Utils::OutputIp6Address(const otIp6Address &aAddress)
 {
     char string[OT_IP6_ADDRESS_STRING_SIZE];
 
@@ -169,13 +169,13 @@
     return OutputFormat("%s", string);
 }
 
-void Output::OutputIp6AddressLine(const otIp6Address &aAddress)
+void Utils::OutputIp6AddressLine(const otIp6Address &aAddress)
 {
     OutputIp6Address(aAddress);
     OutputNewLine();
 }
 
-void Output::OutputIp6Prefix(const otIp6Prefix &aPrefix)
+void Utils::OutputIp6Prefix(const otIp6Prefix &aPrefix)
 {
     char string[OT_IP6_PREFIX_STRING_SIZE];
 
@@ -184,25 +184,25 @@
     OutputFormat("%s", string);
 }
 
-void Output::OutputIp6PrefixLine(const otIp6Prefix &aPrefix)
+void Utils::OutputIp6PrefixLine(const otIp6Prefix &aPrefix)
 {
     OutputIp6Prefix(aPrefix);
     OutputNewLine();
 }
 
-void Output::OutputIp6Prefix(const otIp6NetworkPrefix &aPrefix)
+void Utils::OutputIp6Prefix(const otIp6NetworkPrefix &aPrefix)
 {
     OutputFormat("%x:%x:%x:%x::/64", (aPrefix.m8[0] << 8) | aPrefix.m8[1], (aPrefix.m8[2] << 8) | aPrefix.m8[3],
                  (aPrefix.m8[4] << 8) | aPrefix.m8[5], (aPrefix.m8[6] << 8) | aPrefix.m8[7]);
 }
 
-void Output::OutputIp6PrefixLine(const otIp6NetworkPrefix &aPrefix)
+void Utils::OutputIp6PrefixLine(const otIp6NetworkPrefix &aPrefix)
 {
     OutputIp6Prefix(aPrefix);
     OutputNewLine();
 }
 
-void Output::OutputSockAddr(const otSockAddr &aSockAddr)
+void Utils::OutputSockAddr(const otSockAddr &aSockAddr)
 {
     char string[OT_IP6_SOCK_ADDR_STRING_SIZE];
 
@@ -211,13 +211,13 @@
     return OutputFormat("%s", string);
 }
 
-void Output::OutputSockAddrLine(const otSockAddr &aSockAddr)
+void Utils::OutputSockAddrLine(const otSockAddr &aSockAddr)
 {
     OutputSockAddr(aSockAddr);
     OutputNewLine();
 }
 
-void Output::OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength)
+void Utils::OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength)
 {
     otDnsTxtEntry         entry;
     otDnsTxtEntryIterator iterator;
@@ -262,7 +262,7 @@
     OutputFormat("]");
 }
 
-const char *Output::PercentageToString(uint16_t aValue, PercentageStringBuffer &aBuffer)
+const char *Utils::PercentageToString(uint16_t aValue, PercentageStringBuffer &aBuffer)
 {
     uint32_t     scaledValue = aValue;
     StringWriter writer(aBuffer.mChars, sizeof(aBuffer.mChars));
@@ -275,7 +275,7 @@
 
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
 
-void Output::OutputFormatV(const char *aFormat, va_list aArguments) { mImplementer.OutputV(aFormat, aArguments); }
+void Utils::OutputFormatV(const char *aFormat, va_list aArguments) { mImplementer.OutputV(aFormat, aArguments); }
 
 void OutputImplementer::OutputV(const char *aFormat, va_list aArguments)
 {
@@ -376,7 +376,7 @@
 }
 
 #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
-void Output::LogInput(const Arg *aArgs)
+void Utils::LogInput(const Arg *aArgs)
 {
     String<kInputOutputLogStringSize> inputString;
 
@@ -389,7 +389,7 @@
 }
 #endif
 
-void Output::OutputTableHeader(uint8_t aNumColumns, const char *const aTitles[], const uint8_t aWidths[])
+void Utils::OutputTableHeader(uint8_t aNumColumns, const char *const aTitles[], const uint8_t aWidths[])
 {
     for (uint8_t index = 0; index < aNumColumns; index++)
     {
@@ -418,7 +418,7 @@
     OutputTableSeparator(aNumColumns, aWidths);
 }
 
-void Output::OutputTableSeparator(uint8_t aNumColumns, const uint8_t aWidths[])
+void Utils::OutputTableSeparator(uint8_t aNumColumns, const uint8_t aWidths[])
 {
     for (uint8_t index = 0; index < aNumColumns; index++)
     {
@@ -433,5 +433,95 @@
     OutputLine("+");
 }
 
+otError Utils::ParseEnableOrDisable(const Arg &aArg, bool &aEnable)
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArg == "enable")
+    {
+        aEnable = true;
+    }
+    else if (aArg == "disable")
+    {
+        aEnable = false;
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+    return error;
+}
+
+otError Utils::ProcessEnableDisable(Arg aArgs[], SetEnabledHandler aSetEnabledHandler)
+{
+    otError error = OT_ERROR_NONE;
+    bool    enable;
+
+    if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE)
+    {
+        aSetEnabledHandler(GetInstancePtr(), enable);
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+    return error;
+}
+
+otError Utils::ProcessEnableDisable(Arg aArgs[], SetEnabledHandlerFailable aSetEnabledHandler)
+{
+    otError error = OT_ERROR_NONE;
+    bool    enable;
+
+    if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE)
+    {
+        error = aSetEnabledHandler(GetInstancePtr(), enable);
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+    return error;
+}
+
+otError Utils::ProcessEnableDisable(Arg               aArgs[],
+                                    IsEnabledHandler  aIsEnabledHandler,
+                                    SetEnabledHandler aSetEnabledHandler)
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArgs[0].IsEmpty())
+    {
+        OutputEnabledDisabledStatus(aIsEnabledHandler(GetInstancePtr()));
+    }
+    else
+    {
+        error = ProcessEnableDisable(aArgs, aSetEnabledHandler);
+    }
+
+    return error;
+}
+
+otError Utils::ProcessEnableDisable(Arg                       aArgs[],
+                                    IsEnabledHandler          aIsEnabledHandler,
+                                    SetEnabledHandlerFailable aSetEnabledHandler)
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArgs[0].IsEmpty())
+    {
+        OutputEnabledDisabledStatus(aIsEnabledHandler(GetInstancePtr()));
+    }
+    else
+    {
+        error = ProcessEnableDisable(aArgs, aSetEnabledHandler);
+    }
+
+    return error;
+}
+
 } // namespace Cli
 } // namespace ot
diff --git a/src/cli/cli_output.hpp b/src/cli/cli_utils.hpp
similarity index 75%
rename from src/cli/cli_output.hpp
rename to src/cli/cli_utils.hpp
index 3029108..ebf9cc1 100644
--- a/src/cli/cli_output.hpp
+++ b/src/cli/cli_utils.hpp
@@ -70,7 +70,7 @@
     return (aString[0] == '\0') ? 0 : (static_cast<uint8_t>(aString[0]) + Cmd(aString + 1) * 255u);
 }
 
-class Output;
+class Utils;
 
 /**
  * Implements the basic output functions.
@@ -78,7 +78,7 @@
  */
 class OutputImplementer
 {
-    friend class Output;
+    friend class Utils;
 
 public:
     /**
@@ -111,13 +111,13 @@
 };
 
 /**
- * Provides CLI output helper methods.
+ * Provides CLI helper methods.
  *
  */
-class Output
+class Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg; ///< An argument
+    typedef ot::Utils::CmdLineParser::Arg Arg; ///< An argument
 
     /**
      * Represent a CLI command table entry, mapping a command with `aName` to a handler method.
@@ -190,7 +190,7 @@
      * @param[in] aImplementer        An `OutputImplementer`.
      *
      */
-    Output(otInstance *aInstance, OutputImplementer &aImplementer)
+    Utils(otInstance *aInstance, OutputImplementer &aImplementer)
         : mInstance(aInstance)
         , mImplementer(aImplementer)
     {
@@ -543,6 +543,108 @@
         memset(reinterpret_cast<void *>(&aObject), 0, sizeof(ObjectType));
     }
 
+    // Definitions of handlers to process Get/Set/Enable/Disable.
+    template <typename ValueType> using GetHandler         = ValueType (&)(otInstance *);
+    template <typename ValueType> using SetHandler         = void (&)(otInstance *, ValueType);
+    template <typename ValueType> using SetHandlerFailable = otError (&)(otInstance *, ValueType);
+    using IsEnabledHandler                                 = bool (&)(otInstance *);
+    using SetEnabledHandler                                = void (&)(otInstance *, bool);
+    using SetEnabledHandlerFailable                        = otError (&)(otInstance *, bool);
+
+    // Returns format string to output a `ValueType` (e.g., "%u" for `uint16_t`).
+    template <typename ValueType> static constexpr const char *FormatStringFor(void);
+
+    /**
+     * Checks a given argument string against "enable" or "disable" commands.
+     *
+     * @param[in]  aArg     The argument string to parse.
+     * @param[out] aEnable  Boolean variable to return outcome on success.
+     *                      Set to TRUE for "enable" command, and FALSE for "disable" command.
+     *
+     * @retval OT_ERROR_NONE             Successfully parsed the @p aString and updated @p aEnable.
+     * @retval OT_ERROR_INVALID_COMMAND  The @p aString is not "enable" or "disable" command.
+     *
+     */
+    static otError ParseEnableOrDisable(const Arg &aArg, bool &aEnable);
+
+    // General template implementation.
+    // Specializations for `uint32_t` and `int32_t` are added at the end.
+    template <typename ValueType> otError ProcessGet(Arg aArgs[], GetHandler<ValueType> aGetHandler)
+    {
+        static_assert(
+            TypeTraits::IsSame<ValueType, uint8_t>::kValue || TypeTraits::IsSame<ValueType, uint16_t>::kValue ||
+                TypeTraits::IsSame<ValueType, int8_t>::kValue || TypeTraits::IsSame<ValueType, int16_t>::kValue ||
+                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;
+
+        VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+        OutputLine(FormatStringFor<ValueType>(), aGetHandler(GetInstancePtr()));
+
+    exit:
+        return error;
+    }
+
+    template <typename ValueType> otError ProcessSet(Arg aArgs[], SetHandler<ValueType> aSetHandler)
+    {
+        otError   error;
+        ValueType value;
+
+        SuccessOrExit(error = aArgs[0].ParseAs<ValueType>(value));
+        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+        aSetHandler(GetInstancePtr(), value);
+
+    exit:
+        return error;
+    }
+
+    template <typename ValueType> otError ProcessSet(Arg aArgs[], SetHandlerFailable<ValueType> aSetHandler)
+    {
+        otError   error;
+        ValueType value;
+
+        SuccessOrExit(error = aArgs[0].ParseAs<ValueType>(value));
+        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+        error = aSetHandler(GetInstancePtr(), value);
+
+    exit:
+        return error;
+    }
+
+    template <typename ValueType>
+    otError ProcessGetSet(Arg aArgs[], GetHandler<ValueType> aGetHandler, SetHandler<ValueType> aSetHandler)
+    {
+        otError error = ProcessGet(aArgs, aGetHandler);
+
+        VerifyOrExit(error != OT_ERROR_NONE);
+        error = ProcessSet(aArgs, aSetHandler);
+
+    exit:
+        return error;
+    }
+
+    template <typename ValueType>
+    otError ProcessGetSet(Arg aArgs[], GetHandler<ValueType> aGetHandler, SetHandlerFailable<ValueType> aSetHandler)
+    {
+        otError error = ProcessGet(aArgs, aGetHandler);
+
+        VerifyOrExit(error != OT_ERROR_NONE);
+        error = ProcessSet(aArgs, aSetHandler);
+
+    exit:
+        return error;
+    }
+
+    otError ProcessEnableDisable(Arg aArgs[], SetEnabledHandler aSetEnabledHandler);
+    otError ProcessEnableDisable(Arg aArgs[], SetEnabledHandlerFailable aSetEnabledHandler);
+    otError ProcessEnableDisable(Arg aArgs[], IsEnabledHandler aIsEnabledHandler, SetEnabledHandler aSetEnabledHandler);
+    otError ProcessEnableDisable(Arg                       aArgs[],
+                                 IsEnabledHandler          aIsEnabledHandler,
+                                 SetEnabledHandlerFailable aSetEnabledHandler);
+
 protected:
     void OutputFormatV(const char *aFormat, va_list aArguments);
 
@@ -562,6 +664,46 @@
     OutputImplementer &mImplementer;
 };
 
+// Specializations of `FormatStringFor<ValueType>()`
+
+template <> inline constexpr const char *Utils::FormatStringFor<uint8_t>(void) { return "%u"; }
+
+template <> inline constexpr const char *Utils::FormatStringFor<uint16_t>(void) { return "%u"; }
+
+template <> inline constexpr const char *Utils::FormatStringFor<uint32_t>(void) { return "%lu"; }
+
+template <> inline constexpr const char *Utils::FormatStringFor<int8_t>(void) { return "%d"; }
+
+template <> inline constexpr const char *Utils::FormatStringFor<int16_t>(void) { return "%d"; }
+
+template <> inline constexpr const char *Utils::FormatStringFor<int32_t>(void) { return "%ld"; }
+
+template <> inline constexpr const char *Utils::FormatStringFor<const char *>(void) { return "%s"; }
+
+// Specialization of ProcessGet<> for `uint32_t` and `int32_t`
+
+template <> inline otError Utils::ProcessGet<uint32_t>(Arg aArgs[], GetHandler<uint32_t> aGetHandler)
+{
+    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 otError Utils::ProcessGet<int32_t>(Arg aArgs[], GetHandler<int32_t> aGetHandler)
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    OutputLine(FormatStringFor<int32_t>(), static_cast<long int>(aGetHandler(GetInstancePtr())));
+
+exit:
+    return error;
+}
+
 } // namespace Cli
 } // namespace ot
 
diff --git a/src/cli/radio.cmake b/src/cli/radio.cmake
index 449f996..47652d2 100644
--- a/src/cli/radio.cmake
+++ b/src/cli/radio.cmake
@@ -45,7 +45,7 @@
 target_sources(openthread-cli-radio
     PRIVATE
         cli.cpp
-        cli_output.cpp
+        cli_utils.cpp
 )
 
 if(NOT DEFINED OT_MBEDTLS_RCP)
diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn
index c7ee0fa..4cf9d77 100644
--- a/src/core/BUILD.gn
+++ b/src/core/BUILD.gn
@@ -41,6 +41,8 @@
       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 == "1.4") {
+      defines += [ "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_4" ]
     } else if (openthread_config_thread_version != "") {
       assert(false,
              "Unrecognized Thread version: ${openthread_config_thread_version}")
@@ -335,6 +337,7 @@
   "api/link_metrics_api.cpp",
   "api/link_raw_api.cpp",
   "api/logging_api.cpp",
+  "api/mdns_api.cpp",
   "api/mesh_diag_api.cpp",
   "api/message_api.cpp",
   "api/multi_radio_api.cpp",
@@ -359,6 +362,7 @@
   "api/thread_ftd_api.cpp",
   "api/trel_api.cpp",
   "api/udp_api.cpp",
+  "api/verhoeff_checksum_api.cpp",
   "backbone_router/backbone_tmf.cpp",
   "backbone_router/backbone_tmf.hpp",
   "backbone_router/bbr_leader.cpp",
@@ -573,6 +577,8 @@
   "net/ip6_mpl.cpp",
   "net/ip6_mpl.hpp",
   "net/ip6_types.hpp",
+  "net/mdns.cpp",
+  "net/mdns.hpp",
   "net/nat64_translator.cpp",
   "net/nat64_translator.hpp",
   "net/nd6.cpp",
@@ -735,6 +741,8 @@
   "utils/slaac_address.hpp",
   "utils/srp_client_buffers.cpp",
   "utils/srp_client_buffers.hpp",
+  "utils/verhoeff_checksum.cpp",
+  "utils/verhoeff_checksum.hpp",
 ]
 
 openthread_radio_sources = [
@@ -811,6 +819,7 @@
     "config/link_raw.h",
     "config/logging.h",
     "config/mac.h",
+    "config/mdns.h",
     "config/mesh_diag.h",
     "config/mesh_forwarder.h",
     "config/misc.h",
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 5e6cee1..8bc006e 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -62,6 +62,7 @@
     api/link_metrics_api.cpp
     api/link_raw_api.cpp
     api/logging_api.cpp
+    api/mdns_api.cpp
     api/mesh_diag_api.cpp
     api/message_api.cpp
     api/multi_radio_api.cpp
@@ -86,6 +87,7 @@
     api/thread_ftd_api.cpp
     api/trel_api.cpp
     api/udp_api.cpp
+    api/verhoeff_checksum_api.cpp
     backbone_router/backbone_tmf.cpp
     backbone_router/bbr_leader.cpp
     backbone_router/bbr_local.cpp
@@ -177,6 +179,7 @@
     net/ip6_filter.cpp
     net/ip6_headers.cpp
     net/ip6_mpl.cpp
+    net/mdns.cpp
     net/nat64_translator.cpp
     net/nd6.cpp
     net/nd_agent.cpp
@@ -257,6 +260,7 @@
     utils/power_calibration.cpp
     utils/slaac_address.cpp
     utils/srp_client_buffers.cpp
+    utils/verhoeff_checksum.cpp
 )
 
 set(RADIO_COMMON_SOURCES
diff --git a/src/core/api/border_agent_api.cpp b/src/core/api/border_agent_api.cpp
index 3590116..6a9f42f 100644
--- a/src/core/api/border_agent_api.cpp
+++ b/src/core/api/border_agent_api.cpp
@@ -64,4 +64,35 @@
     return AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().GetUdpPort();
 }
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+
+otError otBorderAgentSetEphemeralKey(otInstance *aInstance,
+                                     const char *aKeyString,
+                                     uint32_t    aTimeout,
+                                     uint16_t    aUdpPort)
+{
+    AssertPointerIsNotNull(aKeyString);
+
+    return AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().SetEphemeralKey(aKeyString, aTimeout, aUdpPort);
+}
+
+void otBorderAgentClearEphemeralKey(otInstance *aInstance)
+{
+    AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().ClearEphemeralKey();
+}
+
+bool otBorderAgentIsEphemeralKeyActive(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().IsEphemeralKeyActive();
+}
+
+void otBorderAgentSetEphemeralKeyCallback(otInstance                       *aInstance,
+                                          otBorderAgentEphemeralKeyCallback aCallback,
+                                          void                             *aContext)
+{
+    AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().SetEphemeralKeyCallback(aCallback, aContext);
+}
+
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+
 #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
diff --git a/src/core/api/border_routing_api.cpp b/src/core/api/border_routing_api.cpp
index b62ee70..8bc2b7a 100644
--- a/src/core/api/border_routing_api.cpp
+++ b/src/core/api/border_routing_api.cpp
@@ -75,6 +75,11 @@
     AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().ClearRouteInfoOptionPreference();
 }
 
+otError otBorderRoutingSetExtraRouterAdvertOptions(otInstance *aInstance, const uint8_t *aOptions, uint16_t aLength)
+{
+    return AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().SetExtraRouterAdvertOptions(aOptions, aLength);
+}
+
 otRoutePreference otBorderRoutingGetRoutePreference(otInstance *aInstance)
 {
     return static_cast<otRoutePreference>(
@@ -200,6 +205,14 @@
     return static_cast<otBorderRoutingDhcp6PdState>(
         AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetDhcp6PdState());
 }
+
+void otBorderRoutingDhcp6PdSetRequestCallback(otInstance                           *aInstance,
+                                              otBorderRoutingRequestDhcp6PdCallback aCallback,
+                                              void                                 *aContext)
+{
+    AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().SetRequestDhcp6PdCallback(aCallback, aContext);
+}
+
 #endif
 
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
diff --git a/src/core/api/channel_manager_api.cpp b/src/core/api/channel_manager_api.cpp
index af17e0d..3aa5d36 100644
--- a/src/core/api/channel_manager_api.cpp
+++ b/src/core/api/channel_manager_api.cpp
@@ -33,7 +33,9 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
 
 #include <openthread/channel_manager.h>
 
@@ -43,16 +45,19 @@
 
 using namespace ot;
 
+#if OPENTHREAD_FTD
 void otChannelManagerRequestChannelChange(otInstance *aInstance, uint8_t aChannel)
 {
-    AsCoreType(aInstance).Get<Utils::ChannelManager>().RequestChannelChange(aChannel);
+    AsCoreType(aInstance).Get<Utils::ChannelManager>().RequestNetworkChannelChange(aChannel);
 }
+#endif
 
 uint8_t otChannelManagerGetRequestedChannel(otInstance *aInstance)
 {
     return AsCoreType(aInstance).Get<Utils::ChannelManager>().GetRequestedChannel();
 }
 
+#if OPENTHREAD_FTD
 uint16_t otChannelManagerGetDelay(otInstance *aInstance)
 {
     return AsCoreType(aInstance).Get<Utils::ChannelManager>().GetDelay();
@@ -66,19 +71,39 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
 otError otChannelManagerRequestChannelSelect(otInstance *aInstance, bool aSkipQualityCheck)
 {
-    return AsCoreType(aInstance).Get<Utils::ChannelManager>().RequestChannelSelect(aSkipQualityCheck);
+    return AsCoreType(aInstance).Get<Utils::ChannelManager>().RequestNetworkChannelSelect(aSkipQualityCheck);
 }
 #endif
 
 void otChannelManagerSetAutoChannelSelectionEnabled(otInstance *aInstance, bool aEnabled)
 {
-    AsCoreType(aInstance).Get<Utils::ChannelManager>().SetAutoChannelSelectionEnabled(aEnabled);
+    AsCoreType(aInstance).Get<Utils::ChannelManager>().SetAutoNetworkChannelSelectionEnabled(aEnabled);
 }
 
 bool otChannelManagerGetAutoChannelSelectionEnabled(otInstance *aInstance)
 {
-    return AsCoreType(aInstance).Get<Utils::ChannelManager>().GetAutoChannelSelectionEnabled();
+    return AsCoreType(aInstance).Get<Utils::ChannelManager>().GetAutoNetworkChannelSelectionEnabled();
 }
+#endif // OPENTHREAD_FTD
+
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+#if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
+otError otChannelManagerRequestCslChannelSelect(otInstance *aInstance, bool aSkipQualityCheck)
+{
+    return AsCoreType(aInstance).Get<Utils::ChannelManager>().RequestCslChannelSelect(aSkipQualityCheck);
+}
+#endif
+
+void otChannelManagerSetAutoCslChannelSelectionEnabled(otInstance *aInstance, bool aEnabled)
+{
+    AsCoreType(aInstance).Get<Utils::ChannelManager>().SetAutoCslChannelSelectionEnabled(aEnabled);
+}
+
+bool otChannelManagerGetAutoCslChannelSelectionEnabled(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Utils::ChannelManager>().GetAutoCslChannelSelectionEnabled();
+}
+#endif
 
 otError otChannelManagerSetAutoChannelSelectionInterval(otInstance *aInstance, uint32_t aInterval)
 {
diff --git a/src/core/api/link_metrics_api.cpp b/src/core/api/link_metrics_api.cpp
index 8e678d9..d822ba8 100644
--- a/src/core/api/link_metrics_api.cpp
+++ b/src/core/api/link_metrics_api.cpp
@@ -99,6 +99,11 @@
 }
 
 #if OPENTHREAD_CONFIG_LINK_METRICS_MANAGER_ENABLE
+bool otLinkMetricsManagerIsEnabled(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Utils::LinkMetricsManager>().IsEnabled();
+}
+
 void otLinkMetricsManagerSetEnabled(otInstance *aInstance, bool aEnable)
 {
     AsCoreType(aInstance).Get<Utils::LinkMetricsManager>().SetEnabled(aEnable);
diff --git a/src/core/api/mdns_api.cpp b/src/core/api/mdns_api.cpp
new file mode 100644
index 0000000..8563303
--- /dev/null
+++ b/src/core/api/mdns_api.cpp
@@ -0,0 +1,230 @@
+/*
+ *  Copyright (c) 2024, 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 mDNS API.
+ */
+
+#include "openthread-core-config.h"
+
+#include <openthread/mdns.h>
+
+#include "instance/instance.hpp"
+#include "net/mdns.hpp"
+
+using namespace ot;
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+
+otError otMdnsSetEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().SetEnabled(aEnable, aInfraIfIndex);
+}
+
+bool otMdnsIsEnabled(otInstance *aInstance) { return AsCoreType(aInstance).Get<Dns::Multicast::Core>().IsEnabled(); }
+
+void otMdnsSetQuestionUnicastAllowed(otInstance *aInstance, bool aAllow)
+{
+    AsCoreType(aInstance).Get<Dns::Multicast::Core>().SetQuestionUnicastAllowed(aAllow);
+}
+
+bool otMdnsIsQuestionUnicastAllowed(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().IsQuestionUnicastAllowed();
+}
+
+void otMdnsSetConflictCallback(otInstance *aInstance, otMdnsConflictCallback aCallback)
+{
+    AsCoreType(aInstance).Get<Dns::Multicast::Core>().SetConflictCallback(aCallback);
+}
+
+otError otMdnsRegisterHost(otInstance            *aInstance,
+                           const otMdnsHost      *aHost,
+                           otMdnsRequestId        aRequestId,
+                           otMdnsRegisterCallback aCallback)
+{
+    AssertPointerIsNotNull(aHost);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().RegisterHost(*aHost, aRequestId, aCallback);
+}
+
+otError otMdnsUnregisterHost(otInstance *aInstance, const otMdnsHost *aHost)
+{
+    AssertPointerIsNotNull(aHost);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().UnregisterHost(*aHost);
+}
+
+otError otMdnsRegisterService(otInstance            *aInstance,
+                              const otMdnsService   *aService,
+                              otMdnsRequestId        aRequestId,
+                              otMdnsRegisterCallback aCallback)
+{
+    AssertPointerIsNotNull(aService);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().RegisterService(*aService, aRequestId, aCallback);
+}
+
+otError otMdnsUnregisterService(otInstance *aInstance, const otMdnsService *aService)
+{
+    AssertPointerIsNotNull(aService);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().UnregisterService(*aService);
+}
+
+otError otMdnsRegisterKey(otInstance            *aInstance,
+                          const otMdnsKey       *aKey,
+                          otMdnsRequestId        aRequestId,
+                          otMdnsRegisterCallback aCallback)
+{
+    AssertPointerIsNotNull(aKey);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().RegisterKey(*aKey, aRequestId, aCallback);
+}
+
+otError otMdnsUnregisterKey(otInstance *aInstance, const otMdnsKey *aKey)
+{
+    AssertPointerIsNotNull(aKey);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().UnregisterKey(*aKey);
+}
+
+otMdnsIterator *otMdnsAllocateIterator(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().AllocateIterator();
+}
+
+void otMdnsFreeIterator(otInstance *aInstance, otMdnsIterator *aIterator)
+{
+    AssertPointerIsNotNull(aIterator);
+
+    AsCoreType(aInstance).Get<Dns::Multicast::Core>().FreeIterator(*aIterator);
+}
+
+otError otMdnsGetNextHost(otInstance *aInstance, otMdnsIterator *aIterator, otMdnsHost *aHost, otMdnsEntryState *aState)
+{
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aHost);
+    AssertPointerIsNotNull(aState);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().GetNextHost(*aIterator, *aHost, *aState);
+}
+
+otError otMdnsGetNextService(otInstance       *aInstance,
+                             otMdnsIterator   *aIterator,
+                             otMdnsService    *aService,
+                             otMdnsEntryState *aState)
+{
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aService);
+    AssertPointerIsNotNull(aState);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().GetNextService(*aIterator, *aService, *aState);
+}
+
+otError otMdnsGetNextKey(otInstance *aInstance, otMdnsIterator *aIterator, otMdnsKey *aKey, otMdnsEntryState *aState)
+{
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aKey);
+    AssertPointerIsNotNull(aState);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().GetNextKey(*aIterator, *aKey, *aState);
+}
+
+otError otMdnsStartBrowser(otInstance *aInstance, const otMdnsBrowser *aBroswer)
+{
+    AssertPointerIsNotNull(aBroswer);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StartBrowser(*aBroswer);
+}
+
+otError otMdnsStopBrowser(otInstance *aInstance, const otMdnsBrowser *aBroswer)
+{
+    AssertPointerIsNotNull(aBroswer);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StopBrowser(*aBroswer);
+}
+
+otError otMdnsStartSrvResolver(otInstance *aInstance, const otMdnsSrvResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StartSrvResolver(*aResolver);
+}
+
+otError otMdnsStopSrvResolver(otInstance *aInstance, const otMdnsSrvResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StopSrvResolver(*aResolver);
+}
+
+otError otMdnsStartTxtResolver(otInstance *aInstance, const otMdnsTxtResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StartTxtResolver(*aResolver);
+}
+
+otError otMdnsStopTxtResolver(otInstance *aInstance, const otMdnsTxtResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StopTxtResolver(*aResolver);
+}
+
+otError otMdnsStartIp6AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StartIp6AddressResolver(*aResolver);
+}
+
+otError otMdnsStopIp6AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StopIp6AddressResolver(*aResolver);
+}
+
+otError otMdnsStartIp4AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StartIp4AddressResolver(*aResolver);
+}
+
+otError otMdnsStopIp4AddressResolver(otInstance *aInstance, const otMdnsAddressResolver *aResolver)
+{
+    AssertPointerIsNotNull(aResolver);
+
+    return AsCoreType(aInstance).Get<Dns::Multicast::Core>().StopIp4AddressResolver(*aResolver);
+}
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
diff --git a/src/core/api/message_api.cpp b/src/core/api/message_api.cpp
index 41dedc1..fe55b90 100644
--- a/src/core/api/message_api.cpp
+++ b/src/core/api/message_api.cpp
@@ -90,6 +90,11 @@
 
 int8_t otMessageGetRss(const otMessage *aMessage) { return AsCoreType(aMessage).GetAverageRss(); }
 
+otError otMessageGetThreadLinkInfo(const otMessage *aMessage, otThreadLinkInfo *aLinkInfo)
+{
+    return AsCoreType(aMessage).GetLinkInfo(AsCoreType(aLinkInfo));
+}
+
 otError otMessageAppend(otMessage *aMessage, const void *aBuf, uint16_t aLength)
 {
     AssertPointerIsNotNull(aBuf);
diff --git a/src/core/api/netdiag_api.cpp b/src/core/api/netdiag_api.cpp
index ffcc4c1..36c084d 100644
--- a/src/core/api/netdiag_api.cpp
+++ b/src/core/api/netdiag_api.cpp
@@ -89,6 +89,11 @@
     return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().GetVendorSwVersion();
 }
 
+const char *otThreadGetVendorAppUrl(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().GetVendorAppUrl();
+}
+
 #if OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
 otError otThreadSetVendorName(otInstance *aInstance, const char *aVendorName)
 {
@@ -104,4 +109,9 @@
 {
     return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().SetVendorSwVersion(aVendorSwVersion);
 }
+
+otError otThreadSetVendorAppUrl(otInstance *aInstance, const char *aVendorAppUrl)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().SetVendorAppUrl(aVendorAppUrl);
+}
 #endif
diff --git a/src/core/api/thread_api.cpp b/src/core/api/thread_api.cpp
index 6bf2ca3..97e5508 100644
--- a/src/core/api/thread_api.cpp
+++ b/src/core/api/thread_api.cpp
@@ -275,15 +275,15 @@
 
 void otThreadSetKeySequenceCounter(otInstance *aInstance, uint32_t aKeySequenceCounter)
 {
-    AsCoreType(aInstance).Get<KeyManager>().SetCurrentKeySequence(aKeySequenceCounter);
+    AsCoreType(aInstance).Get<KeyManager>().SetCurrentKeySequence(aKeySequenceCounter, KeyManager::kForceUpdate);
 }
 
-uint32_t otThreadGetKeySwitchGuardTime(otInstance *aInstance)
+uint16_t otThreadGetKeySwitchGuardTime(otInstance *aInstance)
 {
     return AsCoreType(aInstance).Get<KeyManager>().GetKeySwitchGuardTime();
 }
 
-void otThreadSetKeySwitchGuardTime(otInstance *aInstance, uint32_t aKeySwitchGuardTime)
+void otThreadSetKeySwitchGuardTime(otInstance *aInstance, uint16_t aKeySwitchGuardTime)
 {
     AsCoreType(aInstance).Get<KeyManager>().SetKeySwitchGuardTime(aKeySwitchGuardTime);
 }
diff --git a/src/core/api/verhoeff_checksum_api.cpp b/src/core/api/verhoeff_checksum_api.cpp
new file mode 100644
index 0000000..ac658f9
--- /dev/null
+++ b/src/core/api/verhoeff_checksum_api.cpp
@@ -0,0 +1,60 @@
+/*
+ *  Copyright (c) 2024, 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 Verhoeff Checksum public APIs.
+ */
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
+#include <openthread/verhoeff_checksum.h>
+
+#include "common/debug.hpp"
+#include "utils/verhoeff_checksum.hpp"
+
+using namespace ot::Utils;
+
+otError otVerhoeffChecksumCalculate(const char *aDecimalString, char *aChecksum)
+{
+    AssertPointerIsNotNull(aDecimalString);
+    AssertPointerIsNotNull(aChecksum);
+
+    return VerhoeffChecksum::Calculate(aDecimalString, *aChecksum);
+}
+
+otError otVerhoeffChecksumValidate(const char *aDecimalString)
+{
+    AssertPointerIsNotNull(aDecimalString);
+
+    return VerhoeffChecksum::Validate(aDecimalString);
+}
+
+#endif // OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
diff --git a/src/core/backbone_router/bbr_manager.cpp b/src/core/backbone_router/bbr_manager.cpp
index 551cfa2..4fb15e4 100644
--- a/src/core/backbone_router/bbr_manager.cpp
+++ b/src/core/backbone_router/bbr_manager.cpp
@@ -40,6 +40,7 @@
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
 #include "common/num_utils.hpp"
+#include "common/numeric_limits.hpp"
 #include "common/random.hpp"
 #include "instance/instance.hpp"
 #include "thread/mle_types.hpp"
@@ -202,7 +203,7 @@
     }
     else
     {
-        VerifyOrExit(timeout < UINT32_MAX, status = ThreadStatusTlv::kMlrNoPersistent);
+        VerifyOrExit(timeout < NumericLimits<uint32_t>::kMax, status = ThreadStatusTlv::kMlrNoPersistent);
 
         if (timeout != 0)
         {
diff --git a/src/core/border_router/infra_if.cpp b/src/core/border_router/infra_if.cpp
index a152e62..0b44ee2 100644
--- a/src/core/border_router/infra_if.cpp
+++ b/src/core/border_router/infra_if.cpp
@@ -156,6 +156,10 @@
     Get<Srp::AdvertisingProxy>().HandleInfraIfStateChanged();
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF
+    Get<Dns::Multicast::Core>().HandleInfraIfStateChanged();
+#endif
+
 exit:
     return error;
 }
diff --git a/src/core/border_router/routing_manager.cpp b/src/core/border_router/routing_manager.cpp
index 81f2d9a..cd9e66a 100644
--- a/src/core/border_router/routing_manager.cpp
+++ b/src/core/border_router/routing_manager.cpp
@@ -109,6 +109,11 @@
     else if (aInfraIfIndex != mInfraIf.GetIfIndex())
     {
         LogInfo("Reinitializing - InfraIfIndex:%lu -> %lu", ToUlong(mInfraIf.GetIfIndex()), ToUlong(aInfraIfIndex));
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF
+        IgnoreError(Get<Dns::Multicast::Core>().SetEnabled(false, mInfraIf.GetIfIndex()));
+#endif
+
         mInfraIf.SetIfIndex(aInfraIfIndex);
     }
 
@@ -363,6 +368,22 @@
     return;
 }
 
+Error RoutingManager::SetExtraRouterAdvertOptions(const uint8_t *aOptions, uint16_t aLength)
+{
+    Error error = kErrorNone;
+
+    if (aOptions == nullptr)
+    {
+        mExtraRaOptions.Free();
+    }
+    else
+    {
+        error = mExtraRaOptions.SetFrom(aOptions, aLength);
+    }
+
+    return error;
+}
+
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
 void RoutingManager::HandleSrpServerAutoEnableMode(void)
 {
@@ -478,7 +499,10 @@
     mNat64PrefixManager.Evaluate();
 #endif
 
-    SendRouterAdvertisement(kAdvPrefixesFromNetData);
+    if (IsInitalPolicyEvaluationDone())
+    {
+        SendRouterAdvertisement(kAdvPrefixesFromNetData);
+    }
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
     if (Get<Srp::Server>().IsAutoEnableMode() && IsInitalPolicyEvaluationDone())
@@ -568,35 +592,24 @@
 
 void RoutingManager::SendRouterAdvertisement(RouterAdvTxMode aRaTxMode)
 {
-    // RA message max length is derived to accommodate:
-    //
-    // - The RA header.
-    // - One RA Flags Extensions Option (with stub router flag).
-    // - One PIO for current local on-link prefix.
-    // - At most `kMaxOldPrefixes` for old deprecating on-link prefixes.
-    // - At most 3 times `kMaxOnMeshPrefixes` RIO for on-mesh prefixes.
-    //   Factor three is used for RIOs to account for any new prefix
-    //   with older prefixes entries being deprecated and prefixes
-    //   being invalidated.
-
-    static constexpr uint16_t kMaxRaLength =
-        sizeof(Ip6::Nd::RouterAdvertMessage::Header) + sizeof(Ip6::Nd::RaFlagsExtOption) +
-        sizeof(Ip6::Nd::PrefixInfoOption) + sizeof(Ip6::Nd::PrefixInfoOption) * OnLinkPrefixManager::kMaxOldPrefixes +
-        3 * kMaxOnMeshPrefixes * (sizeof(Ip6::Nd::RouteInfoOption) + sizeof(Ip6::Prefix));
-
-    uint8_t                      buffer[kMaxRaLength];
-    Ip6::Nd::RouterAdvertMessage raMsg(mRaInfo.mHeader, buffer);
+    Error                   error = kErrorNone;
+    RouterAdvert::TxMessage raMsg;
+    RouterAdvert::Header    header;
+    Ip6::Address            destAddress;
+    InfraIf::Icmp6Packet    packet;
 
     LogInfo("Preparing RA");
 
-    mDiscoveredPrefixTable.DetermineAndSetFlags(raMsg);
+    header = mRaInfo.mHeader;
+    mDiscoveredPrefixTable.DetermineAndSetFlags(header);
 
-    LogInfo("- RA Header - flags - M:%u O:%u", raMsg.GetHeader().IsManagedAddressConfigFlagSet(),
-            raMsg.GetHeader().IsOtherConfigFlagSet());
-    LogInfo("- RA Header - default route - lifetime:%u", raMsg.GetHeader().GetRouterLifetime());
+    SuccessOrExit(error = raMsg.AppendHeader(header));
+
+    LogInfo("- RA Header - flags - M:%u O:%u", header.IsManagedAddressConfigFlagSet(), header.IsOtherConfigFlagSet());
+    LogInfo("- RA Header - default route - lifetime:%u", header.GetRouterLifetime());
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_STUB_ROUTER_FLAG_IN_EMITTED_RA_ENABLE
-    SuccessOrAssert(raMsg.AppendFlagsExtensionOption(/* aStubRouterFlag */ true));
+    SuccessOrExit(error = raMsg.AppendFlagsExtensionOption(/* aStubRouterFlag */ true));
     LogInfo("- FlagsExt - StubRouter:1");
 #endif
 
@@ -604,109 +617,42 @@
     // advertised or deprecated and for old prefix if is being
     // deprecated.
 
-    mOnLinkPrefixManager.AppendAsPiosTo(raMsg);
+    SuccessOrExit(error = mOnLinkPrefixManager.AppendAsPiosTo(raMsg));
 
     if (aRaTxMode == kInvalidateAllPrevPrefixes)
     {
-        mRioAdvertiser.InvalidatPrevRios(raMsg);
+        SuccessOrExit(error = mRioAdvertiser.InvalidatPrevRios(raMsg));
     }
     else
     {
-        mRioAdvertiser.AppendRios(raMsg);
+        SuccessOrExit(error = mRioAdvertiser.AppendRios(raMsg));
     }
 
-    if (raMsg.ContainsAnyOptions())
+    if (mExtraRaOptions.GetLength() > 0)
     {
-        Error        error;
-        Ip6::Address destAddress;
-
-        ++mRaInfo.mTxCount;
-
-        destAddress.SetToLinkLocalAllNodesMulticast();
-
-        error = mInfraIf.Send(raMsg.GetAsPacket(), destAddress);
-
-        if (error == kErrorNone)
-        {
-            mRaInfo.mLastTxTime = TimerMilli::GetNow();
-            Get<Ip6::Ip6>().GetBorderRoutingCounters().mRaTxSuccess++;
-            LogInfo("Sent RA 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 RA on %s: %s", mInfraIf.ToString().AsCString(), ErrorToString(error));
-        }
-    }
-}
-
-bool RoutingManager::IsReceivedRouterAdvertFromManager(const Ip6::Nd::RouterAdvertMessage &aRaMessage) const
-{
-    // Determines whether or not a received RA message was prepared by
-    // by `RoutingManager` itself.
-
-    bool        isFromManager = false;
-    uint16_t    rioCount      = 0;
-    Ip6::Prefix prefix;
-
-    VerifyOrExit(aRaMessage.ContainsAnyOptions());
-
-    for (const Ip6::Nd::Option &option : aRaMessage)
-    {
-        switch (option.GetType())
-        {
-        case Ip6::Nd::Option::kTypePrefixInfo:
-        {
-            const Ip6::Nd::PrefixInfoOption &pio = static_cast<const Ip6::Nd::PrefixInfoOption &>(option);
-
-            VerifyOrExit(pio.IsValid());
-            pio.GetPrefix(prefix);
-
-            // If it is a non-deprecated PIO, it should match the
-            // local on-link prefix.
-
-            if (pio.GetPreferredLifetime() > 0)
-            {
-                VerifyOrExit(prefix == mOnLinkPrefixManager.GetLocalPrefix());
-            }
-
-            break;
-        }
-
-        case Ip6::Nd::Option::kTypeRouteInfo:
-        {
-            // RIO (with non-zero lifetime) should match entries from
-            // `mRioAdvertiser`. We keep track of the number of matched
-            // RIOs and check after the loop ends that all entries were
-            // seen.
-
-            const Ip6::Nd::RouteInfoOption &rio = static_cast<const Ip6::Nd::RouteInfoOption &>(option);
-
-            VerifyOrExit(rio.IsValid());
-            rio.GetPrefix(prefix);
-
-            if (rio.GetRouteLifetime() != 0)
-            {
-                VerifyOrExit(mRioAdvertiser.HasAdvertised(prefix));
-                rioCount++;
-            }
-
-            break;
-        }
-
-        default:
-            ExitNow();
-        }
+        SuccessOrExit(error = raMsg.AppendBytes(mExtraRaOptions.GetBytes(), mExtraRaOptions.GetLength()));
     }
 
-    VerifyOrExit(rioCount == mRioAdvertiser.GetAdvertisedRioCount());
+    VerifyOrExit(raMsg.ContainsAnyOptions());
 
-    isFromManager = true;
+    destAddress.SetToLinkLocalAllNodesMulticast();
+    raMsg.GetAsPacket(packet);
+
+    mRaInfo.IncrementTxCountAndSaveHash(packet);
+
+    SuccessOrExit(error = mInfraIf.Send(packet, destAddress));
+
+    mRaInfo.mLastTxTime = TimerMilli::GetNow();
+    Get<Ip6::Ip6>().GetBorderRoutingCounters().mRaTxSuccess++;
+    LogInfo("Sent RA on %s", mInfraIf.ToString().AsCString());
+    DumpDebg("[BR-CERT] direction=send | type=RA |", packet.GetBytes(), packet.GetLength());
 
 exit:
-    return isFromManager;
+    if (error != kErrorNone)
+    {
+        Get<Ip6::Ip6>().GetBorderRoutingCounters().mRaTxFailure++;
+        LogWarn("Failed to send RA on %s: %s", mInfraIf.ToString().AsCString(), ErrorToString(error));
+    }
 }
 
 bool RoutingManager::IsValidBrUlaPrefix(const Ip6::Prefix &aBrUlaPrefix)
@@ -726,7 +672,7 @@
     return (aPrefix.GetLength() == kOmrPrefixLength) && !aPrefix.IsLinkLocal() && !aPrefix.IsMulticast();
 }
 
-bool RoutingManager::IsValidOnLinkPrefix(const Ip6::Nd::PrefixInfoOption &aPio)
+bool RoutingManager::IsValidOnLinkPrefix(const PrefixInfoOption &aPio)
 {
     Ip6::Prefix prefix;
 
@@ -781,10 +727,10 @@
 
 void RoutingManager::HandleNeighborAdvertisement(const InfraIf::Icmp6Packet &aPacket)
 {
-    const Ip6::Nd::NeighborAdvertMessage *naMsg;
+    const NeighborAdvertMessage *naMsg;
 
     VerifyOrExit(aPacket.GetLength() >= sizeof(naMsg));
-    naMsg = reinterpret_cast<const Ip6::Nd::NeighborAdvertMessage *>(aPacket.GetBytes());
+    naMsg = reinterpret_cast<const NeighborAdvertMessage *>(aPacket.GetBytes());
 
     mDiscoveredPrefixTable.ProcessNeighborAdvertMessage(*naMsg);
 
@@ -794,7 +740,7 @@
 
 void RoutingManager::HandleRouterAdvertisement(const InfraIf::Icmp6Packet &aPacket, const Ip6::Address &aSrcAddress)
 {
-    Ip6::Nd::RouterAdvertMessage routerAdvMessage(aPacket);
+    RouterAdvert::RxMessage routerAdvMessage(aPacket);
 
     OT_ASSERT(mIsRunning);
 
@@ -818,7 +764,7 @@
     return;
 }
 
-bool RoutingManager::ShouldProcessPrefixInfoOption(const Ip6::Nd::PrefixInfoOption &aPio, const Ip6::Prefix &aPrefix)
+bool RoutingManager::ShouldProcessPrefixInfoOption(const PrefixInfoOption &aPio, const Ip6::Prefix &aPrefix)
 {
     // Indicate whether to process or skip a given prefix
     // from a PIO (from received RA message).
@@ -844,7 +790,7 @@
     return shouldProcess;
 }
 
-bool RoutingManager::ShouldProcessRouteInfoOption(const Ip6::Nd::RouteInfoOption &aRio, const Ip6::Prefix &aPrefix)
+bool RoutingManager::ShouldProcessRouteInfoOption(const RouteInfoOption &aRio, const Ip6::Prefix &aPrefix)
 {
     // Indicate whether to process or skip a given prefix
     // from a RIO (from received RA message).
@@ -943,18 +889,18 @@
     return contains;
 }
 
-void RoutingManager::UpdateRouterAdvertHeader(const Ip6::Nd::RouterAdvertMessage *aRouterAdvertMessage)
+void RoutingManager::UpdateRouterAdvertHeader(const RouterAdvert::RxMessage *aRouterAdvertMessage)
 {
     // Updates the `mRaInfo` from the given RA message.
 
-    Ip6::Nd::RouterAdvertMessage::Header oldHeader;
+    RouterAdvert::Header oldHeader;
 
     if (aRouterAdvertMessage != nullptr)
     {
         // We skip and do not update RA header if the received RA message
         // was not prepared and sent by `RoutingManager` itself.
 
-        VerifyOrExit(!IsReceivedRouterAdvertFromManager(*aRouterAdvertMessage));
+        VerifyOrExit(!mRaInfo.IsRaFromManager(*aRouterAdvertMessage));
     }
 
     oldHeader                 = mRaInfo.mHeader;
@@ -1057,8 +1003,8 @@
 {
 }
 
-void RoutingManager::DiscoveredPrefixTable::ProcessRouterAdvertMessage(const Ip6::Nd::RouterAdvertMessage &aRaMessage,
-                                                                       const Ip6::Address                 &aSrcAddress)
+void RoutingManager::DiscoveredPrefixTable::ProcessRouterAdvertMessage(const RouterAdvert::RxMessage &aRaMessage,
+                                                                       const Ip6::Address            &aSrcAddress)
 {
     // Process a received RA message and update the prefix table.
 
@@ -1088,20 +1034,20 @@
 
     ProcessRaHeader(aRaMessage.GetHeader(), *router);
 
-    for (const Ip6::Nd::Option &option : aRaMessage)
+    for (const Option &option : aRaMessage)
     {
         switch (option.GetType())
         {
-        case Ip6::Nd::Option::kTypePrefixInfo:
-            ProcessPrefixInfoOption(static_cast<const Ip6::Nd::PrefixInfoOption &>(option), *router);
+        case Option::kTypePrefixInfo:
+            ProcessPrefixInfoOption(static_cast<const PrefixInfoOption &>(option), *router);
             break;
 
-        case Ip6::Nd::Option::kTypeRouteInfo:
-            ProcessRouteInfoOption(static_cast<const Ip6::Nd::RouteInfoOption &>(option), *router);
+        case Option::kTypeRouteInfo:
+            ProcessRouteInfoOption(static_cast<const RouteInfoOption &>(option), *router);
             break;
 
-        case Ip6::Nd::Option::kTypeRaFlagsExtension:
-            ProcessRaFlagsExtOption(static_cast<const Ip6::Nd::RaFlagsExtOption &>(option), *router);
+        case Option::kTypeRaFlagsExtension:
+            ProcessRaFlagsExtOption(static_cast<const RaFlagsExtOption &>(option), *router);
             break;
 
         default:
@@ -1117,8 +1063,7 @@
     return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::ProcessRaHeader(const Ip6::Nd::RouterAdvertMessage::Header &aRaHeader,
-                                                            Router                                     &aRouter)
+void RoutingManager::DiscoveredPrefixTable::ProcessRaHeader(const RouterAdvert::Header &aRaHeader, Router &aRouter)
 {
     Entry      *entry;
     Ip6::Prefix prefix;
@@ -1160,8 +1105,7 @@
     return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::ProcessPrefixInfoOption(const Ip6::Nd::PrefixInfoOption &aPio,
-                                                                    Router                          &aRouter)
+void RoutingManager::DiscoveredPrefixTable::ProcessPrefixInfoOption(const PrefixInfoOption &aPio, Router &aRouter)
 {
     Ip6::Prefix prefix;
     Entry      *entry;
@@ -1206,8 +1150,7 @@
     return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::ProcessRouteInfoOption(const Ip6::Nd::RouteInfoOption &aRio,
-                                                                   Router                         &aRouter)
+void RoutingManager::DiscoveredPrefixTable::ProcessRouteInfoOption(const RouteInfoOption &aRio, Router &aRouter)
 {
     Ip6::Prefix prefix;
     Entry      *entry;
@@ -1249,8 +1192,8 @@
     return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::ProcessRaFlagsExtOption(const Ip6::Nd::RaFlagsExtOption &aRaFlagsOption,
-                                                                    Router                          &aRouter)
+void RoutingManager::DiscoveredPrefixTable::ProcessRaFlagsExtOption(const RaFlagsExtOption &aRaFlagsOption,
+                                                                    Router                 &aRouter)
 {
     VerifyOrExit(aRaFlagsOption.IsValid());
     aRouter.mStubRouterFlag = aRaFlagsOption.IsStubRouterFlagSet();
@@ -1560,8 +1503,7 @@
 
 void RoutingManager::DiscoveredPrefixTable::SignalTableChanged(void) { mSignalTask.Post(); }
 
-void RoutingManager::DiscoveredPrefixTable::ProcessNeighborAdvertMessage(
-    const Ip6::Nd::NeighborAdvertMessage &aNaMessage)
+void RoutingManager::DiscoveredPrefixTable::ProcessNeighborAdvertMessage(const NeighborAdvertMessage &aNaMessage)
 {
     Router *router;
 
@@ -1640,8 +1582,8 @@
 
 void RoutingManager::DiscoveredPrefixTable::SendNeighborSolicitToRouter(const Router &aRouter)
 {
-    InfraIf::Icmp6Packet            packet;
-    Ip6::Nd::NeighborSolicitMessage neighborSolicitMsg;
+    InfraIf::Icmp6Packet   packet;
+    NeighborSolicitMessage neighborSolicitMsg;
 
     VerifyOrExit(!Get<RoutingManager>().mRsSender.IsInProgress());
 
@@ -1657,10 +1599,10 @@
     return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::DetermineAndSetFlags(Ip6::Nd::RouterAdvertMessage &aRaMessage) const
+void RoutingManager::DiscoveredPrefixTable::DetermineAndSetFlags(RouterAdvert::Header &aHeader) const
 {
     // Determine the `M` and `O` flags to include in the RA message
-    // header `aRaMessage` to be emitted.
+    // header to be emitted.
     //
     // If any discovered router on infrastructure which is not itself a
     // stub router (e.g., another Thread BR) includes the `M` or `O`
@@ -1683,12 +1625,12 @@
 
         if (router.mManagedAddressConfigFlag)
         {
-            aRaMessage.GetHeader().SetManagedAddressConfigFlag();
+            aHeader.SetManagedAddressConfigFlag();
         }
 
         if (router.mOtherConfigFlag)
         {
-            aRaMessage.GetHeader().SetOtherConfigFlag();
+            aHeader.SetOtherConfigFlag();
         }
     }
 }
@@ -1775,7 +1717,7 @@
 //---------------------------------------------------------------------------------------------------------------------
 // DiscoveredPrefixTable::Entry
 
-void RoutingManager::DiscoveredPrefixTable::Entry::SetFrom(const Ip6::Nd::RouterAdvertMessage::Header &aRaHeader)
+void RoutingManager::DiscoveredPrefixTable::Entry::SetFrom(const RouterAdvert::Header &aRaHeader)
 {
     mPrefix.Clear();
     mType                    = kTypeRoute;
@@ -1784,7 +1726,7 @@
     mLastUpdateTime          = TimerMilli::GetNow();
 }
 
-void RoutingManager::DiscoveredPrefixTable::Entry::SetFrom(const Ip6::Nd::PrefixInfoOption &aPio)
+void RoutingManager::DiscoveredPrefixTable::Entry::SetFrom(const PrefixInfoOption &aPio)
 {
     aPio.GetPrefix(mPrefix);
     mType                      = kTypeOnLink;
@@ -1793,7 +1735,7 @@
     mLastUpdateTime            = TimerMilli::GetNow();
 }
 
-void RoutingManager::DiscoveredPrefixTable::Entry::SetFrom(const Ip6::Nd::RouteInfoOption &aRio)
+void RoutingManager::DiscoveredPrefixTable::Entry::SetFrom(const RouteInfoOption &aRio)
 {
     aRio.GetPrefix(mPrefix);
     mType                    = kTypeRoute;
@@ -1802,6 +1744,15 @@
     mLastUpdateTime          = TimerMilli::GetNow();
 }
 
+void RoutingManager::DiscoveredPrefixTable::Entry::SetFrom(const PrefixTableEntry &aPrefixTableEntry)
+{
+    mPrefix                    = AsCoreType(&aPrefixTableEntry.mPrefix);
+    mType                      = aPrefixTableEntry.mIsOnLink ? kTypeOnLink : kTypeRoute;
+    mValidLifetime             = aPrefixTableEntry.mValidLifetime;
+    mShared.mPreferredLifetime = aPrefixTableEntry.mPreferredLifetime;
+    mLastUpdateTime            = TimerMilli::GetNow();
+}
+
 bool RoutingManager::DiscoveredPrefixTable::Entry::operator==(const Entry &aOther) const
 {
     return (mType == aOther.mType) && (mPrefix == aOther.mPrefix);
@@ -1834,6 +1785,11 @@
     return mLastUpdateTime + TimeMilli::SecToMsec(delay);
 }
 
+TimeMilli RoutingManager::DiscoveredPrefixTable::Entry::GetStaleTimeFromPreferredLifetime(void) const
+{
+    return mLastUpdateTime + CalculateExpireDelay(GetPreferredLifetime());
+}
+
 bool RoutingManager::DiscoveredPrefixTable::Entry::IsDeprecated(void) const
 {
     OT_ASSERT(IsOnLinkPrefix());
@@ -2525,13 +2481,18 @@
     return (GetState() == kPublishing) || (GetState() == kAdvertising);
 }
 
-void RoutingManager::OnLinkPrefixManager::AppendAsPiosTo(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+Error RoutingManager::OnLinkPrefixManager::AppendAsPiosTo(RouterAdvert::TxMessage &aRaMessage)
 {
-    AppendCurPrefix(aRaMessage);
-    AppendOldPrefixes(aRaMessage);
+    Error error;
+
+    SuccessOrExit(error = AppendCurPrefix(aRaMessage));
+    error = AppendOldPrefixes(aRaMessage);
+
+exit:
+    return error;
 }
 
-void RoutingManager::OnLinkPrefixManager::AppendCurPrefix(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+Error RoutingManager::OnLinkPrefixManager::AppendCurPrefix(RouterAdvert::TxMessage &aRaMessage)
 {
     // Append the local on-link prefix to the `aRaMessage` as a PIO
     // only if it is being advertised or deprecated.
@@ -2540,6 +2501,7 @@
     // If in `kDeprecating` state, we include it as PIO with zero
     // preferred lifetime and the remaining valid lifetime.
 
+    Error     error             = kErrorNone;
     uint32_t  validLifetime     = kDefaultOnLinkPrefixLifetime;
     uint32_t  preferredLifetime = kDefaultOnLinkPrefixLifetime;
     TimeMilli now               = TimerMilli::GetNow();
@@ -2561,17 +2523,18 @@
         ExitNow();
     }
 
-    SuccessOrAssert(aRaMessage.AppendPrefixInfoOption(mLocalPrefix, validLifetime, preferredLifetime));
+    SuccessOrExit(error = aRaMessage.AppendPrefixInfoOption(mLocalPrefix, validLifetime, preferredLifetime));
 
     LogPrefixInfoOption(mLocalPrefix, validLifetime, preferredLifetime);
 
 exit:
-    return;
+    return error;
 }
 
-void RoutingManager::OnLinkPrefixManager::AppendOldPrefixes(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+Error RoutingManager::OnLinkPrefixManager::AppendOldPrefixes(RouterAdvert::TxMessage &aRaMessage)
 {
-    TimeMilli now = TimerMilli::GetNow();
+    Error     error = kErrorNone;
+    TimeMilli now   = TimerMilli::GetNow();
     uint32_t  validLifetime;
 
     for (const OldPrefix &oldPrefix : mOldLocalPrefixes)
@@ -2582,10 +2545,13 @@
         }
 
         validLifetime = TimeMilli::MsecToSec(oldPrefix.mExpireTime - now);
-        SuccessOrAssert(aRaMessage.AppendPrefixInfoOption(oldPrefix.mPrefix, validLifetime, 0));
+        SuccessOrExit(error = aRaMessage.AppendPrefixInfoOption(oldPrefix.mPrefix, validLifetime, 0));
 
         LogPrefixInfoOption(oldPrefix.mPrefix, validLifetime, 0);
     }
+
+exit:
+    return error;
 }
 
 void RoutingManager::OnLinkPrefixManager::HandleNetDataChange(void)
@@ -2821,11 +2787,13 @@
     return;
 }
 
-void RoutingManager::RioAdvertiser::InvalidatPrevRios(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+Error RoutingManager::RioAdvertiser::InvalidatPrevRios(RouterAdvert::TxMessage &aRaMessage)
 {
+    Error error = kErrorNone;
+
     for (const RioPrefix &prefix : mPrefixes)
     {
-        AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage);
+        SuccessOrExit(error = AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage));
     }
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE
@@ -2834,10 +2802,14 @@
 
     mPrefixes.Clear();
     mTimer.Stop();
+
+exit:
+    return error;
 }
 
-void RoutingManager::RioAdvertiser::AppendRios(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+Error RoutingManager::RioAdvertiser::AppendRios(RouterAdvert::TxMessage &aRaMessage)
 {
+    Error                           error    = kErrorNone;
     TimeMilli                       now      = TimerMilli::GetNow();
     TimeMilli                       nextTime = now.GetDistantFuture();
     RioPrefixArray                  oldPrefixes;
@@ -2925,7 +2897,7 @@
         {
             if (now >= prefix.mExpirationTime)
             {
-                AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage);
+                SuccessOrExit(error = AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage));
                 continue;
             }
         }
@@ -2938,7 +2910,7 @@
         if (mPrefixes.PushBack(prefix) != kErrorNone)
         {
             LogWarn("Too many deprecating on-mesh prefixes, removing %s", prefix.mPrefix.ToString().AsCString());
-            AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage);
+            SuccessOrExit(error = AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage));
         }
 
         nextTime = Min(nextTime, prefix.mExpirationTime);
@@ -2955,21 +2927,29 @@
             lifetime = TimeMilli::MsecToSec(prefix.mExpirationTime - now);
         }
 
-        AppendRio(prefix.mPrefix, lifetime, aRaMessage);
+        SuccessOrExit(error = AppendRio(prefix.mPrefix, lifetime, aRaMessage));
     }
 
     if (nextTime != now.GetDistantFuture())
     {
         mTimer.FireAtIfEarlier(nextTime);
     }
+
+exit:
+    return error;
 }
 
-void RoutingManager::RioAdvertiser::AppendRio(const Ip6::Prefix            &aPrefix,
-                                              uint32_t                      aRouteLifetime,
-                                              Ip6::Nd::RouterAdvertMessage &aRaMessage)
+Error RoutingManager::RioAdvertiser::AppendRio(const Ip6::Prefix       &aPrefix,
+                                               uint32_t                 aRouteLifetime,
+                                               RouterAdvert::TxMessage &aRaMessage)
 {
-    SuccessOrAssert(aRaMessage.AppendRouteInfoOption(aPrefix, aRouteLifetime, mPreference));
+    Error error;
+
+    SuccessOrExit(error = aRaMessage.AppendRouteInfoOption(aPrefix, aRouteLifetime, mPreference));
     LogRouteInfoOption(aPrefix, aRouteLifetime, mPreference);
+
+exit:
+    return error;
 }
 
 void RoutingManager::RioAdvertiser::HandleTimer(void)
@@ -3445,6 +3425,66 @@
 #endif // OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
 
 //---------------------------------------------------------------------------------------------------------------------
+// RaInfo
+
+void RoutingManager::RaInfo::IncrementTxCountAndSaveHash(const InfraIf::Icmp6Packet &aRaMessage)
+{
+    mTxCount++;
+    mLastHashIndex++;
+
+    if (mLastHashIndex == kNumHashEntries)
+    {
+        mLastHashIndex = 0;
+    }
+
+    CalculateHash(aRaMessage, mHashes[mLastHashIndex]);
+}
+
+bool RoutingManager::RaInfo::IsRaFromManager(const Ip6::Nd::RouterAdvert::RxMessage &aRaMessage) const
+{
+    // Determines whether or not a received RA message was prepared by
+    // by `RoutingManager` itself (is present in the saved `mHashes`).
+
+    bool     isFromManager = false;
+    uint16_t hashIndex     = mLastHashIndex;
+    uint32_t count         = Min<uint32_t>(mTxCount, kNumHashEntries);
+    Hash     hash;
+
+    CalculateHash(aRaMessage.GetAsPacket(), hash);
+
+    for (; count > 0; count--)
+    {
+        if (mHashes[hashIndex] == hash)
+        {
+            isFromManager = true;
+            break;
+        }
+
+        // Go to the previous index (ring buffer)
+
+        if (hashIndex == 0)
+        {
+            hashIndex = kNumHashEntries - 1;
+        }
+        else
+        {
+            hashIndex--;
+        }
+    }
+
+    return isFromManager;
+}
+
+void RoutingManager::RaInfo::CalculateHash(const InfraIf::Icmp6Packet &aRaMessage, Hash &aHash)
+{
+    Crypto::Sha256 sha256;
+
+    sha256.Start();
+    sha256.Update(aRaMessage.GetBytes(), aRaMessage.GetLength());
+    sha256.Finish(aHash);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
 // RsSender
 
 RoutingManager::RsSender::RsSender(Instance &aInstance)
@@ -3476,10 +3516,10 @@
 
 Error RoutingManager::RsSender::SendRs(void)
 {
-    Ip6::Address                  destAddress;
-    Ip6::Nd::RouterSolicitMessage routerSolicit;
-    InfraIf::Icmp6Packet          packet;
-    Error                         error;
+    Ip6::Address         destAddress;
+    RouterSolicitMessage routerSolicit;
+    InfraIf::Icmp6Packet packet;
+    Error                error;
 
     packet.InitFrom(routerSolicit);
     destAddress.SetToLinkLocalAllRoutersMulticast();
@@ -3602,6 +3642,8 @@
         break;
     }
 
+    mExternalCallback.InvokeIfSet(static_cast<otBorderRoutingDhcp6PdState>(newState));
+
 exit:
     return;
 }
@@ -3652,72 +3694,76 @@
 
 void RoutingManager::PdPrefixManager::ProcessPlatformGeneratedRa(const uint8_t *aRouterAdvert, const uint16_t aLength)
 {
-    Error                                     error = kErrorNone;
-    Ip6::Nd::RouterAdvertMessage::Icmp6Packet packet;
+    Error                     error = kErrorNone;
+    RouterAdvert::Icmp6Packet packet;
 
-    VerifyOrExit(IsRunning(), LogWarn("Ignore platform generated RA since PD is disabled or not running."));
-    packet.Init(aRouterAdvert, aLength);
-    error = Process(Ip6::Nd::RouterAdvertMessage(packet));
-    mNumPlatformRaReceived++;
-    mLastPlatformRaTime = TimerMilli::GetNow();
+    if (mEnabled)
+    {
+        packet.Init(aRouterAdvert, aLength);
+        RouterAdvert::RxMessage aMessage = RouterAdvert::RxMessage(packet);
 
-exit:
+        error = Process(&aMessage, nullptr);
+        mNumPlatformRaReceived++;
+        mLastPlatformRaTime = TimerMilli::GetNow();
+    }
+    else
+    {
+        LogWarn("Ignore platform generated RA since PD is disabled.");
+    }
+
     if (error != kErrorNone)
     {
         LogCrit("Failed to process platform generated ND OnMeshPrefix: %s", ErrorToString(error));
     }
 }
 
-Error RoutingManager::PdPrefixManager::Process(const Ip6::Nd::RouterAdvertMessage &aMessage)
+void RoutingManager::PdPrefixManager::ProcessDhcpPdPrefix(const PrefixTableEntry &aPrefixTableEntry)
 {
-    Error                        error = kErrorNone;
-    DiscoveredPrefixTable::Entry favoredEntry;
-    bool                         currentPrefixUpdated = false;
+    Error error = kErrorNone;
 
-    VerifyOrExit(aMessage.IsValid(), error = kErrorParse);
+    VerifyOrExit(mEnabled, LogWarn("Ignore DHCPv6 delegated prefix since PD is disabled."));
+
+    error = Process(nullptr, &aPrefixTableEntry);
+
+exit:
+
+    if (error != kErrorNone)
+    {
+        LogCrit("Failed to process DHCPv6 delegated prefix: %s", ErrorToString(error));
+    }
+}
+
+Error RoutingManager::PdPrefixManager::Process(const RouterAdvert::RxMessage *aMessage,
+                                               const PrefixTableEntry        *aPrefixTableEntry)
+{
+    bool                         currentPrefixUpdated = false;
+    Error                        error                = kErrorNone;
+    DiscoveredPrefixTable::Entry favoredEntry;
+    DiscoveredPrefixTable::Entry entry;
+
     favoredEntry.Clear();
 
-    for (const Ip6::Nd::Option &option : aMessage)
+    // aMessage or aPrefixTableEntry must be different from null
+    if (aMessage != nullptr)
     {
-        DiscoveredPrefixTable::Entry entry;
+        VerifyOrExit(aMessage->IsValid(), error = kErrorParse);
 
-        if (option.GetType() != Ip6::Nd::Option::Type::kTypePrefixInfo ||
-            !static_cast<const Ip6::Nd::PrefixInfoOption &>(option).IsValid())
+        for (const Option &option : *aMessage)
         {
-            continue;
-        }
-        mNumPlatformPioProcessed++;
-        entry.SetFrom(static_cast<const Ip6::Nd::PrefixInfoOption &>(option));
+            if (option.GetType() != Option::kTypePrefixInfo || !static_cast<const PrefixInfoOption &>(option).IsValid())
+            {
+                continue;
+            }
 
-        if (!IsValidPdPrefix(entry.GetPrefix()))
-        {
-            LogWarn("PdPrefixManager: Ignore invalid PIO entry %s", entry.GetPrefix().ToString().AsCString());
-            continue;
+            mNumPlatformPioProcessed++;
+            entry.SetFrom(static_cast<const PrefixInfoOption &>(option));
+            currentPrefixUpdated |= ProcessPrefixEntry(entry, favoredEntry);
         }
-
-        entry.mPrefix.Tidy();
-        entry.mPrefix.SetLength(kOmrPrefixLength);
-
-        // The platform may send another RA message to announce that the current prefix we are using is no longer
-        // preferred or no longer valid.
-        if (entry.GetPrefix() == GetPrefix())
-        {
-            currentPrefixUpdated = true;
-            mPrefix              = entry;
-        }
-
-        if (entry.IsDeprecated())
-        {
-            continue;
-        }
-
-        // Some platforms may delegate us more than one prefixes. We will pick the smallest one. This is a simple rule
-        // to pick the GUA prefix from the RA messages since GUA prefixes (2000::/3) are always smaller than ULA
-        // prefixes (fc00::/7).
-        if (favoredEntry.GetPrefix().GetLength() == 0 || entry.GetPrefix() < favoredEntry.GetPrefix())
-        {
-            favoredEntry = entry;
-        }
+    }
+    else // aPrefixTableEntry != nullptr
+    {
+        entry.SetFrom(*aPrefixTableEntry);
+        currentPrefixUpdated = ProcessPrefixEntry(entry, favoredEntry);
     }
 
     if (currentPrefixUpdated && mPrefix.IsDeprecated())
@@ -3736,7 +3782,15 @@
 exit:
     if (HasPrefix())
     {
-        mTimer.FireAt(mPrefix.GetStaleTime());
+        // If prefix has been set from aPrefixTableEntry use only preferred lifetime to calculate stale time
+        if (aPrefixTableEntry)
+        {
+            mTimer.FireAt(mPrefix.GetStaleTimeFromPreferredLifetime());
+        }
+        else
+        {
+            mTimer.FireAt(mPrefix.GetStaleTime());
+        }
     }
     else
     {
@@ -3746,6 +3800,41 @@
     return error;
 }
 
+bool RoutingManager::PdPrefixManager::ProcessPrefixEntry(DiscoveredPrefixTable::Entry &aEntry,
+                                                         DiscoveredPrefixTable::Entry &aFavoredEntry)
+{
+    bool currentPrefixUpdated = false;
+
+    if (!IsValidPdPrefix(aEntry.GetPrefix()))
+    {
+        LogWarn("PdPrefixManager: Ignore invalid prefix entry %s", aEntry.GetPrefix().ToString().AsCString());
+        ExitNow();
+    }
+
+    aEntry.mPrefix.SetLength(kOmrPrefixLength);
+    aEntry.mPrefix.Tidy();
+
+    // Check if there is an update to the current prefix. The valid or preferred lifetime might change.
+    if (aEntry.GetPrefix() == GetPrefix())
+    {
+        currentPrefixUpdated = true;
+        mPrefix              = aEntry;
+    }
+
+    VerifyOrExit(!aEntry.IsDeprecated());
+
+    // Some platforms may delegate us more than one prefix. We will pick the smallest one. This is a simple rule
+    // to pick the GUA prefix from the RA messages since GUA prefixes (2000::/3) are always smaller than ULA
+    // prefixes (fc00::/7).
+    if (aFavoredEntry.GetPrefix().GetLength() == 0 || aEntry.GetPrefix() < aFavoredEntry.GetPrefix())
+    {
+        aFavoredEntry = aEntry;
+    }
+
+exit:
+    return currentPrefixUpdated;
+}
+
 void RoutingManager::PdPrefixManager::SetEnabled(bool aEnabled)
 {
     Dhcp6PdState oldState = GetState();
@@ -3762,6 +3851,14 @@
 {
     AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().ProcessPlatformGeneratedRa(aMessage, aLength);
 }
+
+extern "C" void otPlatBorderRoutingProcessDhcp6PdPrefix(otInstance                            *aInstance,
+                                                        const otBorderRoutingPrefixTableEntry *aPrefixInfo)
+{
+    AssertPointerIsNotNull(aPrefixInfo);
+
+    AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().ProcessDhcpPdPrefix(*aPrefixInfo);
+}
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE
 
 } // namespace BorderRouter
diff --git a/src/core/border_router/routing_manager.hpp b/src/core/border_router/routing_manager.hpp
index 4aef01d..c522173 100644
--- a/src/core/border_router/routing_manager.hpp
+++ b/src/core/border_router/routing_manager.hpp
@@ -47,14 +47,17 @@
 #error "OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE is required for OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE."
 #endif
 
+#include <openthread/border_routing.h>
 #include <openthread/nat64.h>
 #include <openthread/netdata.h>
 
 #include "border_router/infra_if.hpp"
 #include "common/array.hpp"
+#include "common/callback.hpp"
 #include "common/error.hpp"
 #include "common/heap_allocatable.hpp"
 #include "common/heap_array.hpp"
+#include "common/heap_data.hpp"
 #include "common/linked_list.hpp"
 #include "common/locator.hpp"
 #include "common/message.hpp"
@@ -62,6 +65,7 @@
 #include "common/pool.hpp"
 #include "common/string.hpp"
 #include "common/timer.hpp"
+#include "crypto/sha256.hpp"
 #include "net/ip6.hpp"
 #include "net/nat64_translator.hpp"
 #include "net/nd6.hpp"
@@ -84,11 +88,12 @@
     friend class ot::Instance;
 
 public:
-    typedef NetworkData::RoutePreference       RoutePreference;     ///< Route preference (high, medium, low).
-    typedef otBorderRoutingPrefixTableIterator PrefixTableIterator; ///< Prefix Table Iterator.
-    typedef otBorderRoutingPrefixTableEntry    PrefixTableEntry;    ///< Prefix Table Entry.
-    typedef otBorderRoutingRouterEntry         RouterEntry;         ///< Router Entry.
-    typedef otPdProcessedRaInfo                PdProcessedRaInfo;   ///< Data of PdProcessedRaInfo.
+    typedef NetworkData::RoutePreference          RoutePreference;     ///< Route preference (high, medium, low).
+    typedef otBorderRoutingPrefixTableIterator    PrefixTableIterator; ///< Prefix Table Iterator.
+    typedef otBorderRoutingPrefixTableEntry       PrefixTableEntry;    ///< Prefix Table Entry.
+    typedef otBorderRoutingRouterEntry            RouterEntry;         ///< Router Entry.
+    typedef otPdProcessedRaInfo                   PdProcessedRaInfo;   ///< Data of PdProcessedRaInfo.
+    typedef otBorderRoutingRequestDhcp6PdCallback PdCallback;          ///< DHCPv6 PD callback.
 
     /**
      * This constant specifies the maximum number of route prefixes that may be published by `RoutingManager`
@@ -233,6 +238,22 @@
     void ClearRouteInfoOptionPreference(void) { mRioAdvertiser.ClearPreference(); }
 
     /**
+     * Sets additional options to append at the end of emitted Router Advertisement (RA) messages.
+     *
+     * The content of @p aOptions is copied internally, so can be a temporary stack variable.
+     *
+     * Subsequent calls to this method will overwrite the previously set value.
+     *
+     * @param[in] aOptions   A pointer to the encoded options. Can be `nullptr` to clear.
+     * @param[in] aLength    Number of bytes in @p aOptions.
+     *
+     * @retval kErrorNone     Successfully set the extra option bytes.
+     * @retval kErrorNoBufs   Could not allocate buffer to save the buffer.
+     *
+     */
+    Error SetExtraRouterAdvertOptions(const uint8_t *aOptions, uint16_t aLength);
+
+    /**
      * Gets the current preference used for published routes in Network Data.
      *
      * The preference is determined as follows:
@@ -523,6 +544,19 @@
     }
 
     /**
+     * Handles a prefix delegated from a DHCPv6 PD server. The prefix is received on the DHCPv6 PD client callback and
+     * then this method can be used to configure the prefix in the Routing Manager module.
+     *
+     * Note: This method is a part of DHCPv6 PD support on Thread border routers. For platforms where it doesn't make
+     * sense to generate a RA to set a DHCPv6 PD prefix this method can be used to set the prefix directly. The lifetime
+     * of the prefix can be updated by calling the function again with updated values.
+     *
+     * @param[in] aPrefixInfo  Prefix information structure received from the DHCPv6 PD server.
+     *
+     */
+    void ProcessDhcpPdPrefix(const PrefixTableEntry &aPrefixInfo) { mPdPrefixManager.ProcessDhcpPdPrefix(aPrefixInfo); }
+
+    /**
      * Enables / Disables the functions for DHCPv6 PD.
      *
      * @param[in] aEnabled  Whether to accept platform generated RA messages.
@@ -538,6 +572,21 @@
      *
      */
     Dhcp6PdState GetDhcp6PdState(void) const { return mPdPrefixManager.GetState(); }
+
+    /**
+     * Sets the callback whenever the state of a prefix request or release, via the DHCPv6 Prefix Delegation (PD),
+     * changes on the Thread interface.
+     *
+     * @param[in] aCallback  A pointer to a function that is called whenever the state of a prefix request or release
+     * changes.
+     * @param[in] aContext   A pointer to arbitrary context information.
+     *
+     */
+    void SetRequestDhcp6PdCallback(PdCallback aCallback, void *aContext)
+    {
+        mPdPrefixManager.SetRequestDhcp6PdCallback(aCallback, aContext);
+    }
+
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE
 
 private:
@@ -581,6 +630,15 @@
     static_assert(kPolicyEvaluationMaxDelay > kPolicyEvaluationMinDelay,
                   "kPolicyEvaluationMaxDelay must be larger than kPolicyEvaluationMinDelay");
 
+    using Option                 = Ip6::Nd::Option;
+    using PrefixInfoOption       = Ip6::Nd::PrefixInfoOption;
+    using RouteInfoOption        = Ip6::Nd::RouteInfoOption;
+    using RaFlagsExtOption       = Ip6::Nd::RaFlagsExtOption;
+    using RouterAdvert           = Ip6::Nd::RouterAdvert;
+    using NeighborAdvertMessage  = Ip6::Nd::NeighborAdvertMessage;
+    using NeighborSolicitMessage = Ip6::Nd::NeighborSolicitMessage;
+    using RouterSolicitMessage   = Ip6::Nd::RouterSolicitMessage;
+
     enum RouterAdvTxMode : uint8_t // Used in `SendRouterAdvertisement()`
     {
         kInvalidateAllPrevPrefixes,
@@ -630,9 +688,8 @@
     public:
         explicit DiscoveredPrefixTable(Instance &aInstance);
 
-        void ProcessRouterAdvertMessage(const Ip6::Nd::RouterAdvertMessage &aRaMessage,
-                                        const Ip6::Address                 &aSrcAddress);
-        void ProcessNeighborAdvertMessage(const Ip6::Nd::NeighborAdvertMessage &aNaMessage);
+        void ProcessRouterAdvertMessage(const RouterAdvert::RxMessage &aRaMessage, const Ip6::Address &aSrcAddress);
+        void ProcessNeighborAdvertMessage(const NeighborAdvertMessage &aNaMessage);
 
         bool ContainsDefaultOrNonUlaRoutePrefix(void) const;
         bool ContainsNonUlaOnLinkPrefix(void) const;
@@ -648,7 +705,7 @@
 
         TimeMilli CalculateNextStaleTime(TimeMilli aNow) const;
 
-        void DetermineAndSetFlags(Ip6::Nd::RouterAdvertMessage &aRaMessage) const;
+        void DetermineAndSetFlags(RouterAdvert::Header &aHeader) const;
 
         void  InitIterator(PrefixTableIterator &aIterator) const;
         Error GetNextEntry(PrefixTableIterator &aIterator, PrefixTableEntry &aEntry) const;
@@ -724,9 +781,10 @@
                 TimeMilli mNow;
             };
 
-            void               SetFrom(const Ip6::Nd::RouterAdvertMessage::Header &aRaHeader);
-            void               SetFrom(const Ip6::Nd::PrefixInfoOption &aPio);
-            void               SetFrom(const Ip6::Nd::RouteInfoOption &aRio);
+            void               SetFrom(const RouterAdvert::Header &aRaHeader);
+            void               SetFrom(const PrefixInfoOption &aPio);
+            void               SetFrom(const RouteInfoOption &aRio);
+            void               SetFrom(const PrefixTableEntry &aPrefixTableEntry);
             Type               GetType(void) const { return mType; }
             bool               IsOnLinkPrefix(void) const { return (mType == kTypeOnLink); }
             bool               IsRoutePrefix(void) const { return (mType == kTypeRoute); }
@@ -736,6 +794,7 @@
             void               ClearValidLifetime(void) { mValidLifetime = 0; }
             TimeMilli          GetExpireTime(void) const;
             TimeMilli          GetStaleTime(void) const;
+            TimeMilli          GetStaleTimeFromPreferredLifetime(void) const;
             RoutePreference    GetPreference(void) const;
             bool               operator==(const Entry &aOther) const;
             bool               Matches(const Matcher &aMatcher) const;
@@ -824,10 +883,10 @@
             void SetInitTime(void) { mData32 = TimerMilli::GetNow().GetValue(); }
         };
 
-        void         ProcessRaHeader(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);
-        void         ProcessRaFlagsExtOption(const Ip6::Nd::RaFlagsExtOption &aFlagsOption, Router &aRouter);
+        void         ProcessRaHeader(const RouterAdvert::Header &aRaHeader, Router &aRouter);
+        void         ProcessPrefixInfoOption(const PrefixInfoOption &aPio, Router &aRouter);
+        void         ProcessRouteInfoOption(const RouteInfoOption &aRio, Router &aRouter);
+        void         ProcessRaFlagsExtOption(const RaFlagsExtOption &aFlagsOption, Router &aRouter);
         bool         Contains(const Entry::Checker &aChecker) const;
         void         RemovePrefix(const Entry::Matcher &aMatcher);
         void         RemoveOrDeprecateEntriesFromInactiveRouters(void);
@@ -951,7 +1010,7 @@
         bool               IsInitalEvaluationDone(void) const;
         void               HandleDiscoveredPrefixTableChanged(void);
         bool               ShouldPublishUlaRoute(void) const;
-        void               AppendAsPiosTo(Ip6::Nd::RouterAdvertMessage &aRaMessage);
+        Error              AppendAsPiosTo(RouterAdvert::TxMessage &aRaMessage);
         bool               IsPublishingOrAdvertising(void) const;
         void               HandleNetDataChange(void);
         void               HandleExtPanIdChange(void);
@@ -980,8 +1039,8 @@
         void  PublishAndAdvertise(void);
         void  Deprecate(void);
         void  ResetExpireTime(TimeMilli aNow);
-        void  AppendCurPrefix(Ip6::Nd::RouterAdvertMessage &aRaMessage);
-        void  AppendOldPrefixes(Ip6::Nd::RouterAdvertMessage &aRaMessage);
+        Error AppendCurPrefix(RouterAdvert::TxMessage &aRaMessage);
+        Error AppendOldPrefixes(RouterAdvert::TxMessage &aRaMessage);
         void  DeprecateOldPrefix(const Ip6::Prefix &aPrefix, TimeMilli aExpireTime);
         void  SavePrefix(const Ip6::Prefix &aPrefix, TimeMilli aExpireTime);
 
@@ -1013,8 +1072,8 @@
         void            SetPreference(RoutePreference aPreference);
         void            ClearPreference(void);
         void            HandleRoleChanged(void);
-        void            AppendRios(Ip6::Nd::RouterAdvertMessage &aRaMessage);
-        void            InvalidatPrevRios(Ip6::Nd::RouterAdvertMessage &aRaMessage);
+        Error           AppendRios(RouterAdvert::TxMessage &aRaMessage);
+        Error           InvalidatPrevRios(RouterAdvert::TxMessage &aRaMessage);
         bool            HasAdvertised(const Ip6::Prefix &aPrefix) const { return mPrefixes.ContainsMatching(aPrefix); }
         uint16_t        GetAdvertisedRioCount(void) const { return mPrefixes.GetLength(); }
         void            HandleTimer(void);
@@ -1041,9 +1100,9 @@
             void Add(const Ip6::Prefix &aPrefix);
         };
 
-        void SetPreferenceBasedOnRole(void);
-        void UpdatePreference(RoutePreference aPreference);
-        void AppendRio(const Ip6::Prefix &aPrefix, uint32_t aRouteLifetime, Ip6::Nd::RouterAdvertMessage &aRaMessage);
+        void  SetPreferenceBasedOnRole(void);
+        void  UpdatePreference(RoutePreference aPreference);
+        Error AppendRio(const Ip6::Prefix &aPrefix, uint32_t aRouteLifetime, RouterAdvert::TxMessage &aRaMessage);
 
         using RioTimer = TimerMilliIn<RoutingManager, &RoutingManager::HandleRioAdvertiserimer>;
 
@@ -1152,26 +1211,44 @@
 
     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
+        // Tracks info about emitted RA messages:
+        //
+        // - Number of RAs sent
+        // - Last RA TX time
+        // - Hashes of last TX RAs (to tell if a received RA is from
+        //   `RoutingManager` itself)
+        // - RA header to use, and
+        // - Whether the RA 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.
 
+        typedef Crypto::Sha256::Hash Hash;
+
+        static constexpr uint16_t kNumHashEntries = 5;
+
         RaInfo(void)
             : mHeaderUpdateTime(TimerMilli::GetNow())
             , mIsHeaderFromHost(false)
             , mTxCount(0)
             , mLastTxTime(TimerMilli::GetNow() - kMinDelayBetweenRtrAdvs)
+            , mLastHashIndex(0)
         {
         }
 
-        Ip6::Nd::RouterAdvertMessage::Header mHeader;
-        TimeMilli                            mHeaderUpdateTime;
-        bool                                 mIsHeaderFromHost;
-        uint32_t                             mTxCount;
-        TimeMilli                            mLastTxTime;
+        void        IncrementTxCountAndSaveHash(const InfraIf::Icmp6Packet &aRaMessage);
+        bool        IsRaFromManager(const Ip6::Nd::RouterAdvert::RxMessage &aRaMessage) const;
+        static void CalculateHash(const InfraIf::Icmp6Packet &aRaMessage, Hash &aHash);
+
+        RouterAdvert::Header mHeader;
+        TimeMilli            mHeaderUpdateTime;
+        bool                 mIsHeaderFromHost;
+        uint32_t             mTxCount;
+        TimeMilli            mLastTxTime;
+        Hash                 mHashes[kNumHashEntries];
+        uint16_t             mLastHashIndex;
     };
 
     void HandleRsSenderTimer(void) { mRsSender.HandleTimer(); }
@@ -1232,9 +1309,14 @@
         Dhcp6PdState       GetState(void) const;
 
         void  ProcessPlatformGeneratedRa(const uint8_t *aRouterAdvert, uint16_t aLength);
+        void  ProcessDhcpPdPrefix(const PrefixTableEntry &aPrefixTableEntry);
         Error GetPrefixInfo(PrefixTableEntry &aInfo) const;
         Error GetProcessedRaInfo(PdProcessedRaInfo &aPdProcessedRaInfo) const;
         void  HandleTimer(void) { WithdrawPrefix(); }
+        void  SetRequestDhcp6PdCallback(PdCallback aCallback, void *aContext)
+        {
+            mExternalCallback.Set(aCallback, aContext);
+        }
 
         static const char *StateToString(Dhcp6PdState aState);
 
@@ -1246,21 +1328,25 @@
         }
 
     private:
-        Error Process(const Ip6::Nd::RouterAdvertMessage &aMessage);
+        Error Process(const RouterAdvert::RxMessage *aMessage, const PrefixTableEntry *aPrefixTableEntry);
+        bool  ProcessPrefixEntry(DiscoveredPrefixTable::Entry &aEntry, DiscoveredPrefixTable::Entry &aFavoredEntry);
         void  EvaluateStateChange(Dhcp6PdState aOldState);
         void  WithdrawPrefix(void);
         void  StartStop(bool aStart);
 
         using PlatformOmrPrefixTimer = TimerMilliIn<RoutingManager, &RoutingManager::HandlePdPrefixManagerTimer>;
+        using ExternalCallback       = Callback<PdCallback>;
 
         bool                         mEnabled;
         bool                         mIsRunning;
         uint32_t                     mNumPlatformPioProcessed;
         uint32_t                     mNumPlatformRaReceived;
         TimeMilli                    mLastPlatformRaTime;
+        ExternalCallback             mExternalCallback;
         PlatformOmrPrefixTimer       mTimer;
         DiscoveredPrefixTable::Entry mPrefix;
     };
+
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE
 
     void  EvaluateState(void);
@@ -1282,17 +1368,16 @@
     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);
+    bool ShouldProcessPrefixInfoOption(const PrefixInfoOption &aPio, const Ip6::Prefix &aPrefix);
+    bool ShouldProcessRouteInfoOption(const RouteInfoOption &aRio, const Ip6::Prefix &aPrefix);
     void UpdateDiscoveredPrefixTableOnNetDataChange(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 UpdateRouterAdvertHeader(const RouterAdvert::RxMessage *aRouterAdvertMessage);
     void ResetDiscoveredPrefixStaleTimer(void);
 
     static bool IsValidBrUlaPrefix(const Ip6::Prefix &aBrUlaPrefix);
-    static bool IsValidOnLinkPrefix(const Ip6::Nd::PrefixInfoOption &aPio);
+    static bool IsValidOnLinkPrefix(const PrefixInfoOption &aPio);
     static bool IsValidOnLinkPrefix(const Ip6::Prefix &aOnLinkPrefix);
 
     static void LogPrefixInfoOption(const Ip6::Prefix &aPrefix, uint32_t aValidLifetime, uint32_t aPreferredLifetime);
@@ -1334,8 +1419,9 @@
     PdPrefixManager mPdPrefixManager;
 #endif
 
-    RaInfo   mRaInfo;
-    RsSender mRsSender;
+    RaInfo     mRaInfo;
+    RsSender   mRsSender;
+    Heap::Data mExtraRaOptions;
 
     DiscoveredPrefixStaleTimer mDiscoveredPrefixStaleTimer;
     RoutingPolicyTimer         mRoutingPolicyTimer;
diff --git a/src/core/coap/coap.cpp b/src/core/coap/coap.cpp
index 696d0fc..6afd693 100644
--- a/src/core/coap/coap.cpp
+++ b/src/core/coap/coap.cpp
@@ -619,7 +619,6 @@
 {
     Error            error       = kErrorNone;
     bool             isOptionSet = false;
-    uint64_t         optionBuf   = 0;
     uint16_t         blockOption = 0;
     Option::Iterator iterator;
 
@@ -655,8 +654,9 @@
         }
 
         // Copy option
-        SuccessOrExit(error = iterator.ReadOptionValue(&optionBuf));
-        SuccessOrExit(error = aRequest.AppendOption(optionNumber, iterator.GetOption()->GetLength(), &optionBuf));
+        SuccessOrExit(error = aRequest.AppendOptionFromMessage(optionNumber, iterator.GetOption()->GetLength(),
+                                                               iterator.GetMessage(),
+                                                               iterator.GetOptionValueMessageOffset()));
     }
 
     if (!isOptionSet)
diff --git a/src/core/coap/coap_message.cpp b/src/core/coap/coap_message.cpp
index cbf5448..6743f20 100644
--- a/src/core/coap/coap_message.cpp
+++ b/src/core/coap/coap_message.cpp
@@ -97,7 +97,7 @@
 
     if (GetHelpData().mPayloadMarkerSet && (GetHelpData().mHeaderLength == GetLength()))
     {
-        IgnoreError(SetLength(GetLength() - 1));
+        RemoveFooter(sizeof(uint8_t));
     }
 
     WriteBytes(0, &GetHelpData().mHeader, GetOptionStart());
@@ -146,8 +146,12 @@
     return rval;
 }
 
-Error Message::AppendOption(uint16_t aNumber, uint16_t aLength, const void *aValue)
+Error Message::AppendOptionHeader(uint16_t aNumber, uint16_t aLength)
 {
+    /*
+     * Appends a CoAP Option header field (Option Delta/Length) per RFC 7252.
+     */
+
     Error    error = kErrorNone;
     uint16_t delta;
     uint8_t  header[kMaxOptionHeaderSize];
@@ -167,10 +171,33 @@
     VerifyOrExit(static_cast<uint32_t>(GetLength()) + headerLength + aLength < kMaxHeaderLength, error = kErrorNoBufs);
 
     SuccessOrExit(error = AppendBytes(header, headerLength));
-    SuccessOrExit(error = AppendBytes(aValue, aLength));
 
     GetHelpData().mOptionLast = aNumber;
 
+exit:
+    return error;
+}
+
+Error Message::AppendOption(uint16_t aNumber, uint16_t aLength, const void *aValue)
+{
+    Error error = kErrorNone;
+
+    SuccessOrExit(error = AppendOptionHeader(aNumber, aLength));
+    SuccessOrExit(error = AppendBytes(aValue, aLength));
+
+    GetHelpData().mHeaderLength = GetLength();
+
+exit:
+    return error;
+}
+
+Error Message::AppendOptionFromMessage(uint16_t aNumber, uint16_t aLength, const Message &aMessage, uint16_t aOffset)
+{
+    Error error = kErrorNone;
+
+    SuccessOrExit(error = AppendOptionHeader(aNumber, aLength));
+    SuccessOrExit(error = AppendBytesFromMessage(aMessage, aOffset, aLength));
+
     GetHelpData().mHeaderLength = GetLength();
 
 exit:
diff --git a/src/core/coap/coap_message.hpp b/src/core/coap/coap_message.hpp
index 789c919..62128b6 100644
--- a/src/core/coap/coap_message.hpp
+++ b/src/core/coap/coap_message.hpp
@@ -402,6 +402,23 @@
     Error AppendOption(uint16_t aNumber, uint16_t aLength, const void *aValue);
 
     /**
+     * Appends a CoAP option reading Option value from another or potentially the same message.
+     *
+     * @param[in] aNumber   The CoAP Option number.
+     * @param[in] aLength   The CoAP Option length.
+     * @param[in] aMessage  The message to read the CoAP Option value from (it can be the same as the current message).
+     * @param[in] aOffset   The offset in @p aMessage to start reading the CoAP Option value from (@p aLength bytes are
+     *                      used as Option value).
+     *
+     * @retval kErrorNone         Successfully appended the option.
+     * @retval kErrorInvalidArgs  The option type is not equal or greater than the last option type.
+     * @retval kErrorNoBufs       The option length exceeds the buffer size.
+     * @retval kErrorParse        Not enough bytes in @p aMessage to read @p aLength bytes from @p aOffset.
+     *
+     */
+    Error AppendOptionFromMessage(uint16_t aNumber, uint16_t aLength, const Message &aMessage, uint16_t aOffset);
+
+    /**
      * Appends an unsigned integer CoAP option as specified in RFC-7252 section-3.2
      *
      * @param[in]  aNumber  The CoAP Option number.
@@ -966,6 +983,8 @@
     }
 
     uint8_t WriteExtendedOptionField(uint16_t aValue, uint8_t *&aBuffer);
+
+    Error AppendOptionHeader(uint16_t aNumber, uint16_t aLength);
 };
 
 /**
@@ -1187,6 +1206,16 @@
          */
         uint16_t GetPayloadMessageOffset(void) const { return mNextOptionOffset; }
 
+        /**
+         * Gets the offset of beginning of the CoAP Option Value.
+         *
+         * MUST be used during the iterator is in progress.
+         *
+         * @returns The offset of beginning of the CoAP Option Value
+         *
+         */
+        uint16_t GetOptionValueMessageOffset(void) const { return mNextOptionOffset - mOption.mLength; }
+
     private:
         // `mOption.mLength` value to indicate iterator is done.
         static constexpr uint16_t kIteratorDoneLength = 0xffff;
diff --git a/src/core/common/log.cpp b/src/core/common/log.cpp
index 1809cd5..afddcfd 100644
--- a/src/core/common/log.cpp
+++ b/src/core/common/log.cpp
@@ -136,6 +136,16 @@
     return;
 }
 
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
+void Logger::LogOnError(const char *aModuleName, Error aError, const char *aText)
+{
+    if (aError != kErrorNone)
+    {
+        LogAtLevel<kLogLevelWarn>(aModuleName, "Failed to %s: %s", aText, ErrorToString(aError));
+    }
+}
+#endif
+
 #if OPENTHREAD_CONFIG_LOG_PKT_DUMP
 
 template <LogLevel kLogLevel>
diff --git a/src/core/common/log.hpp b/src/core/common/log.hpp
index 47293e0..49f0416 100644
--- a/src/core/common/log.hpp
+++ b/src/core/common/log.hpp
@@ -162,6 +162,22 @@
 #define LogDebg(...)
 #endif
 
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
+/**
+ * Emits an error log message at warning log level if there is an error.
+ *
+ * The emitted log will use the the following format "Failed to {aText}: {ErrorToString(aError)}", and will be emitted
+ * only if there is an error, i.e., @p aError is not `kErrorNone`.
+ *
+ * @param[in] aError       The error to check and log.
+ * @param[in] aText        The text to include in the log.
+ *
+ */
+#define LogWarnOnError(aError, aText) Logger::LogOnError(kLogModuleName, aError, aText)
+#else
+#define LogWarnOnError(aError, aText)
+#endif
+
 #if OT_SHOULD_LOG
 /**
  * Emits a log message at a given log level.
@@ -316,6 +332,10 @@
 
     static void LogVarArgs(const char *aModuleName, LogLevel aLogLevel, const char *aFormat, va_list aArgs);
 
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
+    static void LogOnError(const char *aModuleName, Error aError, const char *aText);
+#endif
+
 #if OPENTHREAD_CONFIG_LOG_PKT_DUMP
     static constexpr uint8_t kStringLineLength = 80;
     static constexpr uint8_t kDumpBytesPerLine = 16;
diff --git a/src/core/common/message.cpp b/src/core/common/message.cpp
index 50e1fcc..72dd7d6 100644
--- a/src/core/common/message.cpp
+++ b/src/core/common/message.cpp
@@ -547,6 +547,8 @@
     return error;
 }
 
+void Message::RemoveFooter(uint16_t aLength) { IgnoreError(SetLength(GetLength() - Min(aLength, GetLength()))); }
+
 void Message::GetFirstChunk(uint16_t aOffset, uint16_t &aLength, Chunk &aChunk) const
 {
     // This method gets the first message chunk (contiguous data
@@ -770,12 +772,19 @@
     SuccessOrExit(error = messageCopy->AppendBytesFromMessage(*this, 0, aLength));
 
     // Copy selected message information.
+
     offset = Min(GetOffset(), aLength);
     messageCopy->SetOffset(offset);
 
     messageCopy->SetSubType(GetSubType());
     messageCopy->SetLoopbackToHostAllowed(IsLoopbackToHostAllowed());
     messageCopy->SetOrigin(GetOrigin());
+    messageCopy->SetTimestamp(GetTimestamp());
+    messageCopy->SetMeshDest(GetMeshDest());
+    messageCopy->SetPanId(GetPanId());
+    messageCopy->SetChannel(GetChannel());
+    messageCopy->SetRssAverager(GetRssAverager());
+    messageCopy->SetLqiAverager(GetLqiAverager());
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     messageCopy->SetTimeSync(IsTimeSync());
 #endif
@@ -795,18 +804,48 @@
 bool Message::IsChildPending(void) const { return GetMetadata().mChildMask.HasAny(); }
 #endif
 
-void Message::SetLinkInfo(const ThreadLinkInfo &aLinkInfo)
+Error Message::GetLinkInfo(ThreadLinkInfo &aLinkInfo) const
 {
-    SetLinkSecurityEnabled(aLinkInfo.mLinkSecurity);
-    SetPanId(aLinkInfo.mPanId);
-    AddRss(aLinkInfo.mRss);
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    AddLqi(aLinkInfo.mLqi);
+    Error error = kErrorNone;
+
+    VerifyOrExit(IsOriginThreadNetif(), error = kErrorNotFound);
+
+    aLinkInfo.Clear();
+
+    aLinkInfo.mPanId               = GetPanId();
+    aLinkInfo.mChannel             = GetChannel();
+    aLinkInfo.mRss                 = GetAverageRss();
+    aLinkInfo.mLqi                 = GetAverageLqi();
+    aLinkInfo.mLinkSecurity        = IsLinkSecurityEnabled();
+    aLinkInfo.mIsDstPanIdBroadcast = IsDstPanIdBroadcast();
+
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    aLinkInfo.mTimeSyncSeq       = GetTimeSyncSeq();
+    aLinkInfo.mNetworkTimeOffset = GetNetworkTimeOffset();
 #endif
+
+#if OPENTHREAD_CONFIG_MULTI_RADIO
+    aLinkInfo.mRadioType = GetRadioType();
+#endif
+
+exit:
+    return error;
+}
+
+void Message::UpdateLinkInfoFrom(const ThreadLinkInfo &aLinkInfo)
+{
+    SetPanId(aLinkInfo.mPanId);
+    SetChannel(aLinkInfo.mChannel);
+    AddRss(aLinkInfo.mRss);
+    AddLqi(aLinkInfo.mLqi);
+    SetLinkSecurityEnabled(aLinkInfo.mLinkSecurity);
+    GetMetadata().mIsDstPanIdBroadcast = aLinkInfo.IsDstPanIdBroadcast();
+
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     SetTimeSyncSeq(aLinkInfo.mTimeSyncSeq);
     SetNetworkTimeOffset(aLinkInfo.mNetworkTimeOffset);
 #endif
+
 #if OPENTHREAD_CONFIG_MULTI_RADIO
     SetRadioType(static_cast<Mac::RadioType>(aLinkInfo.mRadioType));
 #endif
diff --git a/src/core/common/message.hpp b/src/core/common/message.hpp
index d48f719..739d237 100644
--- a/src/core/common/message.hpp
+++ b/src/core/common/message.hpp
@@ -200,9 +200,7 @@
         uint16_t     mPanId;       // PAN ID (used for MLE Discover Request and Response).
         uint8_t      mChannel;     // The message channel (used for MLE Announce).
         RssAverager  mRssAverager; // The averager maintaining the received signal strength (RSS) average.
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-        LqiAverager mLqiAverager; // The averager maintaining the Link quality indicator (LQI) average.
-#endif
+        LqiAverager  mLqiAverager; // The averager maintaining the Link quality indicator (LQI) average.
 #if OPENTHREAD_FTD
         ChildMask mChildMask; // ChildMask to indicate which sleepy children need to receive this.
 #endif
@@ -218,7 +216,9 @@
         bool    mMulticastLoop : 1;       // Whether this multicast message may be looped back.
         bool    mResolvingAddress : 1;    // Whether the message is pending an address query resolution.
         bool    mAllowLookbackToHost : 1; // Whether the message is allowed to be looped back to host.
-        uint8_t mOrigin : 2;              // The origin of the message.
+        bool    mIsDstPanIdBroadcast : 1; // IWhether the dest PAN ID is broadcast.
+        uint8_t mOrigin : 2;
+        // The origin of the message.
 #if OPENTHREAD_CONFIG_MULTI_RADIO
         uint8_t mRadioType : 2;      // The radio link type the message was received on, or should be sent on.
         bool    mIsRadioTypeSet : 1; // Whether the radio type is set.
@@ -667,6 +667,17 @@
     Error InsertHeader(uint16_t aOffset, uint16_t aLength);
 
     /**
+     * Removes footer bytes from the end of the message.
+     *
+     * The caller should ensure the message contains the bytes to be removed, otherwise as many bytes as available
+     * will be removed.
+     *
+     * @param[in] aLength   Number of footer bytes to remove from end of the `Message`.
+     *
+     */
+    void RemoveFooter(uint16_t aLength);
+
+    /**
      * Appends bytes to the end of the message.
      *
      * On success, this method grows the message by @p aLength bytes.
@@ -1015,11 +1026,15 @@
     void SetMeshDest(uint16_t aMeshDest) { GetMetadata().mMeshDest = aMeshDest; }
 
     /**
-     * Returns the IEEE 802.15.4 Destination PAN ID.
+     * Returns the IEEE 802.15.4 Source or Destination PAN ID.
      *
-     * @note Only use this when sending MLE Discover Request or Response messages.
+     * For a message received over the Thread radio, specifies the Source PAN ID when present in MAC header, otherwise
+     * specifies the Destination PAN ID.
      *
-     * @returns The IEEE 802.15.4 Destination PAN ID.
+     * For a message to be sent over the Thread radio, this is set and used for MLE Discover Request or Response
+     * messages.
+     *
+     * @returns The IEEE 802.15.4 PAN ID.
      *
      */
     uint16_t GetPanId(void) const { return GetMetadata().mPanId; }
@@ -1035,6 +1050,17 @@
     void SetPanId(uint16_t aPanId) { GetMetadata().mPanId = aPanId; }
 
     /**
+     * Indicates whether the Destination PAN ID is broadcast.
+     *
+     * This is applicable for messages received over Thread radio.
+     *
+     * @retval TRUE   The Destination PAN ID is broadcast.
+     * @retval FALSE  The Destination PAN ID is not broadcast.
+     *
+     */
+    bool IsDstPanIdBroadcast(void) const { return GetMetadata().mIsDstPanIdBroadcast; }
+
+    /**
      * Returns the IEEE 802.15.4 Channel to use for transmission.
      *
      * @note Only use this when sending MLE Announce messages.
@@ -1255,7 +1281,6 @@
      */
     const RssAverager &GetRssAverager(void) const { return GetMetadata().mRssAverager; }
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     /**
      * Updates the average LQI (Link Quality Indicator) associated with the message.
      *
@@ -1282,7 +1307,25 @@
      *
      */
     uint8_t GetPsduCount(void) const { return GetMetadata().mLqiAverager.GetCount(); }
-#endif
+
+    /**
+     * Returns a const reference to LqiAverager of the message.
+     *
+     * @returns A const reference to the LqiAverager of the message.
+     *
+     */
+    const LqiAverager &GetLqiAverager(void) const { return GetMetadata().mLqiAverager; }
+
+    /**
+     * Retrieves `ThreadLinkInfo` from the message if received over Thread radio with origin `kOriginThreadNetif`.
+     *
+     * @pram[out] aLinkInfo     A reference to a `ThreadLinkInfo` to populate.
+     *
+     * @retval kErrorNone       Successfully retrieved the link info, @p `aLinkInfo` is updated.
+     * @retval kErrorNotFound   Message origin is not `kOriginThreadNetif`.
+     *
+     */
+    Error GetLinkInfo(ThreadLinkInfo &aLinkInfo) const;
 
     /**
      * Sets the message's link info properties (PAN ID, link security, RSS) from a given `ThreadLinkInfo`.
@@ -1290,7 +1333,7 @@
      * @param[in] aLinkInfo   The `ThreadLinkInfo` instance from which to set message's related properties.
      *
      */
-    void SetLinkInfo(const ThreadLinkInfo &aLinkInfo);
+    void UpdateLinkInfoFrom(const ThreadLinkInfo &aLinkInfo);
 
     /**
      * Returns a pointer to the message queue (if any) where this message is queued.
@@ -1489,6 +1532,9 @@
     void SetMessageQueue(MessageQueue *aMessageQueue);
     void SetPriorityQueue(PriorityQueue *aPriorityQueue);
 
+    void SetRssAverager(const RssAverager &aRssAverager) { GetMetadata().mRssAverager = aRssAverager; }
+    void SetLqiAverager(const LqiAverager &aLqiAverager) { GetMetadata().mLqiAverager = aLqiAverager; }
+
     Message       *&Next(void) { return GetMetadata().mNext; }
     Message *const &Next(void) const { return GetMetadata().mNext; }
     Message       *&Prev(void) { return GetMetadata().mPrev; }
diff --git a/src/core/common/string.cpp b/src/core/common/string.cpp
index 4019628..c759110 100644
--- a/src/core/common/string.cpp
+++ b/src/core/common/string.cpp
@@ -89,13 +89,16 @@
 
 uint16_t StringLength(const char *aString, uint16_t aMaxLength)
 {
-    uint16_t ret;
+    uint16_t ret = 0;
 
-    for (ret = 0; (ret < aMaxLength) && (aString[ret] != kNullChar); ret++)
+    VerifyOrExit(aString != nullptr);
+
+    for (; (ret < aMaxLength) && (aString[ret] != kNullChar); ret++)
     {
         // Empty loop.
     }
 
+exit:
     return ret;
 }
 
diff --git a/src/core/common/string.hpp b/src/core/common/string.hpp
index 0c73bb6..358d7f8 100644
--- a/src/core/common/string.hpp
+++ b/src/core/common/string.hpp
@@ -85,8 +85,8 @@
  * @param[in] aString      A pointer to the string.
  * @param[in] aMaxLength   The maximum length in bytes.
  *
- * @returns The number of characters that precede the terminating null character or @p aMaxLength, whichever is
- *          smaller.
+ * @returns The number of characters that precede the terminating null character or @p aMaxLength,
+ *          whichever is smaller. `0` if @p aString is `nullptr`.
  *
  */
 uint16_t StringLength(const char *aString, uint16_t aMaxLength);
diff --git a/src/core/common/tasklet.hpp b/src/core/common/tasklet.hpp
index 48a3f07..5158bdd 100644
--- a/src/core/common/tasklet.hpp
+++ b/src/core/common/tasklet.hpp
@@ -45,8 +45,6 @@
 
 namespace ot {
 
-class TaskletScheduler;
-
 /**
  * @addtogroup core-tasklet
  *
diff --git a/src/core/common/tlvs.hpp b/src/core/common/tlvs.hpp
index fd2d679..1acc806 100644
--- a/src/core/common/tlvs.hpp
+++ b/src/core/common/tlvs.hpp
@@ -530,6 +530,22 @@
      *
      * On success this method grows the message by the size of the TLV.
      *
+     * @param[in]  aMessage      The message to append to.
+     * @param[in]  aType         The TLV type to append.
+     * @param[in]  aValue        A buffer containing the TLV value.
+     * @param[in]  aLength       The value length (in bytes).
+     *
+     * @retval kErrorNone     Successfully appended the TLV to the message.
+     * @retval kErrorNoBufs   Insufficient available buffers to grow the message.
+     *
+     */
+    static Error AppendTlv(Message &aMessage, uint8_t aType, const void *aValue, uint8_t aLength);
+
+    /**
+     * 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.
+     *
      * @tparam     TlvType       The TLV type to append.
      *
      * @param[in]  aMessage      A reference to the message to append to.
@@ -687,7 +703,6 @@
     };
 
     static Error FindTlv(const Message &aMessage, uint8_t aType, void *aValue, uint16_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);
diff --git a/src/core/config/border_agent.h b/src/core/config/border_agent.h
index 9be5db1..ab3925e 100644
--- a/src/core/config/border_agent.h
+++ b/src/core/config/border_agent.h
@@ -76,6 +76,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+ *
+ * Define to 1 to enable ephemeral key mechanism and its APIs in Border Agent.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+#define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_4)
+#endif
+
+/**
  * @}
  *
  */
diff --git a/src/core/config/channel_manager.h b/src/core/config/channel_manager.h
index 7c481c2..4ecb3dc 100644
--- a/src/core/config/channel_manager.h
+++ b/src/core/config/channel_manager.h
@@ -56,6 +56,18 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE
+ *
+ * Define as 1 to enable Channel Manager support for selecting CSL channels.
+ *
+ * `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE` must be enabled in addition.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE
+#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE 0
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_DELAY
  *
  * The minimum delay (in seconds) used by Channel Manager module for performing a channel change.
diff --git a/src/core/config/ip6.h b/src/core/config/ip6.h
index 2e8e982..f16f8a3 100644
--- a/src/core/config/ip6.h
+++ b/src/core/config/ip6.h
@@ -228,6 +228,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_IP6_RESTRICT_FORWARDING_LARGER_SCOPE_MCAST_WITH_LOCAL_SRC
+ *
+ * Define as 1 to restrict multicast forwarding to larger scope from local sources.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_IP6_RESTRICT_FORWARDING_LARGER_SCOPE_MCAST_WITH_LOCAL_SRC
+#define OPENTHREAD_CONFIG_IP6_RESTRICT_FORWARDING_LARGER_SCOPE_MCAST_WITH_LOCAL_SRC 0
+#endif
+
+/**
  * @}
  *
  */
diff --git a/src/core/config/mdns.h b/src/core/config/mdns.h
new file mode 100644
index 0000000..addd14b
--- /dev/null
+++ b/src/core/config/mdns.h
@@ -0,0 +1,111 @@
+/*
+ *  Copyright (c) 2024, 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 Multicast DNS (mDNS).
+ *
+ */
+
+#ifndef CONFIG_MULTICAST_DNS_H_
+#define CONFIG_MULTICAST_DNS_H_
+
+/**
+ * @addtogroup config-mdns
+ *
+ * @brief
+ *   This module includes configuration variables for the Multicast DNS (mDNS).
+ *
+ * @{
+ *
+ */
+
+/**
+ * @def OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+ *
+ * Define to 1 to enable Multicast DNS (mDNS) support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+ *
+ * Define to 1 to allow public OpenThread APIs to be defined for Multicast DNS (mDNS) module.
+ *
+ * The OpenThread mDNS module is mainly intended for use by other OT core modules, so the public APIs are by default
+ * not provided.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF
+ *
+ * Define to 1 for mDNS module to be automatically enabled/disabled on the same infra-if used for border routing
+ * based on infra-if state.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_MULTICAST_DNS_DEFAULT_QUESTION_UNICAST_ALLOWED
+ *
+ * Specified the default value for `otMdnsIsQuestionUnicastAllowed()` which indicates whether mDNS core is allowed to
+ * send "QU" questions (questions requesting unicast response). When allowed, the first probe will be sent as "QU"
+ * question. The `otMdnsSetQuestionUnicastAllowed()` can be used to change the default value at run-time.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MULTICAST_DNS_DEFAULT_QUESTION_UNICAST_ALLOWED
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_DEFAULT_QUESTION_UNICAST_ALLOWED 1
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE
+ *
+ * Define to 1 to add mock (empty) implementation of mDNS platform APIs.
+ *
+ * This is intended for generating code size report only and should not be used otherwise.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE 0
+#endif
+
+/**
+ * @}
+ *
+ */
+
+#endif // CONFIG_MULTICAST_DNS_H_
diff --git a/src/core/config/misc.h b/src/core/config/misc.h
index b04e32e..e764b88 100644
--- a/src/core/config/misc.h
+++ b/src/core/config/misc.h
@@ -142,6 +142,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+ *
+ * Define to 1 to enable Verhoeff checksum utility module.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+#define OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
  *
  * Define to 1 to enable multiple instance support.
@@ -424,9 +434,14 @@
 /**
  * @def OPENTHREAD_CONFIG_DEFAULT_SED_BUFFER_SIZE
  *
- * This setting configures the default buffer size for IPv6 datagram destined for an attached SED.
- * A Thread Router MUST be able to buffer at least one 1280-octet IPv6 datagram for an attached SED according to
- * the Thread Conformance Specification.
+ * Specifies the value used in emitted Connectivity TLV "Rx-off Child Buffer Size" field which indicates the
+ * guaranteed buffer capacity for all IPv6 datagrams destined to a given rx-off-when-idle child.
+ *
+ * Changing this config does not automatically adjust message buffers. Vendors should ensure their device can support
+ * the specified value based on the message buffer model used:
+ *  - OT internal message pool (refer to `OPENTHREAD_CONFIG_NUM_MESSAGE_BUFFERS` and `MESSAGE_BUFFER_SIZE`), or
+ *  - Heap allocated message buffers (refer to `OPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE),
+ *  - Platform-specific message management (refer to`OPENTHREAD_CONFIG_PLATFORM_MESSAGE_MANAGEMENT`).
  *
  */
 #ifndef OPENTHREAD_CONFIG_DEFAULT_SED_BUFFER_SIZE
@@ -436,9 +451,11 @@
 /**
  * @def OPENTHREAD_CONFIG_DEFAULT_SED_DATAGRAM_COUNT
  *
- * This setting configures the default datagram count of 106-octet IPv6 datagram per attached SED.
- * A Thread Router MUST be able to buffer at least one 106-octet IPv6 datagram per attached SED according to
- * the Thread Conformance Specification.
+ * Specifies the value used in emitted Connectivity TLV "Rx-off Child Datagram Count" field which indicates the
+ * guaranteed queue capacity in number of IPv6 datagrams destined to a given rx-off-when-idle child.
+ *
+ * Similar to `OPENTHREAD_CONFIG_DEFAULT_SED_BUFFER_SIZE`, vendors should ensure their device can support the specified
+ * value based on the message buffer model used.
  *
  */
 #ifndef OPENTHREAD_CONFIG_DEFAULT_SED_DATAGRAM_COUNT
diff --git a/src/core/config/mle.h b/src/core/config/mle.h
index 0810c01..d09a3cc 100644
--- a/src/core/config/mle.h
+++ b/src/core/config/mle.h
@@ -104,12 +104,12 @@
  * Define as 1 to enable feature to set device properties which are used for calculating the local leader weight on a
  * device.
  *
- * It is enabled by default on Thread Version 1.3.1 or later.
+ * It is enabled by default on Thread Version 1.4 or later.
  *
  */
 #ifndef OPENTHREAD_CONFIG_MLE_DEVICE_PROPERTY_LEADER_WEIGHT_ENABLE
 #define OPENTHREAD_CONFIG_MLE_DEVICE_PROPERTY_LEADER_WEIGHT_ENABLE \
-    (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_3_1)
+    (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_4)
 #endif
 
 /**
diff --git a/src/core/config/network_diagnostic.h b/src/core/config/network_diagnostic.h
index ccf128d..6f670ef 100644
--- a/src/core/config/network_diagnostic.h
+++ b/src/core/config/network_diagnostic.h
@@ -76,6 +76,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_APP_URL
+ *
+ * Specifies the default Vendor App URL string.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_APP_URL
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_APP_URL ""
+#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.
diff --git a/src/core/config/platform.h b/src/core/config/platform.h
index a3ebf23..bfc00a4 100644
--- a/src/core/config/platform.h
+++ b/src/core/config/platform.h
@@ -116,7 +116,21 @@
  *
  */
 #ifndef OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
-#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE
+#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+ *
+ * Define as 1 to enable run-time selection of DNSSD module, i.e., whether the native OpenThread mDNS module is used or
+ * the platform `otPlatDnssd` APIs are used (DNSSD support is delegated to the platform layer).
+ *
+ * This config is mainly intended for testing, allowing test-specific `otPlatDnssd` APIs to be used instead of the
+ * native mDNS module in unit tests.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION 0
 #endif
 
 /**
diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp
index 4735ce3..9022096 100644
--- a/src/core/instance/instance.cpp
+++ b/src/core/instance/instance.cpp
@@ -87,7 +87,7 @@
     , mSettings(*this)
     , mSettingsDriver(*this)
     , mMessagePool(*this)
-#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     // DNS-SD (mDNS) platform is initialized early to
     // allow other modules to use it.
     , mDnssd(*this)
@@ -122,6 +122,9 @@
 #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE
     , mDnsDso(*this)
 #endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    , mMdnsCore(*this)
+#endif
 #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE
     , mSntpClient(*this)
 #endif
@@ -230,7 +233,9 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
     , mChannelMonitor(*this)
 #endif
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
     , mChannelManager(*this)
 #endif
 #if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
@@ -329,6 +334,19 @@
     return instance;
 }
 
+Instance &Instance::Get(uint8_t aIdx)
+{
+    void *instance = gMultiInstanceRaw + aIdx * INSTANCE_SIZE_ALIGNED;
+    return *static_cast<Instance *>(instance);
+}
+
+uint8_t Instance::GetIdx(Instance *aInstance)
+{
+    return static_cast<uint8_t>(
+        (reinterpret_cast<uint8_t *>(aInstance) - reinterpret_cast<uint8_t *>(gMultiInstanceRaw)) /
+        INSTANCE_SIZE_ALIGNED);
+}
+
 #endif // #if OPENTHREAD_CONFIG_MULTIPLE_STATIC_INSTANCE_ENABLE
 
 Instance *Instance::Init(void *aBuffer, size_t *aBufferSize)
diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp
index 1ebc01c..2b462b4 100644
--- a/src/core/instance/instance.hpp
+++ b/src/core/instance/instance.hpp
@@ -92,6 +92,7 @@
 #include "net/dnssd_server.hpp"
 #include "net/ip6.hpp"
 #include "net/ip6_filter.hpp"
+#include "net/mdns.hpp"
 #include "net/nat64_translator.hpp"
 #include "net/nd_agent.hpp"
 #include "net/netif.hpp"
@@ -202,6 +203,26 @@
      *
      */
     static Instance *InitMultiple(uint8_t aIdx);
+
+    /**
+     * Returns a reference to the OpenThread instance.
+     *
+     * @param[in] aIdx The index of the OpenThread instance to get.
+     *
+     * @returns A reference to the OpenThread instance.
+     *
+     */
+    static Instance &Get(uint8_t aIdx);
+
+    /**
+     * Returns the index of the OpenThread instance.
+     *
+     * @param[in] aInstance The reference of the OpenThread instance to get index.
+     *
+     * @returns The index of the OpenThread instance.
+     *
+     */
+    static uint8_t GetIdx(Instance *aInstance);
 #endif
 
 #else // OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
@@ -460,7 +481,7 @@
     SettingsDriver mSettingsDriver;
     MessagePool    mMessagePool;
 
-#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     // DNS-SD (mDNS) platform is initialized early to
     // allow other modules to use it.
     Dnssd mDnssd;
@@ -506,6 +527,10 @@
     Dns::Dso mDnsDso;
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    Dns::Multicast::Core mMdnsCore;
+#endif
+
 #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE
     Sntp::Client mSntpClient;
 #endif
@@ -645,7 +670,9 @@
     Utils::ChannelMonitor mChannelMonitor;
 #endif
 
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
     Utils::ChannelManager mChannelManager;
 #endif
 
@@ -875,7 +902,7 @@
 template <> inline PanIdQueryClient &Instance::Get(void) { return mCommissioner.GetPanIdQueryClient(); }
 #endif
 
-#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
 template <> inline Dnssd &Instance::Get(void) { return mDnssd; }
 #endif
 
@@ -903,6 +930,10 @@
 template <> inline Dns::Dso &Instance::Get(void) { return mDnsDso; }
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+template <> inline Dns::Multicast::Core &Instance::Get(void) { return mMdnsCore; }
+#endif
+
 template <> inline NetworkDiagnostic::Server &Instance::Get(void) { return mNetworkDiagnosticServer; }
 
 #if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
@@ -946,7 +977,9 @@
 template <> inline Utils::ChannelMonitor &Instance::Get(void) { return mChannelMonitor; }
 #endif
 
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
 template <> inline Utils::ChannelManager &Instance::Get(void) { return mChannelManager; }
 #endif
 
diff --git a/src/core/mac/mac.cpp b/src/core/mac/mac.cpp
index 243db1d..e70fc98 100644
--- a/src/core/mac/mac.cpp
+++ b/src/core/mac/mac.cpp
@@ -1134,9 +1134,13 @@
     }
 
     // Only track the CCA success rate for frame transmissions
-    // on the PAN channel.
+    // on the PAN channel or the CSL channel.
 
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+    if ((aChannel == mPanChannel) || (IsCslEnabled() && (aChannel == mCslChannel)))
+#else
     if (aChannel == mPanChannel)
+#endif
     {
         if (mCcaSampleCount < kMaxCcaSampleCount)
         {
@@ -1637,7 +1641,7 @@
 
         if (keySequence > keyManager.GetCurrentKeySequence())
         {
-            keyManager.SetCurrentKeySequence(keySequence);
+            keyManager.SetCurrentKeySequence(keySequence, KeyManager::kApplyKeySwitchGuard);
         }
     }
 
diff --git a/src/core/mac/sub_mac.cpp b/src/core/mac/sub_mac.cpp
index 2884d86..edeba2e 100644
--- a/src/core/mac/sub_mac.cpp
+++ b/src/core/mac/sub_mac.cpp
@@ -333,8 +333,8 @@
     //   be due to clocks drift and/or CSL Phase rounding error.
     // This means that a deviation absolute value greater than the margin would result in the frame
     // not being received out of the debug mode.
-    logString.Append("Expected sample time %lu, margin ±%lu, deviation %d", ToUlong(sampleTime), ToUlong(ahead),
-                     deviation);
+    logString.Append("Expected sample time %lu, margin ±%lu, deviation %ld", ToUlong(sampleTime), ToUlong(ahead),
+                     static_cast<long>(deviation));
 
     // Treat as a warning when the deviation is not within the margins. Neither kCslReceiveTimeAhead
     // or kMinReceiveOnAhead/kMinReceiveOnAfter are considered for the margin since they have no
diff --git a/src/core/meshcop/border_agent.cpp b/src/core/meshcop/border_agent.cpp
index 7678942..891511b 100644
--- a/src/core/meshcop/border_agent.cpp
+++ b/src/core/meshcop/border_agent.cpp
@@ -125,7 +125,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send error CoAP message", error);
+    LogWarnOnError(error, "send error CoAP message");
 }
 
 void BorderAgent::SendErrorMessage(const Coap::Message &aRequest, bool aSeparate, Error aError)
@@ -155,7 +155,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send error CoAP message", error);
+    LogWarnOnError(error, "send error CoAP message");
 }
 
 Error BorderAgent::SendMessage(Coap::Message &aMessage)
@@ -236,6 +236,12 @@
 #if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
     , mIdInitialized(false)
 #endif
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    , mUsingEphemeralKey(false)
+    , mOldUdpPort(0)
+    , mEphemeralKeyTimer(aInstance)
+    , mEphemeralKeyTask(aInstance)
+#endif
 {
     mCommissionerAloc.InitAsThreadOriginMeshLocal();
 }
@@ -338,7 +344,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send proxy stream", error);
+    LogWarnOnError(error, "send proxy stream");
 }
 
 bool BorderAgent::HandleUdpReceive(void *aContext, const otMessage *aMessage, const otMessageInfo *aMessageInfo)
@@ -389,7 +395,7 @@
     FreeMessageOnError(message, error);
     if (error != kErrorDestinationAddressFiltered)
     {
-        LogError("notify commissioner on ProxyRx (c/ur)", error);
+        LogWarnOnError(error, "notify commissioner on ProxyRx (c/ur)");
     }
 
     return error != kErrorDestinationAddressFiltered;
@@ -427,7 +433,7 @@
     LogInfo("Sent to commissioner");
 
 exit:
-    LogError("send to commissioner", error);
+    LogWarnOnError(error, "send to commissioner");
     return error;
 }
 
@@ -511,7 +517,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send to joiner router request RelayTx (c/tx)", error);
+    LogWarnOnError(error, "send to joiner router request RelayTx (c/tx)");
 }
 
 Error BorderAgent::ForwardToLeader(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo, Uri aUri)
@@ -565,7 +571,7 @@
     LogInfo("Forwarded request to leader on %s", PathForUri(aUri));
 
 exit:
-    LogError("forward to leader", error);
+    LogWarnOnError(error, "forward to leader");
 
     if (error != kErrorNone)
     {
@@ -599,25 +605,55 @@
         LogInfo("Commissioner disconnected");
         IgnoreError(Get<Ip6::Udp>().RemoveReceiver(mUdpReceiver));
         Get<ThreadNetif>().RemoveUnicastAddress(mCommissionerAloc);
-        mState        = kStateStarted;
-        mUdpProxyPort = 0;
+
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+        if (mUsingEphemeralKey)
+        {
+            RestartAfterRemovingEphemeralKey();
+        }
+        else
+#endif
+        {
+            mState        = kStateStarted;
+            mUdpProxyPort = 0;
+        }
     }
 }
 
 uint16_t BorderAgent::GetUdpPort(void) const { return Get<Tmf::SecureAgent>().GetUdpPort(); }
 
-void BorderAgent::Start(void)
+Error BorderAgent::Start(uint16_t aUdpPort)
 {
     Error error;
     Pskc  pskc;
 
-    VerifyOrExit(mState == kStateStopped, error = kErrorNone);
-
     Get<KeyManager>().GetPskc(pskc);
-    SuccessOrExit(error = Get<Tmf::SecureAgent>().Start(kUdpPort));
-    SuccessOrExit(error = Get<Tmf::SecureAgent>().SetPsk(pskc.m8, Pskc::kSize));
-
+    error = Start(aUdpPort, pskc.m8, Pskc::kSize);
     pskc.Clear();
+
+    return error;
+}
+
+Error BorderAgent::Start(uint16_t aUdpPort, const uint8_t *aPsk, uint8_t aPskLength)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(mState == kStateStopped);
+
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    if (mUsingEphemeralKey)
+    {
+        SuccessOrExit(error = Get<Tmf::SecureAgent>().Start(aUdpPort, kMaxEphemeralKeyConnectionAttempts,
+                                                            HandleSecureAgentStopped, this));
+    }
+    else
+#endif
+    {
+        SuccessOrExit(error = Get<Tmf::SecureAgent>().Start(aUdpPort));
+    }
+
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().SetPsk(aPsk, aPskLength));
+
     Get<Tmf::SecureAgent>().SetConnectedCallback(HandleConnected, this);
 
     mState        = kStateStarted;
@@ -626,7 +662,8 @@
     LogInfo("Border Agent start listening on port %u", GetUdpPort());
 
 exit:
-    LogError("start agent", error);
+    LogWarnOnError(error, "start agent");
+    return error;
 }
 
 void BorderAgent::HandleTimeout(void)
@@ -642,27 +679,128 @@
 {
     VerifyOrExit(mState != kStateStopped);
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    if (mUsingEphemeralKey)
+    {
+        mUsingEphemeralKey = false;
+        mEphemeralKeyTimer.Stop();
+        mEphemeralKeyTask.Post();
+    }
+#endif
+
     mTimer.Stop();
     Get<Tmf::SecureAgent>().Stop();
 
     mState        = kStateStopped;
     mUdpProxyPort = 0;
-
     LogInfo("Border Agent stopped");
 
 exit:
     return;
 }
 
-#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
-void BorderAgent::LogError(const char *aActionText, Error aError)
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+
+Error BorderAgent::SetEphemeralKey(const char *aKeyString, uint32_t aTimeout, uint16_t aUdpPort)
 {
-    if (aError != kErrorNone)
+    Error    error  = kErrorNone;
+    uint16_t length = StringLength(aKeyString, kMaxEphemeralKeyLength + 1);
+
+    VerifyOrExit(mState == kStateStarted, error = kErrorInvalidState);
+    VerifyOrExit((length >= kMinEphemeralKeyLength) && (length <= kMaxEphemeralKeyLength), error = kErrorInvalidArgs);
+
+    if (!mUsingEphemeralKey)
     {
-        LogWarn("Failed to %s: %s", aActionText, ErrorToString(aError));
+        mOldUdpPort = GetUdpPort();
     }
+
+    Stop();
+
+    // We set the `mUsingEphemeralKey` before `Start()` since
+    // callbacks (like `HandleConnected()`) may be invoked from
+    // `Start()` itself.
+
+    mUsingEphemeralKey = true;
+
+    error = Start(aUdpPort, reinterpret_cast<const uint8_t *>(aKeyString), static_cast<uint8_t>(length));
+
+    if (error != kErrorNone)
+    {
+        mUsingEphemeralKey = false;
+        IgnoreError(Start(mOldUdpPort));
+        ExitNow();
+    }
+
+    mEphemeralKeyTask.Post();
+
+    if (aTimeout == 0)
+    {
+        aTimeout = kDefaultEphemeralKeyTimeout;
+    }
+
+    aTimeout = Min(aTimeout, kMaxEphemeralKeyTimeout);
+
+    mEphemeralKeyTimer.Start(aTimeout);
+
+    LogInfo("Allow ephemeral key for %lu msec on port %u", ToUlong(aTimeout), GetUdpPort());
+
+exit:
+    return error;
 }
-#endif
+
+void BorderAgent::ClearEphemeralKey(void)
+{
+    VerifyOrExit(mUsingEphemeralKey);
+
+    LogInfo("Clearing ephemeral key");
+    mEphemeralKeyTimer.Stop();
+
+    switch (mState)
+    {
+    case kStateStarted:
+        RestartAfterRemovingEphemeralKey();
+        break;
+
+    case kStateStopped:
+    case kStateActive:
+        // If there is an active commissioner connection, we wait till
+        // it gets disconnected before removing ephemeral key and
+        // restarting the agent.
+        break;
+    }
+
+exit:
+    return;
+}
+
+void BorderAgent::HandleEphemeralKeyTimeout(void)
+{
+    LogInfo("Ephemeral key timed out");
+    ClearEphemeralKey();
+}
+
+void BorderAgent::InvokeEphemeralKeyCallback(void) { mEphemeralKeyCallback.InvokeIfSet(); }
+
+void BorderAgent::RestartAfterRemovingEphemeralKey(void)
+{
+    LogInfo("Removing ephemeral key and restarting agent");
+
+    Stop();
+    IgnoreError(Start(mOldUdpPort));
+}
+
+void BorderAgent::HandleSecureAgentStopped(void *aContext)
+{
+    reinterpret_cast<BorderAgent *>(aContext)->HandleSecureAgentStopped();
+}
+
+void BorderAgent::HandleSecureAgentStopped(void)
+{
+    LogInfo("Reached max allowed connection attempts with ephemeral key");
+    RestartAfterRemovingEphemeralKey();
+}
+
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
 
 } // namespace MeshCoP
 } // namespace ot
diff --git a/src/core/meshcop/border_agent.hpp b/src/core/meshcop/border_agent.hpp
index 76d2938..c46a702 100644
--- a/src/core/meshcop/border_agent.hpp
+++ b/src/core/meshcop/border_agent.hpp
@@ -41,9 +41,12 @@
 #include <openthread/border_agent.h>
 
 #include "common/as_core_type.hpp"
+#include "common/heap_allocatable.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/notifier.hpp"
+#include "common/tasklet.hpp"
+#include "meshcop/secure_transport.hpp"
 #include "net/udp6.hpp"
 #include "thread/tmf.hpp"
 #include "thread/uri_paths.hpp"
@@ -59,6 +62,30 @@
     friend class Tmf::SecureAgent;
 
 public:
+    /**
+     * Minimum length of the ephemeral key string.
+     *
+     */
+    static constexpr uint16_t kMinEphemeralKeyLength = OT_BORDER_AGENT_MIN_EPHEMERAL_KEY_LENGTH;
+
+    /**
+     * Maximum length of the ephemeral key string.
+     *
+     */
+    static constexpr uint16_t kMaxEphemeralKeyLength = OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_LENGTH;
+
+    /**
+     * Default ephemeral key timeout interval in milliseconds.
+     *
+     */
+    static constexpr uint32_t kDefaultEphemeralKeyTimeout = OT_BORDER_AGENT_DEFAULT_EPHEMERAL_KEY_TIMEOUT;
+
+    /**
+     * Maximum ephemeral key timeout interval in milliseconds.
+     *
+     */
+    static constexpr uint32_t kMaxEphemeralKeyTimeout = OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_TIMEOUT;
+
     typedef otBorderAgentId Id; ///< Border Agent ID.
 
     /**
@@ -124,7 +151,7 @@
      * Starts the Border Agent service.
      *
      */
-    void Start(void);
+    void Start(void) { IgnoreError(Start(kUdpPort)); }
 
     /**
      * Stops the Border Agent service.
@@ -140,6 +167,75 @@
      */
     State GetState(void) const { return mState; }
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    /**
+     * Sets the ephemeral key for a given timeout duration.
+     *
+     * The ephemeral key can be set when the Border Agent is already running and is not currently connected to any
+     * external commissioner (i.e., it is in `kStateStarted` state).
+     *
+     * The given @p aKeyString is directly used as the ephemeral PSK (excluding the trailing null `\0` character). Its
+     * length must be between `kMinEphemeralKeyLength` and `kMaxEphemeralKeyLength`, inclusive.
+     *
+     * Setting the ephemeral key again before a previously set one is timed out will replace the previous one and will
+     * reset the timeout.
+     *
+     * While the timeout interval is in effect, the ephemeral key can be used only once by an external commissioner to
+     * connect. Once the commissioner disconnects, the ephemeral key is cleared, and Border Agent reverts to using
+     * PSKc.
+     *
+     * @param[in] aKeyString   The ephemeral key.
+     * @param[in] aTimeout     The timeout duration in milliseconds to use the ephemeral key.
+     *                         If zero, the default `kDefaultEphemeralKeyTimeout` value will be used.
+     *                         If the timeout value is larger than `kMaxEphemeralKeyTimeout`, the max value will be
+     *                         used instead.
+     * @param[in] aUdpPort     The UDP port to use with ephemeral key. If UDP port is zero, an ephemeral port will be
+     *                         used. `GetUdpPort()` will return the current UDP port being used.
+     *
+     * @retval kErrorNone           Successfully set the ephemeral key.
+     * @retval kErrorInvalidState   Agent is not running or connected to external commissioner.
+     * @retval kErrorInvalidArgs    The given @p aKeyString is not valid.
+     * @retval kErrorFailed         Failed to set the key (e.g., could not bind to UDP port).
+     *
+     */
+    Error SetEphemeralKey(const char *aKeyString, uint32_t aTimeout, uint16_t aUdpPort);
+
+    /**
+     * Cancels the ephemeral key in use if any.
+     *
+     * Can be used to cancel a previously set ephemeral key before it times out. If the Border Agent is not running or
+     * there is no ephemeral key in use, calling this function has no effect.
+     *
+     * If a commissioner is connected using the ephemeral key and is currently active, calling this method does not
+     * change its state. In this case the `IsEphemeralKeyActive()` will continue to return `true` until the commissioner
+     * disconnects.
+     *
+     */
+    void ClearEphemeralKey(void);
+
+    /**
+     * Indicates whether or not an ephemeral key is currently active.
+     *
+     * @retval TRUE    An ephemeral key is active.
+     * @retval FALSE   No ephemeral key is active.
+     *
+     */
+    bool IsEphemeralKeyActive(void) const { return mUsingEphemeralKey; }
+
+    /**
+     * Callback function pointer to notify when there is any changes related to use of ephemeral key by Border Agent.
+     *
+     *
+     */
+    typedef otBorderAgentEphemeralKeyCallback EphemeralKeyCallback;
+
+    void SetEphemeralKeyCallback(EphemeralKeyCallback aCallback, void *aContext)
+    {
+        mEphemeralKeyCallback.Set(aCallback, aContext);
+    }
+
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+
     /**
      * Returns the UDP Proxy port to which the commissioner is currently
      * bound.
@@ -150,10 +246,17 @@
     uint16_t GetUdpProxyPort(void) const { return mUdpProxyPort; }
 
 private:
+    static_assert(kMaxEphemeralKeyLength <= SecureTransport::kPskMaxLength,
+                  "Max ephemeral key length is larger than max PSK len");
+
     static constexpr uint16_t kUdpPort          = OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT;
     static constexpr uint32_t kKeepAliveTimeout = 50 * 1000; // Timeout to reject a commissioner (in msec)
 
-    class ForwardContext : public InstanceLocatorInit
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    static constexpr uint16_t kMaxEphemeralKeyConnectionAttempts = 10;
+#endif
+
+    class ForwardContext : public InstanceLocatorInit, public Heap::Allocatable<ForwardContext>
     {
     public:
         void     Init(Instance &aInstance, const Coap::Message &aMessage, bool aPetition, bool aSeparate);
@@ -170,6 +273,9 @@
         uint8_t  mToken[Coap::Message::kMaxTokenLength]; // The CoAP Token of the original request.
     };
 
+    Error Start(uint16_t aUdpPort);
+    Error Start(uint16_t aUdpPort, const uint8_t *aPsk, uint8_t aPskLength);
+
     void HandleNotifierEvents(Events aEvents);
 
     Coap::Message::Code CoapCodeFromError(Error aError);
@@ -184,6 +290,14 @@
 
     void HandleTimeout(void);
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    void        RestartAfterRemovingEphemeralKey(void);
+    void        HandleEphemeralKeyTimeout(void);
+    void        InvokeEphemeralKeyCallback(void);
+    static void HandleSecureAgentStopped(void *aContext);
+    void        HandleSecureAgentStopped(void);
+#endif
+
     static void HandleCoapResponse(void                *aContext,
                                    otMessage           *aMessage,
                                    const otMessageInfo *aMessageInfo,
@@ -194,13 +308,11 @@
     static bool HandleUdpReceive(void *aContext, const otMessage *aMessage, const otMessageInfo *aMessageInfo);
     bool        HandleUdpReceive(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
-    void LogError(const char *aActionText, Error aError);
-#else
-    void LogError(const char *, Error) {}
-#endif
-
     using TimeoutTimer = TimerMilliIn<BorderAgent, &BorderAgent::HandleTimeout>;
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    using EphemeralKeyTimer = TimerMilliIn<BorderAgent, &BorderAgent::HandleEphemeralKeyTimeout>;
+    using EphemeralKeyTask  = TaskletIn<BorderAgent, &BorderAgent::InvokeEphemeralKeyCallback>;
+#endif
 
     State                      mState;
     uint16_t                   mUdpProxyPort;
@@ -211,6 +323,13 @@
     Id   mId;
     bool mIdInitialized;
 #endif
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    bool                           mUsingEphemeralKey;
+    uint16_t                       mOldUdpPort;
+    EphemeralKeyTimer              mEphemeralKeyTimer;
+    EphemeralKeyTask               mEphemeralKeyTask;
+    Callback<EphemeralKeyCallback> mEphemeralKeyCallback;
+#endif
 };
 
 DeclareTmfHandler(BorderAgent, kUriRelayRx);
diff --git a/src/core/meshcop/commissioner.cpp b/src/core/meshcop/commissioner.cpp
index 92a52c1..999d732 100644
--- a/src/core/meshcop/commissioner.cpp
+++ b/src/core/meshcop/commissioner.cpp
@@ -302,9 +302,9 @@
     if ((error != kErrorNone) && (error != kErrorAlready))
     {
         Get<Tmf::SecureAgent>().Stop();
+        LogWarnOnError(error, "start commissioner");
     }
 
-    LogError("start commissioner", error);
     return error;
 }
 
@@ -343,7 +343,11 @@
 #endif
 
 exit:
-    LogError("stop commissioner", error);
+    if (error != kErrorAlready)
+    {
+        LogWarnOnError(error, "stop commissioner");
+    }
+
     return error;
 }
 
@@ -405,7 +409,8 @@
     error = SendMgmtCommissionerSetRequest(dataset, nullptr, 0);
 
 exit:
-    LogError("send MGMT_COMMISSIONER_SET.req", error);
+    LogWarnOnError(error, "send MGMT_COMMISSIONER_SET.req");
+    OT_UNUSED_VARIABLE(error);
 }
 
 void Commissioner::ClearJoiners(void)
@@ -857,7 +862,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send keep alive", error);
+    LogWarnOnError(error, "send keep alive");
 }
 
 void Commissioner::HandleLeaderKeepAliveResponse(void                *aContext,
diff --git a/src/core/meshcop/dataset.cpp b/src/core/meshcop/dataset.cpp
index 15c547b..f757392 100644
--- a/src/core/meshcop/dataset.cpp
+++ b/src/core/meshcop/dataset.cpp
@@ -56,6 +56,7 @@
     Error            error;
     Mac::ChannelMask supportedChannels = aInstance.Get<Mac::Mac>().GetSupportedChannelMask();
     Mac::ChannelMask preferredChannels(aInstance.Get<Radio>().GetPreferredChannelMask());
+    StringWriter     nameWriter(mNetworkName.m8, sizeof(mNetworkName));
 
     // If the preferred channel mask is not empty, select a random
     // channel from it, otherwise choose one from the supported
@@ -83,7 +84,7 @@
     SuccessOrExit(error = Random::Crypto::Fill(mExtendedPanId));
     SuccessOrExit(error = AsCoreType(&mMeshLocalPrefix).GenerateRandomUla());
 
-    snprintf(mNetworkName.m8, sizeof(mNetworkName), "%s-%04x", NetworkName::kNetworkNameInit, mPanId);
+    nameWriter.Append("%s-%04x", NetworkName::kNetworkNameInit, mPanId);
 
     mComponents.mIsActiveTimestampPresent = true;
     mComponents.mIsNetworkKeyPresent      = true;
@@ -104,49 +105,49 @@
 {
     bool isSubset = false;
 
-    if (IsNetworkKeyPresent())
+    if (IsPresent<kNetworkKey>())
     {
-        VerifyOrExit(aOther.IsNetworkKeyPresent() && GetNetworkKey() == aOther.GetNetworkKey());
+        VerifyOrExit(aOther.IsPresent<kNetworkKey>() && Get<kNetworkKey>() == aOther.Get<kNetworkKey>());
     }
 
-    if (IsNetworkNamePresent())
+    if (IsPresent<kNetworkName>())
     {
-        VerifyOrExit(aOther.IsNetworkNamePresent() && GetNetworkName() == aOther.GetNetworkName());
+        VerifyOrExit(aOther.IsPresent<kNetworkName>() && Get<kNetworkName>() == aOther.Get<kNetworkName>());
     }
 
-    if (IsExtendedPanIdPresent())
+    if (IsPresent<kExtendedPanId>())
     {
-        VerifyOrExit(aOther.IsExtendedPanIdPresent() && GetExtendedPanId() == aOther.GetExtendedPanId());
+        VerifyOrExit(aOther.IsPresent<kExtendedPanId>() && Get<kExtendedPanId>() == aOther.Get<kExtendedPanId>());
     }
 
-    if (IsMeshLocalPrefixPresent())
+    if (IsPresent<kMeshLocalPrefix>())
     {
-        VerifyOrExit(aOther.IsMeshLocalPrefixPresent() && GetMeshLocalPrefix() == aOther.GetMeshLocalPrefix());
+        VerifyOrExit(aOther.IsPresent<kMeshLocalPrefix>() && Get<kMeshLocalPrefix>() == aOther.Get<kMeshLocalPrefix>());
     }
 
-    if (IsPanIdPresent())
+    if (IsPresent<kPanId>())
     {
-        VerifyOrExit(aOther.IsPanIdPresent() && GetPanId() == aOther.GetPanId());
+        VerifyOrExit(aOther.IsPresent<kPanId>() && Get<kPanId>() == aOther.Get<kPanId>());
     }
 
-    if (IsChannelPresent())
+    if (IsPresent<kChannel>())
     {
-        VerifyOrExit(aOther.IsChannelPresent() && GetChannel() == aOther.GetChannel());
+        VerifyOrExit(aOther.IsPresent<kChannel>() && Get<kChannel>() == aOther.Get<kChannel>());
     }
 
-    if (IsPskcPresent())
+    if (IsPresent<kPskc>())
     {
-        VerifyOrExit(aOther.IsPskcPresent() && GetPskc() == aOther.GetPskc());
+        VerifyOrExit(aOther.IsPresent<kPskc>() && Get<kPskc>() == aOther.Get<kPskc>());
     }
 
-    if (IsSecurityPolicyPresent())
+    if (IsPresent<kSecurityPolicy>())
     {
-        VerifyOrExit(aOther.IsSecurityPolicyPresent() && GetSecurityPolicy() == aOther.GetSecurityPolicy());
+        VerifyOrExit(aOther.IsPresent<kSecurityPolicy>() && Get<kSecurityPolicy>() == aOther.Get<kSecurityPolicy>());
     }
 
-    if (IsChannelMaskPresent())
+    if (IsPresent<kChannelMask>())
     {
-        VerifyOrExit(aOther.IsChannelMaskPresent() && GetChannelMask() == aOther.GetChannelMask());
+        VerifyOrExit(aOther.IsPresent<kChannelMask>() && Get<kChannelMask>() == aOther.Get<kChannelMask>());
     }
 
     isSubset = true;
@@ -171,14 +172,64 @@
 
     for (const Tlv *cur = GetTlvsStart(); cur < end; cur = cur->GetNext())
     {
-        VerifyOrExit(!cur->IsExtended() && (cur + 1) <= end && cur->GetNext() <= end && Tlv::IsValid(*cur),
-                     rval = false);
+        VerifyOrExit(!cur->IsExtended() && (cur + 1) <= end && cur->GetNext() <= end && IsTlvValid(*cur), rval = false);
     }
 
 exit:
     return rval;
 }
 
+bool Dataset::IsTlvValid(const Tlv &aTlv)
+{
+    bool    isValid   = true;
+    uint8_t minLength = 0;
+
+    switch (aTlv.GetType())
+    {
+    case Tlv::kPanId:
+        minLength = sizeof(PanIdTlv::UintValueType);
+        break;
+    case Tlv::kExtendedPanId:
+        minLength = sizeof(ExtendedPanIdTlv::ValueType);
+        break;
+    case Tlv::kPskc:
+        minLength = sizeof(PskcTlv::ValueType);
+        break;
+    case Tlv::kNetworkKey:
+        minLength = sizeof(NetworkKeyTlv::ValueType);
+        break;
+    case Tlv::kMeshLocalPrefix:
+        minLength = sizeof(MeshLocalPrefixTlv::ValueType);
+        break;
+    case Tlv::kChannel:
+        VerifyOrExit(aTlv.GetLength() >= sizeof(ChannelTlvValue), isValid = false);
+        isValid = aTlv.ReadValueAs<ChannelTlv>().IsValid();
+        break;
+    case Tlv::kNetworkName:
+        isValid = As<NetworkNameTlv>(aTlv).IsValid();
+        break;
+
+    case Tlv::kSecurityPolicy:
+        isValid = As<SecurityPolicyTlv>(aTlv).IsValid();
+        break;
+
+    case Tlv::kChannelMask:
+        isValid = As<ChannelMaskTlv>(aTlv).IsValid();
+        break;
+
+    default:
+        break;
+    }
+
+    if (minLength > 0)
+    {
+        isValid = (aTlv.GetLength() >= minLength);
+    }
+
+exit:
+    return isValid;
+}
+
 const Tlv *Dataset::FindTlv(Tlv::Type aType) const { return As<Tlv>(Tlv::FindTlv(mTlvs, mLength, aType)); }
 
 void Dataset::ConvertTo(Info &aDatasetInfo) const
@@ -190,11 +241,11 @@
         switch (cur->GetType())
         {
         case Tlv::kActiveTimestamp:
-            aDatasetInfo.SetActiveTimestamp(cur->ReadValueAs<ActiveTimestampTlv>());
+            aDatasetInfo.Set<kActiveTimestamp>(cur->ReadValueAs<ActiveTimestampTlv>());
             break;
 
         case Tlv::kChannel:
-            aDatasetInfo.SetChannel(cur->ReadValueAs<ChannelTlv>().GetChannel());
+            aDatasetInfo.Set<kChannel>(cur->ReadValueAs<ChannelTlv>().GetChannel());
             break;
 
         case Tlv::kChannelMask:
@@ -203,46 +254,46 @@
 
             if (As<ChannelMaskTlv>(cur)->ReadChannelMask(mask) == kErrorNone)
             {
-                aDatasetInfo.SetChannelMask(mask);
+                aDatasetInfo.Set<kChannelMask>(mask);
             }
 
             break;
         }
 
         case Tlv::kDelayTimer:
-            aDatasetInfo.SetDelay(cur->ReadValueAs<DelayTimerTlv>());
+            aDatasetInfo.Set<kDelay>(cur->ReadValueAs<DelayTimerTlv>());
             break;
 
         case Tlv::kExtendedPanId:
-            aDatasetInfo.SetExtendedPanId(cur->ReadValueAs<ExtendedPanIdTlv>());
+            aDatasetInfo.Set<kExtendedPanId>(cur->ReadValueAs<ExtendedPanIdTlv>());
             break;
 
         case Tlv::kMeshLocalPrefix:
-            aDatasetInfo.SetMeshLocalPrefix(cur->ReadValueAs<MeshLocalPrefixTlv>());
+            aDatasetInfo.Set<kMeshLocalPrefix>(cur->ReadValueAs<MeshLocalPrefixTlv>());
             break;
 
         case Tlv::kNetworkKey:
-            aDatasetInfo.SetNetworkKey(cur->ReadValueAs<NetworkKeyTlv>());
+            aDatasetInfo.Set<kNetworkKey>(cur->ReadValueAs<NetworkKeyTlv>());
             break;
 
         case Tlv::kNetworkName:
-            aDatasetInfo.SetNetworkName(As<NetworkNameTlv>(cur)->GetNetworkName());
+            IgnoreError(aDatasetInfo.Update<kNetworkName>().Set(As<NetworkNameTlv>(cur)->GetNetworkName()));
             break;
 
         case Tlv::kPanId:
-            aDatasetInfo.SetPanId(cur->ReadValueAs<PanIdTlv>());
+            aDatasetInfo.Set<kPanId>(cur->ReadValueAs<PanIdTlv>());
             break;
 
         case Tlv::kPendingTimestamp:
-            aDatasetInfo.SetPendingTimestamp(cur->ReadValueAs<PendingTimestampTlv>());
+            aDatasetInfo.Set<kPendingTimestamp>(cur->ReadValueAs<PendingTimestampTlv>());
             break;
 
         case Tlv::kPskc:
-            aDatasetInfo.SetPskc(cur->ReadValueAs<PskcTlv>());
+            aDatasetInfo.Set<kPskc>(cur->ReadValueAs<PskcTlv>());
             break;
 
         case Tlv::kSecurityPolicy:
-            aDatasetInfo.SetSecurityPolicy(As<SecurityPolicyTlv>(cur)->GetSecurityPolicy());
+            aDatasetInfo.Set<kSecurityPolicy>(As<SecurityPolicyTlv>(cur)->GetSecurityPolicy());
             break;
 
         default:
@@ -251,10 +302,10 @@
     }
 }
 
-void Dataset::ConvertTo(otOperationalDatasetTlvs &aDataset) const
+void Dataset::ConvertTo(Tlvs &aTlvs) const
 {
-    memcpy(aDataset.mTlvs, mTlvs, mLength);
-    aDataset.mLength = static_cast<uint8_t>(mLength);
+    memcpy(aTlvs.mTlvs, mTlvs, mLength);
+    aTlvs.mLength = static_cast<uint8_t>(mLength);
 }
 
 void Dataset::Set(Type aType, const Dataset &aDataset)
@@ -271,91 +322,91 @@
     mUpdateTime = aDataset.GetUpdateTime();
 }
 
-void Dataset::SetFrom(const otOperationalDatasetTlvs &aDataset)
+void Dataset::SetFrom(const Tlvs &aTlvs)
 {
-    mLength = aDataset.mLength;
-    memcpy(mTlvs, aDataset.mTlvs, mLength);
+    mLength = aTlvs.mLength;
+    memcpy(mTlvs, aTlvs.mTlvs, mLength);
 }
 
 Error Dataset::SetFrom(const Info &aDatasetInfo)
 {
     Error error = kErrorNone;
 
-    if (aDatasetInfo.IsActiveTimestampPresent())
+    if (aDatasetInfo.IsPresent<kActiveTimestamp>())
     {
         Timestamp activeTimestamp;
 
-        aDatasetInfo.GetActiveTimestamp(activeTimestamp);
+        aDatasetInfo.Get<kActiveTimestamp>(activeTimestamp);
         IgnoreError(Write<ActiveTimestampTlv>(activeTimestamp));
     }
 
-    if (aDatasetInfo.IsPendingTimestampPresent())
+    if (aDatasetInfo.IsPresent<kPendingTimestamp>())
     {
         Timestamp pendingTimestamp;
 
-        aDatasetInfo.GetPendingTimestamp(pendingTimestamp);
+        aDatasetInfo.Get<kPendingTimestamp>(pendingTimestamp);
         IgnoreError(Write<PendingTimestampTlv>(pendingTimestamp));
     }
 
-    if (aDatasetInfo.IsDelayPresent())
+    if (aDatasetInfo.IsPresent<kDelay>())
     {
-        IgnoreError(Write<DelayTimerTlv>(aDatasetInfo.GetDelay()));
+        IgnoreError(Write<DelayTimerTlv>(aDatasetInfo.Get<kDelay>()));
     }
 
-    if (aDatasetInfo.IsChannelPresent())
+    if (aDatasetInfo.IsPresent<kChannel>())
     {
         ChannelTlvValue channelValue;
 
-        channelValue.SetChannelAndPage(aDatasetInfo.GetChannel());
+        channelValue.SetChannelAndPage(aDatasetInfo.Get<kChannel>());
         IgnoreError(Write<ChannelTlv>(channelValue));
     }
 
-    if (aDatasetInfo.IsChannelMaskPresent())
+    if (aDatasetInfo.IsPresent<kChannelMask>())
     {
         ChannelMaskTlv::Value value;
 
-        ChannelMaskTlv::PrepareValue(value, aDatasetInfo.GetChannelMask());
+        ChannelMaskTlv::PrepareValue(value, aDatasetInfo.Get<kChannelMask>());
         IgnoreError(WriteTlv(Tlv::kChannelMask, value.mData, value.mLength));
     }
 
-    if (aDatasetInfo.IsExtendedPanIdPresent())
+    if (aDatasetInfo.IsPresent<kExtendedPanId>())
     {
-        IgnoreError(Write<ExtendedPanIdTlv>(aDatasetInfo.GetExtendedPanId()));
+        IgnoreError(Write<ExtendedPanIdTlv>(aDatasetInfo.Get<kExtendedPanId>()));
     }
 
-    if (aDatasetInfo.IsMeshLocalPrefixPresent())
+    if (aDatasetInfo.IsPresent<kMeshLocalPrefix>())
     {
-        IgnoreError(Write<MeshLocalPrefixTlv>(aDatasetInfo.GetMeshLocalPrefix()));
+        IgnoreError(Write<MeshLocalPrefixTlv>(aDatasetInfo.Get<kMeshLocalPrefix>()));
     }
 
-    if (aDatasetInfo.IsNetworkKeyPresent())
+    if (aDatasetInfo.IsPresent<kNetworkKey>())
     {
-        IgnoreError(Write<NetworkKeyTlv>(aDatasetInfo.GetNetworkKey()));
+        IgnoreError(Write<NetworkKeyTlv>(aDatasetInfo.Get<kNetworkKey>()));
     }
 
-    if (aDatasetInfo.IsNetworkNamePresent())
+    if (aDatasetInfo.IsPresent<kNetworkName>())
     {
-        NameData nameData = aDatasetInfo.GetNetworkName().GetAsData();
+        NameData nameData = aDatasetInfo.Get<kNetworkName>().GetAsData();
 
         IgnoreError(WriteTlv(Tlv::kNetworkName, nameData.GetBuffer(), nameData.GetLength()));
     }
 
-    if (aDatasetInfo.IsPanIdPresent())
+    if (aDatasetInfo.IsPresent<kPanId>())
     {
-        IgnoreError(Write<PanIdTlv>(aDatasetInfo.GetPanId()));
+        IgnoreError(Write<PanIdTlv>(aDatasetInfo.Get<kPanId>()));
     }
 
-    if (aDatasetInfo.IsPskcPresent())
+    if (aDatasetInfo.IsPresent<kPskc>())
     {
-        IgnoreError(Write<PskcTlv>(aDatasetInfo.GetPskc()));
+        IgnoreError(Write<PskcTlv>(aDatasetInfo.Get<kPskc>()));
     }
 
-    if (aDatasetInfo.IsSecurityPolicyPresent())
+    if (aDatasetInfo.IsPresent<kSecurityPolicy>())
     {
         SecurityPolicyTlv tlv;
 
         tlv.Init();
-        tlv.SetSecurityPolicy(aDatasetInfo.GetSecurityPolicy());
+        tlv.SetSecurityPolicy(aDatasetInfo.Get<kSecurityPolicy>());
         IgnoreError(WriteTlv(tlv));
     }
 
@@ -364,38 +415,9 @@
     return error;
 }
 
-Error Dataset::GetTimestamp(Type aType, Timestamp &aTimestamp) const
+Error Dataset::ReadTimestamp(Type aType, Timestamp &aTimestamp) const
 {
-    Error      error = kErrorNone;
-    const Tlv *tlv;
-
-    if (aType == kActive)
-    {
-        tlv = FindTlv(Tlv::kActiveTimestamp);
-        VerifyOrExit(tlv != nullptr, error = kErrorNotFound);
-        aTimestamp = tlv->ReadValueAs<ActiveTimestampTlv>();
-    }
-    else
-    {
-        tlv = FindTlv(Tlv::kPendingTimestamp);
-        VerifyOrExit(tlv != nullptr, error = kErrorNotFound);
-        aTimestamp = tlv->ReadValueAs<PendingTimestampTlv>();
-    }
-
-exit:
-    return error;
-}
-
-void Dataset::SetTimestamp(Type aType, const Timestamp &aTimestamp)
-{
-    if (aType == kActive)
-    {
-        IgnoreError(Write<ActiveTimestampTlv>(aTimestamp));
-    }
-    else
-    {
-        IgnoreError(Write<PendingTimestampTlv>(aTimestamp));
-    }
+    return (aType == kActive) ? Read<ActiveTimestampTlv>(aTimestamp) : Read<PendingTimestampTlv>(aTimestamp);
 }
 
 Error Dataset::WriteTlv(Tlv::Type aType, const void *aValue, uint8_t aLength)
@@ -449,53 +471,6 @@
 
 void Dataset::RemoveTlv(Tlv::Type aType) { RemoveTlv(FindTlv(aType)); }
 
-Error Dataset::AppendMleDatasetTlv(Type aType, Message &aMessage) const
-{
-    Error          error = kErrorNone;
-    Mle::Tlv       tlv;
-    Mle::Tlv::Type type;
-
-    VerifyOrExit(mLength > 0);
-
-    type = (aType == kActive ? Mle::Tlv::kActiveDataset : Mle::Tlv::kPendingDataset);
-
-    tlv.SetType(type);
-    tlv.SetLength(static_cast<uint8_t>(mLength) - sizeof(Tlv) - sizeof(Timestamp));
-    SuccessOrExit(error = aMessage.Append(tlv));
-
-    for (const Tlv *cur = GetTlvsStart(); cur < GetTlvsEnd(); cur = cur->GetNext())
-    {
-        if (((aType == kActive) && (cur->GetType() == Tlv::kActiveTimestamp)) ||
-            ((aType == kPending) && (cur->GetType() == Tlv::kPendingTimestamp)))
-        {
-            ; // skip Active or Pending Timestamp TLV
-        }
-        else if (cur->GetType() == Tlv::kDelayTimer)
-        {
-            uint32_t elapsed    = TimerMilli::GetNow() - mUpdateTime;
-            uint32_t delayTimer = cur->ReadValueAs<DelayTimerTlv>();
-
-            if (delayTimer > elapsed)
-            {
-                delayTimer -= elapsed;
-            }
-            else
-            {
-                delayTimer = 0;
-            }
-
-            SuccessOrExit(error = Tlv::Append<DelayTimerTlv>(aMessage, delayTimer));
-        }
-        else
-        {
-            SuccessOrExit(error = cur->AppendTo(aMessage));
-        }
-    }
-
-exit:
-    return error;
-}
-
 void Dataset::RemoveTlv(Tlv *aTlv)
 {
     if (aTlv != nullptr)
@@ -508,7 +483,14 @@
     }
 }
 
-Error Dataset::ApplyConfiguration(Instance &aInstance, bool *aIsNetworkKeyUpdated) const
+Error Dataset::ApplyConfiguration(Instance &aInstance) const
+{
+    bool isNetworkKeyUpdated;
+
+    return ApplyConfiguration(aInstance, isNetworkKeyUpdated);
+}
+
+Error Dataset::ApplyConfiguration(Instance &aInstance, bool &aIsNetworkKeyUpdated) const
 {
     Mac::Mac   &mac        = aInstance.Get<Mac::Mac>();
     KeyManager &keyManager = aInstance.Get<KeyManager>();
@@ -516,10 +498,7 @@
 
     VerifyOrExit(IsValid(), error = kErrorParse);
 
-    if (aIsNetworkKeyUpdated)
-    {
-        *aIsNetworkKeyUpdated = false;
-    }
+    aIsNetworkKeyUpdated = false;
 
     for (const Tlv *cur = GetTlvsStart(); cur < GetTlvsEnd(); cur = cur->GetNext())
     {
@@ -558,9 +537,9 @@
 
             keyManager.GetNetworkKey(networkKey);
 
-            if (aIsNetworkKeyUpdated && (cur->ReadValueAs<NetworkKeyTlv>() != networkKey))
+            if (cur->ReadValueAs<NetworkKeyTlv>() != networkKey)
             {
-                *aIsNetworkKeyUpdated = true;
+                aIsNetworkKeyUpdated = true;
             }
 
             keyManager.SetNetworkKey(cur->ReadValueAs<NetworkKeyTlv>());
@@ -568,11 +547,9 @@
         }
 
 #if OPENTHREAD_FTD
-
         case Tlv::kPskc:
             keyManager.SetPskc(cur->ReadValueAs<PskcTlv>());
             break;
-
 #endif
 
         case Tlv::kMeshLocalPrefix:
diff --git a/src/core/meshcop/dataset.hpp b/src/core/meshcop/dataset.hpp
index 131a271..af03896 100644
--- a/src/core/meshcop/dataset.hpp
+++ b/src/core/meshcop/dataset.hpp
@@ -63,7 +63,6 @@
 public:
     static constexpr uint8_t kMaxSize      = OT_OPERATIONAL_DATASET_MAX_LENGTH; ///< Max size of MeshCoP Dataset (bytes)
     static constexpr uint8_t kMaxValueSize = 16;                                ///< Max size of a TLV value (bytes)
-    static constexpr uint8_t kMaxGetTypes  = 64;                                ///< Max number of types in MGMT_GET.req
 
     /**
      * Represents the Dataset type (active or pending).
@@ -76,107 +75,57 @@
     };
 
     /**
+     * Represents a Dataset as a sequence of TLVs.
+     *
+     */
+    typedef otOperationalDatasetTlvs Tlvs;
+
+    /**
+     * Represents a component in Dataset.
+     *
+     */
+    enum Component : uint8_t
+    {
+        kActiveTimestamp,  ///< Active Timestamp
+        kPendingTimestamp, ///< Pending Timestamp
+        kNetworkKey,       ///< Network Key
+        kNetworkName,      ///< Network Name
+        kExtendedPanId,    ///< Extended PAN Identifier
+        kMeshLocalPrefix,  ///< Mesh Local Prefix
+        kDelay,            ///< Delay
+        kPanId,            ///< PAN Identifier
+        kChannel,          ///< Channel
+        kPskc,             ///< PSKc
+        kSecurityPolicy,   ///< Security Policy
+        kChannelMask,      ///< Channel Mask
+    };
+
+    template <Component kComponent> struct TypeFor; ///< Specifies the associate type for a given `Component`.
+
+    class Info;
+
+    /**
      * Represents presence of different components in Active or Pending Operational Dataset.
      *
      */
     class Components : public otOperationalDatasetComponents, public Clearable<Components>
     {
+        friend class Info;
+
     public:
         /**
-         * Indicates whether or not the Active Timestamp is present in the Dataset.
+         * Indicates whether or not the specified `kComponent` is present in the Dataset.
          *
-         * @returns TRUE if Active Timestamp is present, FALSE otherwise.
+         * @tparam kComponent  The component to check.
+         *
+         * @retval TRUE   The component is present in the Dataset.
+         * @retval FALSE  The component is not present in the Dataset.
          *
          */
-        bool IsActiveTimestampPresent(void) const { return mIsActiveTimestampPresent; }
+        template <Component kComponent> bool IsPresent(void) const;
 
-        /**
-         * Indicates whether or not the Pending Timestamp is present in the Dataset.
-         *
-         * @returns TRUE if Pending Timestamp is present, FALSE otherwise.
-         *
-         */
-        bool IsPendingTimestampPresent(void) const { return mIsPendingTimestampPresent; }
-
-        /**
-         * Indicates whether or not the Network Key is present in the Dataset.
-         *
-         * @returns TRUE if Network Key is present, FALSE otherwise.
-         *
-         */
-        bool IsNetworkKeyPresent(void) const { return mIsNetworkKeyPresent; }
-
-        /**
-         * Indicates whether or not the Network Name is present in the Dataset.
-         *
-         * @returns TRUE if Network Name is present, FALSE otherwise.
-         *
-         */
-        bool IsNetworkNamePresent(void) const { return mIsNetworkNamePresent; }
-
-        /**
-         * Indicates whether or not the Extended PAN ID is present in the Dataset.
-         *
-         * @returns TRUE if Extended PAN ID is present, FALSE otherwise.
-         *
-         */
-        bool IsExtendedPanIdPresent(void) const { return mIsExtendedPanIdPresent; }
-
-        /**
-         * Indicates whether or not the Mesh Local Prefix is present in the Dataset.
-         *
-         * @returns TRUE if Mesh Local Prefix is present, FALSE otherwise.
-         *
-         */
-        bool IsMeshLocalPrefixPresent(void) const { return mIsMeshLocalPrefixPresent; }
-
-        /**
-         * Indicates whether or not the Delay Timer is present in the Dataset.
-         *
-         * @returns TRUE if Delay Timer is present, FALSE otherwise.
-         *
-         */
-        bool IsDelayPresent(void) const { return mIsDelayPresent; }
-
-        /**
-         * Indicates whether or not the PAN ID is present in the Dataset.
-         *
-         * @returns TRUE if PAN ID is present, FALSE otherwise.
-         *
-         */
-        bool IsPanIdPresent(void) const { return mIsPanIdPresent; }
-
-        /**
-         * Indicates whether or not the Channel is present in the Dataset.
-         *
-         * @returns TRUE if Channel is present, FALSE otherwise.
-         *
-         */
-        bool IsChannelPresent(void) const { return mIsChannelPresent; }
-
-        /**
-         * Indicates whether or not the PSKc is present in the Dataset.
-         *
-         * @returns TRUE if PSKc is present, FALSE otherwise.
-         *
-         */
-        bool IsPskcPresent(void) const { return mIsPskcPresent; }
-
-        /**
-         * Indicates whether or not the Security Policy is present in the Dataset.
-         *
-         * @returns TRUE if Security Policy is present, FALSE otherwise.
-         *
-         */
-        bool IsSecurityPolicyPresent(void) const { return mIsSecurityPolicyPresent; }
-
-        /**
-         * Indicates whether or not the Channel Mask is present in the Dataset.
-         *
-         * @returns TRUE if Channel Mask is present, FALSE otherwise.
-         *
-         */
-        bool IsChannelMaskPresent(void) const { return mIsChannelMaskPresent; }
+    private:
+        template <Component kComponent> void MarkAsPresent(void);
     };
 
     /**
@@ -187,389 +136,66 @@
     {
     public:
         /**
-         * Indicates whether or not the Active Timestamp is present in the Dataset.
+         * Indicates whether or not the specified component is present in the Dataset.
          *
-         * @returns TRUE if Active Timestamp is present, FALSE otherwise.
+         * @tparam kComponent  The component to check.
+         *
+         * @retval TRUE   The component is present in the Dataset.
+         * @retval FALSE  The component is not present in the Dataset.
          *
          */
-        bool IsActiveTimestampPresent(void) const { return mComponents.mIsActiveTimestampPresent; }
+        template <Component kComponent> bool IsPresent(void) const { return GetComponents().IsPresent<kComponent>(); }
 
         /**
-         * Gets the Active Timestamp in the Dataset.
+         * Gets the specified component in the Dataset.
          *
-         * MUST be used when Active Timestamp component is present in the Dataset, otherwise its behavior is
-         * undefined.
+         * @tparam  kComponent  The component to check.
          *
-         * @param[out] aTimestamp  A reference to output the Active Timestamp in the Dataset.
+         * MUST be used when component is present in the Dataset, otherwise its behavior is undefined.
+         *
+         * @returns The component value.
          *
          */
-        void GetActiveTimestamp(Timestamp &aTimestamp) const { aTimestamp.SetFromTimestamp(mActiveTimestamp); }
+        template <Component kComponent> const typename TypeFor<kComponent>::Type &Get(void) const;
 
         /**
-         * Sets the Active Timestamp in the Dataset.
+         * Gets the specified component in the Dataset.
          *
-         * @param[in] aTimestamp   A Timestamp value.
+         * @tparam  kComponent  The component to check.
+         *
+         * MUST be used when component is present in the Dataset, otherwise its behavior is undefined.
+         *
+         * @pram[out] aComponent  A reference to output the component value.
          *
          */
-        void SetActiveTimestamp(const Timestamp &aTimestamp)
+        template <Component kComponent> void Get(typename TypeFor<kComponent>::Type &aComponent) const;
+
+        /**
+         * Sets the specified component in the Dataset.
+         *
+         * @tparam  kComponent  The component to set.
+         *
+         * @param[in] aComponent   The component value.
+         *
+         */
+        template <Component kComponent> void Set(const typename TypeFor<kComponent>::Type &aComponent)
         {
-            aTimestamp.ConvertTo(mActiveTimestamp);
-            mComponents.mIsActiveTimestampPresent = true;
+            GetComponents().MarkAsPresent<kComponent>();
+            AsNonConst(Get<kComponent>()) = aComponent;
         }
 
         /**
-         * Indicates whether or not the Pending Timestamp is present in the Dataset.
+         * Returns a reference to the specified component in the Dataset to be updated by caller.
          *
-         * @returns TRUE if Pending Timestamp is present, FALSE otherwise.
+         * @tparam  kComponent  The component to set.
+         *
+         * @returns A reference to the component in the Dataset.
          *
          */
-        bool IsPendingTimestampPresent(void) const { return mComponents.mIsPendingTimestampPresent; }
-
-        /**
-         * Gets the Pending Timestamp in the Dataset.
-         *
-         * MUST be used when Pending Timestamp component is present in the Dataset, otherwise its behavior
-         * is undefined.
-         *
-         * @param[out] aTimestamp  A reference to output the Pending Timestamp in the Dataset.
-         *
-         */
-        void GetPendingTimestamp(Timestamp &aTimestamp) const { aTimestamp.SetFromTimestamp(mPendingTimestamp); }
-
-        /**
-         * Sets the Pending Timestamp in the Dataset.
-         *
-         * @param[in] aTimestamp   A Timestamp value.
-         *
-         */
-        void SetPendingTimestamp(const Timestamp &aTimestamp)
+        template <Component kComponent> typename TypeFor<kComponent>::Type &Update(void)
         {
-            aTimestamp.ConvertTo(mPendingTimestamp);
-            mComponents.mIsPendingTimestampPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the Network Key is present in the Dataset.
-         *
-         * @returns TRUE if Network Key is present, FALSE otherwise.
-         *
-         */
-        bool IsNetworkKeyPresent(void) const { return mComponents.mIsNetworkKeyPresent; }
-
-        /**
-         * Gets the Network Key in the Dataset.
-         *
-         * MUST be used when Network Key component is present in the Dataset, otherwise its behavior
-         * is undefined.
-         *
-         * @returns The Network Key in the Dataset.
-         *
-         */
-        const NetworkKey &GetNetworkKey(void) const { return AsCoreType(&mNetworkKey); }
-
-        /**
-         * Sets the Network Key in the Dataset.
-         *
-         * @param[in] aNetworkKey  A Network Key.
-         *
-         */
-        void SetNetworkKey(const NetworkKey &aNetworkKey)
-        {
-            mNetworkKey                      = aNetworkKey;
-            mComponents.mIsNetworkKeyPresent = true;
-        }
-
-        /**
-         * Returns a reference to the Network Key in the Dataset to be updated by caller.
-         *
-         * @returns A reference to the Network Key in the Dataset.
-         *
-         */
-        NetworkKey &UpdateNetworkKey(void)
-        {
-            mComponents.mIsNetworkKeyPresent = true;
-            return AsCoreType(&mNetworkKey);
-        }
-
-        /**
-         * Indicates whether or not the Network Name is present in the Dataset.
-         *
-         * @returns TRUE if Network Name is present, FALSE otherwise.
-         *
-         */
-        bool IsNetworkNamePresent(void) const { return mComponents.mIsNetworkNamePresent; }
-
-        /**
-         * Gets the Network Name in the Dataset.
-         *
-         * MUST be used when Network Name component is present in the Dataset, otherwise its behavior is
-         * undefined.
-         *
-         * @returns The Network Name in the Dataset.
-         *
-         */
-        const NetworkName &GetNetworkName(void) const { return AsCoreType(&mNetworkName); }
-
-        /**
-         * Sets the Network Name in the Dataset.
-         *
-         * @param[in] aNetworkNameData   A Network Name Data.
-         *
-         */
-        void SetNetworkName(const NameData &aNetworkNameData)
-        {
-            IgnoreError(AsCoreType(&mNetworkName).Set(aNetworkNameData));
-            mComponents.mIsNetworkNamePresent = true;
-        }
-
-        /**
-         * Indicates whether or not the Extended PAN ID is present in the Dataset.
-         *
-         * @returns TRUE if Extended PAN ID is present, FALSE otherwise.
-         *
-         */
-        bool IsExtendedPanIdPresent(void) const { return mComponents.mIsExtendedPanIdPresent; }
-
-        /**
-         * Gets the Extended PAN ID in the Dataset.
-         *
-         * MUST be used when Extended PAN ID component is present in the Dataset, otherwise its behavior is
-         * undefined.
-         *
-         * @returns The Extended PAN ID in the Dataset.
-         *
-         */
-        const ExtendedPanId &GetExtendedPanId(void) const { return AsCoreType(&mExtendedPanId); }
-
-        /**
-         * Sets the Extended PAN ID in the Dataset.
-         *
-         * @param[in] aExtendedPanId   An Extended PAN ID.
-         *
-         */
-        void SetExtendedPanId(const ExtendedPanId &aExtendedPanId)
-        {
-            mExtendedPanId                      = aExtendedPanId;
-            mComponents.mIsExtendedPanIdPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the Mesh Local Prefix is present in the Dataset.
-         *
-         * @returns TRUE if Mesh Local Prefix is present, FALSE otherwise.
-         *
-         */
-        bool IsMeshLocalPrefixPresent(void) const { return mComponents.mIsMeshLocalPrefixPresent; }
-
-        /**
-         * Gets the Mesh Local Prefix in the Dataset.
-         *
-         * MUST be used when Mesh Local Prefix component is present in the Dataset, otherwise its behavior
-         * is undefined.
-         *
-         * @returns The Mesh Local Prefix in the Dataset.
-         *
-         */
-        const Ip6::NetworkPrefix &GetMeshLocalPrefix(void) const
-        {
-            return static_cast<const Ip6::NetworkPrefix &>(mMeshLocalPrefix);
-        }
-
-        /**
-         * Sets the Mesh Local Prefix in the Dataset.
-         *
-         * @param[in] aMeshLocalPrefix   A Mesh Local Prefix.
-         *
-         */
-        void SetMeshLocalPrefix(const Ip6::NetworkPrefix &aMeshLocalPrefix)
-        {
-            mMeshLocalPrefix                      = aMeshLocalPrefix;
-            mComponents.mIsMeshLocalPrefixPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the Delay Timer is present in the Dataset.
-         *
-         * @returns TRUE if Delay Timer is present, FALSE otherwise.
-         *
-         */
-        bool IsDelayPresent(void) const { return mComponents.mIsDelayPresent; }
-
-        /**
-         * Gets the Delay Timer in the Dataset.
-         *
-         * MUST be used when Delay Timer component is present in the Dataset, otherwise its behavior is
-         * undefined.
-         *
-         * @returns The Delay Timer in the Dataset.
-         *
-         */
-        uint32_t GetDelay(void) const { return mDelay; }
-
-        /**
-         * Sets the Delay Timer in the Dataset.
-         *
-         * @param[in] aDelay  A Delay value.
-         *
-         */
-        void SetDelay(uint32_t aDelay)
-        {
-            mDelay                      = aDelay;
-            mComponents.mIsDelayPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the PAN ID is present in the Dataset.
-         *
-         * @returns TRUE if PAN ID is present, FALSE otherwise.
-         *
-         */
-        bool IsPanIdPresent(void) const { return mComponents.mIsPanIdPresent; }
-
-        /**
-         * Gets the PAN ID in the Dataset.
-         *
-         * MUST be used when PAN ID component is present in the Dataset, otherwise its behavior is
-         * undefined.
-         *
-         * @returns The PAN ID in the Dataset.
-         *
-         */
-        Mac::PanId GetPanId(void) const { return mPanId; }
-
-        /**
-         * Sets the PAN ID in the Dataset.
-         *
-         * @param[in] aPanId  A PAN ID.
-         *
-         */
-        void SetPanId(Mac::PanId aPanId)
-        {
-            mPanId                      = aPanId;
-            mComponents.mIsPanIdPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the Channel is present in the Dataset.
-         *
-         * @returns TRUE if Channel is present, FALSE otherwise.
-         *
-         */
-        bool IsChannelPresent(void) const { return mComponents.mIsChannelPresent; }
-
-        /**
-         * Gets the Channel in the Dataset.
-         *
-         * MUST be used when Channel component is present in the Dataset, otherwise its behavior is
-         * undefined.
-         *
-         * @returns The Channel in the Dataset.
-         *
-         */
-        uint16_t GetChannel(void) const { return mChannel; }
-
-        /**
-         * Sets the Channel in the Dataset.
-         *
-         * @param[in] aChannel  A Channel.
-         *
-         */
-        void SetChannel(uint16_t aChannel)
-        {
-            mChannel                      = aChannel;
-            mComponents.mIsChannelPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the PSKc is present in the Dataset.
-         *
-         * @returns TRUE if PSKc is present, FALSE otherwise.
-         *
-         */
-        bool IsPskcPresent(void) const { return mComponents.mIsPskcPresent; }
-
-        /**
-         * Gets the PSKc in the Dataset.
-         *
-         * MUST be used when PSKc component is present in the Dataset, otherwise its behavior is undefined.
-         *
-         * @returns The PSKc in the Dataset.
-         *
-         */
-        const Pskc &GetPskc(void) const { return AsCoreType(&mPskc); }
-
-        /**
-         * Set the PSKc in the Dataset.
-         *
-         * @param[in] aPskc  A PSKc value.
-         *
-         */
-        void SetPskc(const Pskc &aPskc)
-        {
-            mPskc                      = aPskc;
-            mComponents.mIsPskcPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the Security Policy is present in the Dataset.
-         *
-         * @returns TRUE if Security Policy is present, FALSE otherwise.
-         *
-         */
-        bool IsSecurityPolicyPresent(void) const { return mComponents.mIsSecurityPolicyPresent; }
-
-        /**
-         * Gets the Security Policy in the Dataset.
-         *
-         * MUST be used when Security Policy component is present in the Dataset, otherwise its behavior is
-         * undefined.
-         *
-         * @returns The Security Policy in the Dataset.
-         *
-         */
-        const SecurityPolicy &GetSecurityPolicy(void) const { return AsCoreType(&mSecurityPolicy); }
-
-        /**
-         * Sets the Security Policy in the Dataset.
-         *
-         * @param[in] aSecurityPolicy  A Security Policy to set in Dataset.
-         *
-         */
-        void SetSecurityPolicy(const SecurityPolicy &aSecurityPolicy)
-        {
-            mSecurityPolicy                      = aSecurityPolicy;
-            mComponents.mIsSecurityPolicyPresent = true;
-        }
-
-        /**
-         * Indicates whether or not the Channel Mask is present in the Dataset.
-         *
-         * @returns TRUE if Channel Mask is present, FALSE otherwise.
-         *
-         */
-        bool IsChannelMaskPresent(void) const { return mComponents.mIsChannelMaskPresent; }
-
-        /**
-         * Gets the Channel Mask in the Dataset.
-         *
-         * MUST be used when Channel Mask component is present in the Dataset, otherwise its behavior is
-         * undefined.
-         *
-         * @returns The Channel Mask in the Dataset.
-         *
-         */
-        otChannelMask GetChannelMask(void) const { return mChannelMask; }
-
-        /**
-         * Sets the Channel Mask in the Dataset.
-         *
-         * @param[in] aChannelMask   A Channel Mask value.
-         *
-         */
-        void SetChannelMask(otChannelMask aChannelMask)
-        {
-            mChannelMask                      = aChannelMask;
-            mComponents.mIsChannelMaskPresent = true;
+            GetComponents().MarkAsPresent<kComponent>();
+            return AsNonConst(Get<kComponent>());
         }
 
         /**
@@ -600,6 +226,10 @@
          *
          */
         bool IsSubsetOf(const Info &aOther) const;
+
+    private:
+        Components       &GetComponents(void) { return static_cast<Components &>(mComponents); }
+        const Components &GetComponents(void) const { return static_cast<const Components &>(mComponents); }
     };
 
     /**
@@ -623,6 +253,20 @@
     bool IsValid(void) const;
 
     /**
+     * Validates the format and value of a given MeshCoP TLV used in Dataset.
+     *
+     * TLV types that can appear in an Active or Pending Operational Dataset are validated. Other TLV types including
+     * unknown TLV types are considered as valid.
+     *
+     * @param[in]  aTlv    The TLV to validate.
+     *
+     * @retval  TRUE       The TLV format and value is valid, or TLV type is unknown (not supported in Dataset).
+     * @retval  FALSE      The TLV format or value is invalid.
+     *
+     */
+    static bool IsTlvValid(const Tlv &aTlv);
+
+    /**
      * Indicates whether or not a given TLV type is present in the Dataset.
      *
      * @param[in] aType  The TLV type to check.
@@ -668,6 +312,58 @@
     const Tlv *FindTlv(Tlv::Type aType) const;
 
     /**
+     * Finds and reads a simple TLV in the Dataset.
+     *
+     * If the specified TLV type is not found, `kErrorNotFound` is reported.
+     *
+     * @tparam  SimpleTlvType   The simple TLV type (must be a sub-class of `SimpleTlvInfo`).
+     *
+     * @param[out] aValue       A reference to return the read TLV value.
+     *
+     * @retval kErrorNone      Successfully found and read the TLV value. @p aValue is updated.
+     * @retval kErrorNotFound  Could not find the TLV in the Dataset.
+     *
+     */
+    template <typename SimpleTlvType> Error Read(typename SimpleTlvType::ValueType &aValue) const
+    {
+        const Tlv *tlv = FindTlv(static_cast<Tlv::Type>(SimpleTlvType::kType));
+
+        return (tlv == nullptr) ? kErrorNotFound : (aValue = tlv->ReadValueAs<SimpleTlvType>(), kErrorNone);
+    }
+
+    /**
+     * Finds and reads an `uint` TLV in the Dataset.
+     *
+     * If the specified TLV type is not found, `kErrorNotFound` is reported.
+     *
+     * @tparam  UintTlvType     The integer simple TLV type (must be a sub-class of `UintTlvInfo`).
+     *
+     * @param[out] aValue       A reference to return the read TLV value.
+     *
+     * @retval kErrorNone      Successfully found and read the TLV value. @p aValue is updated.
+     * @retval kErrorNotFound  Could not find the TLV in the Dataset.
+     *
+     */
+    template <typename UintTlvType> Error Read(typename UintTlvType::UintValueType &aValue) const
+    {
+        const Tlv *tlv = FindTlv(static_cast<Tlv::Type>(UintTlvType::kType));
+
+        return (tlv == nullptr) ? kErrorNotFound : (aValue = tlv->ReadValueAs<UintTlvType>(), kErrorNone);
+    }
+
+    /**
+     * Reads the Timestamp (Active or Pending).
+     *
+     * @param[in]  aType       The type: active or pending.
+     * @param[out] aTimestamp  A reference to a `Timestamp` to output the value.
+     *
+     * @retval kErrorNone      Timestamp was read successfully. @p aTimestamp is updated.
+     * @retval kErrorNotFound  Could not find the requested Timestamp TLV.
+     *
+     */
+    Error ReadTimestamp(Type aType, Timestamp &aTimestamp) const;
+
+    /**
      * Writes a TLV to the Dataset.
      *
      * If the specified TLV type already exists, it will be replaced. Otherwise, the TLV will be appended.
@@ -770,10 +466,10 @@
     /**
      * Converts the TLV representation to structure representation.
      *
-     * @param[out] aDataset  A reference to `otOperationalDatasetTlvs` to output the Dataset.
+     * @param[out] aTlvs  A reference to output the Dataset as a sequence of TLVs.
      *
      */
-    void ConvertTo(otOperationalDatasetTlvs &aDataset) const;
+    void ConvertTo(Tlvs &aTlvs) const;
 
     /**
      * Returns the Dataset size in bytes.
@@ -800,27 +496,6 @@
     TimeMilli GetUpdateTime(void) const { return mUpdateTime; }
 
     /**
-     * Gets the Timestamp (Active or Pending).
-     *
-     * @param[in]  aType       The type: active or pending.
-     * @param[out] aTimestamp  A reference to a `Timestamp` to output the value.
-     *
-     * @retval kErrorNone      Timestamp was read successfully. @p aTimestamp is updated.
-     * @retval kErrorNotFound  Could not find the requested Timestamp TLV.
-     *
-     */
-    Error GetTimestamp(Type aType, Timestamp &aTimestamp) const;
-
-    /**
-     * Sets the Timestamp value.
-     *
-     * @param[in] aType        The type: active or pending.
-     * @param[in] aTimestamp   A Timestamp.
-     *
-     */
-    void SetTimestamp(Type aType, const Timestamp &aTimestamp);
-
-    /**
      * Reads the Dataset from a given message and checks that it is well-formed and valid.
      *
      * @param[in]  aMessage  The message to read from.
@@ -859,34 +534,33 @@
     /**
      * Sets the Dataset using @p aDataset.
      *
-     * @param[in]  aDataset  The input Dataset as otOperationalDatasetTlvs.
+     * @param[in]  aDataset  The input Dataset as `Tlvs`.
      *
      */
-    void SetFrom(const otOperationalDatasetTlvs &aDataset);
-
-    /**
-     * Appends the MLE Dataset TLV but excluding MeshCoP Sub Timestamp TLV.
-     *
-     * @param[in] aType          The type of the dataset, active or pending.
-     * @param[in] aMessage       A message to append to.
-     *
-     * @retval kErrorNone    Successfully append MLE Dataset TLV without MeshCoP Sub Timestamp TLV.
-     * @retval kErrorNoBufs  Insufficient available buffers to append the message with MLE Dataset TLV.
-     *
-     */
-    Error AppendMleDatasetTlv(Type aType, Message &aMessage) const;
+    void SetFrom(const Tlvs &aTlvs);
 
     /**
      * Applies the Active or Pending Dataset to the Thread interface.
      *
      * @param[in]  aInstance            A reference to the OpenThread instance.
-     * @param[out] aIsNetworkKeyUpdated A pointer to where to place whether network key was updated.
      *
      * @retval kErrorNone   Successfully applied configuration.
      * @retval kErrorParse  The dataset has at least one TLV with invalid format.
      *
      */
-    Error ApplyConfiguration(Instance &aInstance, bool *aIsNetworkKeyUpdated = nullptr) const;
+    Error ApplyConfiguration(Instance &aInstance) const;
+
+    /**
+     * Applies the Active or Pending Dataset to the Thread interface.
+     *
+     * @param[in]  aInstance              A reference to the OpenThread instance.
+     * @param[out] aIsNetworkKeyUpdated   Variable to return whether network key was updated.
+     *
+     * @retval kErrorNone   Successfully applied configuration, @p aIsNetworkKeyUpdated is changed.
+     * @retval kErrorParse  The dataset has at least one TLV with invalid format.
+     *
+     */
+    Error ApplyConfiguration(Instance &aInstance, bool &aIsNetworkKeyUpdated) const;
 
     /**
      * Converts a Pending Dataset to an Active Dataset.
@@ -977,6 +651,121 @@
     uint16_t  mLength;         ///< The number of valid bytes in @var mTlvs
 };
 
+//---------------------------------------------------------------------------------------------------------------------
+// Template specializations
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// `Dataset::Components::IsPresent()` and `Dataset::Components::MarkAsPresent()`
+
+#define DefineIsPresentAndMarkAsPresent(Component)                                            \
+    template <> inline bool Dataset::Components::IsPresent<Dataset::k##Component>(void) const \
+    {                                                                                         \
+        return mIs##Component##Present;                                                       \
+    }                                                                                         \
+                                                                                              \
+    template <> inline void Dataset::Components::MarkAsPresent<Dataset::k##Component>(void)   \
+    {                                                                                         \
+        mIs##Component##Present = true;                                                       \
+    }
+
+// clang-format off
+
+DefineIsPresentAndMarkAsPresent(ActiveTimestamp)
+DefineIsPresentAndMarkAsPresent(PendingTimestamp)
+DefineIsPresentAndMarkAsPresent(NetworkKey)
+DefineIsPresentAndMarkAsPresent(NetworkName)
+DefineIsPresentAndMarkAsPresent(ExtendedPanId)
+DefineIsPresentAndMarkAsPresent(MeshLocalPrefix)
+DefineIsPresentAndMarkAsPresent(Delay)
+DefineIsPresentAndMarkAsPresent(PanId)
+DefineIsPresentAndMarkAsPresent(Channel)
+DefineIsPresentAndMarkAsPresent(Pskc)
+DefineIsPresentAndMarkAsPresent(SecurityPolicy)
+DefineIsPresentAndMarkAsPresent(ChannelMask)
+
+#undef DefineIsPresentAndMarkAsPresent
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// `Dataset::TypeFor<>`
+
+template <> struct Dataset::TypeFor<Dataset::kActiveTimestamp>  { using Type = Timestamp; };
+template <> struct Dataset::TypeFor<Dataset::kPendingTimestamp> { using Type = Timestamp; };
+template <> struct Dataset::TypeFor<Dataset::kNetworkKey>       { using Type = NetworkKey; };
+template <> struct Dataset::TypeFor<Dataset::kNetworkName>      { using Type = NetworkName; };
+template <> struct Dataset::TypeFor<Dataset::kExtendedPanId>    { using Type = ExtendedPanId; };
+template <> struct Dataset::TypeFor<Dataset::kMeshLocalPrefix>  { using Type = Ip6::NetworkPrefix; };
+template <> struct Dataset::TypeFor<Dataset::kDelay>            { using Type = uint32_t; };
+template <> struct Dataset::TypeFor<Dataset::kPanId>            { using Type = Mac::PanId; };
+template <> struct Dataset::TypeFor<Dataset::kChannel>          { using Type = uint16_t; };
+template <> struct Dataset::TypeFor<Dataset::kPskc>             { using Type = Pskc; };
+template <> struct Dataset::TypeFor<Dataset::kSecurityPolicy>   { using Type = SecurityPolicy; };
+template <> struct Dataset::TypeFor<Dataset::kChannelMask>      { using Type = uint32_t; };
+
+// clang-format on
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Dataset::Info::Get<>()
+
+template <> inline const NetworkKey &Dataset::Info::Get<Dataset::kNetworkKey>(void) const
+{
+    return AsCoreType(&mNetworkKey);
+}
+
+template <> inline const NetworkName &Dataset::Info::Get<Dataset::kNetworkName>(void) const
+{
+    return AsCoreType(&mNetworkName);
+}
+
+template <> inline const ExtendedPanId &Dataset::Info::Get<Dataset::kExtendedPanId>(void) const
+{
+    return AsCoreType(&mExtendedPanId);
+}
+
+template <> inline const Ip6::NetworkPrefix &Dataset::Info::Get<Dataset::kMeshLocalPrefix>(void) const
+{
+    return static_cast<const Ip6::NetworkPrefix &>(mMeshLocalPrefix);
+}
+
+template <> inline const uint32_t &Dataset::Info::Get<Dataset::kDelay>(void) const { return mDelay; }
+
+template <> inline const Mac::PanId &Dataset::Info::Get<Dataset::kPanId>(void) const { return mPanId; }
+
+template <> inline const uint16_t &Dataset::Info::Get<Dataset::kChannel>(void) const { return mChannel; }
+
+template <> inline const Pskc &Dataset::Info::Get<Dataset::kPskc>(void) const { return AsCoreType(&mPskc); }
+
+template <> inline const SecurityPolicy &Dataset::Info::Get<Dataset::kSecurityPolicy>(void) const
+{
+    return AsCoreType(&mSecurityPolicy);
+}
+
+template <> inline const uint32_t &Dataset::Info::Get<Dataset::kChannelMask>(void) const { return mChannelMask; }
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Active and Pending Timestamp
+
+template <> inline void Dataset::Info::Get<Dataset::kActiveTimestamp>(Timestamp &aTimestamp) const
+{
+    aTimestamp.SetFromTimestamp(mActiveTimestamp);
+}
+
+template <> inline void Dataset::Info::Get<Dataset::kPendingTimestamp>(Timestamp &aTimestamp) const
+{
+    aTimestamp.SetFromTimestamp(mPendingTimestamp);
+}
+
+template <> inline void Dataset::Info::Set<Dataset::kActiveTimestamp>(const Timestamp &aTimestamp)
+{
+    GetComponents().MarkAsPresent<kActiveTimestamp>();
+    aTimestamp.ConvertTo(mActiveTimestamp);
+}
+
+template <> inline void Dataset::Info::Set<Dataset::kPendingTimestamp>(const Timestamp &aTimestamp)
+{
+    GetComponents().MarkAsPresent<kPendingTimestamp>();
+    aTimestamp.ConvertTo(mPendingTimestamp);
+}
+
 } // namespace MeshCoP
 
 DefineCoreType(otOperationalDatasetComponents, MeshCoP::Dataset::Components);
diff --git a/src/core/meshcop/dataset_local.cpp b/src/core/meshcop/dataset_local.cpp
index edb8108..6fec084 100644
--- a/src/core/meshcop/dataset_local.cpp
+++ b/src/core/meshcop/dataset_local.cpp
@@ -82,7 +82,7 @@
     SuccessOrExit(error);
 
     mSaved            = true;
-    mTimestampPresent = (aDataset.GetTimestamp(mType, mTimestamp) == kErrorNone);
+    mTimestampPresent = (aDataset.ReadTimestamp(mType, mTimestamp) == kErrorNone);
 
 exit:
     return error;
@@ -106,25 +106,10 @@
     }
     else
     {
-        uint32_t elapsed;
-        uint32_t delayTimer;
-        Tlv     *tlv = aDataset.FindTlv(Tlv::kDelayTimer);
+        Tlv *tlv = aDataset.FindTlv(Tlv::kDelayTimer);
 
         VerifyOrExit(tlv != nullptr);
-
-        elapsed    = TimerMilli::GetNow() - mUpdateTime;
-        delayTimer = tlv->ReadValueAs<DelayTimerTlv>();
-
-        if (delayTimer > elapsed)
-        {
-            delayTimer -= elapsed;
-        }
-        else
-        {
-            delayTimer = 0;
-        }
-
-        tlv->WriteValueAs<DelayTimerTlv>(delayTimer);
+        tlv->WriteValueAs<DelayTimerTlv>(DelayTimerTlv::CalculateRemainingDelay(*tlv, mUpdateTime));
     }
 
     aDataset.mUpdateTime = TimerMilli::GetNow();
@@ -147,15 +132,15 @@
     return error;
 }
 
-Error DatasetLocal::Read(otOperationalDatasetTlvs &aDataset) const
+Error DatasetLocal::Read(Dataset::Tlvs &aDatasetTlvs) const
 {
     Dataset dataset;
     Error   error;
 
-    ClearAllBytes(aDataset);
+    ClearAllBytes(aDatasetTlvs);
 
     SuccessOrExit(error = Read(dataset));
-    dataset.ConvertTo(aDataset);
+    dataset.ConvertTo(aDatasetTlvs);
 
 exit:
     return error;
@@ -173,11 +158,11 @@
     return error;
 }
 
-Error DatasetLocal::Save(const otOperationalDatasetTlvs &aDataset)
+Error DatasetLocal::Save(const Dataset::Tlvs &aDatasetTlvs)
 {
     Dataset dataset;
 
-    dataset.SetFrom(aDataset);
+    dataset.SetFrom(aDatasetTlvs);
 
     return Save(dataset);
 }
@@ -214,7 +199,7 @@
         LogInfo("%s dataset set", Dataset::TypeToString(mType));
     }
 
-    mTimestampPresent = (aDataset.GetTimestamp(mType, mTimestamp) == kErrorNone);
+    mTimestampPresent = (aDataset.ReadTimestamp(mType, mTimestamp) == kErrorNone);
     mUpdateTime       = TimerMilli::GetNow();
 
 exit:
diff --git a/src/core/meshcop/dataset_local.hpp b/src/core/meshcop/dataset_local.hpp
index 249f069..40e52bd 100644
--- a/src/core/meshcop/dataset_local.hpp
+++ b/src/core/meshcop/dataset_local.hpp
@@ -134,13 +134,13 @@
     /**
      * Retrieves the dataset from non-volatile memory.
      *
-     * @param[out]  aDataset  Where to place the dataset.
+     * @param[out]  aDatasetTlvs  Where to place the dataset.
      *
      * @retval kErrorNone      Successfully retrieved the dataset.
      * @retval kErrorNotFound  There is no corresponding dataset stored in non-volatile memory.
      *
      */
-    Error Read(otOperationalDatasetTlvs &aDataset) const;
+    Error Read(Dataset::Tlvs &aDatasetTlvs) const;
 
     /**
      * Returns the local time this dataset was last updated or restored.
@@ -164,13 +164,13 @@
     /**
      * Stores the dataset into non-volatile memory.
      *
-     * @param[in]  aDataset  The Dataset to save as `otOperationalDatasetTlvs`.
+     * @param[in]  aDatasetTlvs  The Dataset to save as `Dataset::Tlvs`.
      *
      * @retval kErrorNone             Successfully saved the dataset.
      * @retval kErrorNotImplemented   The platform does not implement settings functionality.
      *
      */
-    Error Save(const otOperationalDatasetTlvs &aDataset);
+    Error Save(const Dataset::Tlvs &aDatasetTlvs);
 
     /**
      * Stores the dataset into non-volatile memory.
diff --git a/src/core/meshcop/dataset_manager.cpp b/src/core/meshcop/dataset_manager.cpp
index 8bf1cb3..ef4cfcd 100644
--- a/src/core/meshcop/dataset_manager.cpp
+++ b/src/core/meshcop/dataset_manager.cpp
@@ -53,6 +53,9 @@
 
 RegisterLogModule("DatasetManager");
 
+//---------------------------------------------------------------------------------------------------------------------
+// DatasetManager
+
 DatasetManager::DatasetManager(Instance &aInstance, Dataset::Type aType, Timer::Handler aTimerHandler)
     : InstanceLocator(aInstance)
     , mLocal(aInstance, aType)
@@ -76,7 +79,7 @@
 
     SuccessOrExit(error = mLocal.Restore(dataset));
 
-    mTimestampValid = (dataset.GetTimestamp(GetType(), mTimestamp) == kErrorNone);
+    mTimestampValid = (dataset.ReadTimestamp(GetType(), mTimestamp) == kErrorNone);
 
     if (IsActiveDataset())
     {
@@ -118,13 +121,13 @@
     int   compare;
     bool  isNetworkKeyUpdated = false;
 
-    if (aDataset.GetTimestamp(GetType(), mTimestamp) == kErrorNone)
+    if (aDataset.ReadTimestamp(GetType(), mTimestamp) == kErrorNone)
     {
         mTimestampValid = true;
 
         if (IsActiveDataset())
         {
-            SuccessOrExit(error = aDataset.ApplyConfiguration(GetInstance(), &isNetworkKeyUpdated));
+            SuccessOrExit(error = aDataset.ApplyConfiguration(GetInstance(), isNetworkKeyUpdated));
         }
     }
 
@@ -160,11 +163,11 @@
     return error;
 }
 
-Error DatasetManager::Save(const otOperationalDatasetTlvs &aDataset)
+Error DatasetManager::Save(const Dataset::Tlvs &aDatasetTlvs)
 {
     Error error;
 
-    SuccessOrExit(error = mLocal.Save(aDataset));
+    SuccessOrExit(error = mLocal.Save(aDatasetTlvs));
     HandleDatasetUpdated();
 
 exit:
@@ -213,8 +216,7 @@
 
 void DatasetManager::SignalDatasetChange(void) const
 {
-    Get<Notifier>().Signal(mLocal.GetType() == Dataset::kActive ? kEventActiveDatasetChanged
-                                                                : kEventPendingDatasetChanged);
+    Get<Notifier>().Signal(IsActiveDataset() ? kEventActiveDatasetChanged : kEventPendingDatasetChanged);
 }
 
 Error DatasetManager::GetChannelMask(Mac::ChannelMask &aChannelMask) const
@@ -259,7 +261,7 @@
 
         IgnoreError(Get<PendingDatasetManager>().Read(pendingDataset));
 
-        if ((pendingDataset.GetTimestamp(Dataset::kActive, timestamp) == kErrorNone) &&
+        if ((pendingDataset.Read<ActiveTimestampTlv>(timestamp) == kErrorNone) &&
             (Timestamp::Compare(&timestamp, mLocal.GetTimestamp()) == 0))
         {
             // stop registration attempts during dataset transition
@@ -292,7 +294,11 @@
         OT_FALL_THROUGH;
 
     default:
-        LogError("send Dataset set to leader", error);
+        if (error != kErrorAlready)
+        {
+            LogWarnOnError(error, "send Dataset set to leader");
+        }
+
         FreeMessage(message);
         break;
     }
@@ -340,53 +346,34 @@
 
 void DatasetManager::HandleGet(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const
 {
-    Tlv      tlv;
-    uint16_t offset = aMessage.GetOffset();
-    uint8_t  tlvs[Dataset::kMaxGetTypes];
-    uint8_t  length = 0;
+    TlvList  tlvList;
+    uint8_t  tlvType;
+    uint16_t offset;
+    uint16_t length;
 
-    while (offset < aMessage.GetLength())
+    SuccessOrExit(Tlv::FindTlvValueOffset(aMessage, Tlv::kGet, offset, length));
+
+    for (; length > 0; length--, offset++)
     {
-        SuccessOrExit(aMessage.Read(offset, tlv));
-
-        if (tlv.GetType() == Tlv::kGet)
-        {
-            length = tlv.GetLength();
-
-            if (length > (sizeof(tlvs) - 1))
-            {
-                // leave space for potential DelayTimer type below
-                length = sizeof(tlvs) - 1;
-            }
-
-            aMessage.ReadBytes(offset + sizeof(Tlv), tlvs, length);
-            break;
-        }
-
-        offset += sizeof(tlv) + tlv.GetLength();
+        IgnoreError(aMessage.Read(offset, tlvType));
+        tlvList.Add(tlvType);
     }
 
-    // MGMT_PENDING_GET.rsp must include Delay Timer TLV (Thread 1.1.1 Section 8.7.5.4)
-    VerifyOrExit(length > 0 && IsPendingDataset());
+    // MGMT_PENDING_GET.rsp must include Delay Timer TLV (Thread 1.1.1
+    // Section 8.7.5.4).
 
-    for (uint8_t i = 0; i < length; i++)
+    if (!tlvList.IsEmpty() && IsPendingDataset())
     {
-        if (tlvs[i] == Tlv::kDelayTimer)
-        {
-            ExitNow();
-        }
+        tlvList.Add(Tlv::kDelayTimer);
     }
 
-    tlvs[length++] = Tlv::kDelayTimer;
-
 exit:
-    SendGetResponse(aMessage, aMessageInfo, tlvs, length);
+    SendGetResponse(aMessage, aMessageInfo, tlvList);
 }
 
 void DatasetManager::SendGetResponse(const Coap::Message    &aRequest,
                                      const Ip6::MessageInfo &aMessageInfo,
-                                     uint8_t                *aTlvs,
-                                     uint8_t                 aLength) const
+                                     const TlvList          &aTlvList) const
 {
     Error          error = kErrorNone;
     Coap::Message *message;
@@ -397,37 +384,29 @@
     message = Get<Tmf::Agent>().NewPriorityResponseMessage(aRequest);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
-    if (aLength == 0)
+    for (const Tlv *tlv = dataset.GetTlvsStart(); tlv < dataset.GetTlvsEnd(); tlv = tlv->GetNext())
     {
-        for (const Tlv *cur = dataset.GetTlvsStart(); cur < dataset.GetTlvsEnd(); cur = cur->GetNext())
+        bool shouldAppend = true;
+
+        if (!aTlvList.IsEmpty())
         {
-            if (cur->GetType() != Tlv::kNetworkKey || Get<KeyManager>().GetSecurityPolicy().mObtainNetworkKeyEnabled)
-            {
-                SuccessOrExit(error = cur->AppendTo(*message));
-            }
+            shouldAppend = aTlvList.Contains(tlv->GetType());
         }
-    }
-    else
-    {
-        for (uint8_t index = 0; index < aLength; index++)
+
+        if ((tlv->GetType() == Tlv::kNetworkKey) && !Get<KeyManager>().GetSecurityPolicy().mObtainNetworkKeyEnabled)
         {
-            const Tlv *tlv;
+            shouldAppend = false;
+        }
 
-            if (aTlvs[index] == Tlv::kNetworkKey && !Get<KeyManager>().GetSecurityPolicy().mObtainNetworkKeyEnabled)
-            {
-                continue;
-            }
-
-            if ((tlv = dataset.FindTlv(static_cast<Tlv::Type>(aTlvs[index]))) != nullptr)
-            {
-                SuccessOrExit(error = tlv->AppendTo(*message));
-            }
+        if (shouldAppend)
+        {
+            SuccessOrExit(error = tlv->AppendTo(*message));
         }
     }
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo));
 
-    LogInfo("sent %s dataset get response to %s", (GetType() == Dataset::kActive ? "active" : "pending"),
+    LogInfo("sent %s dataset get response to %s", IsActiveDataset() ? "active" : "pending",
             aMessageInfo.GetPeerAddr().ToString().AsCString());
 
 exit:
@@ -462,30 +441,11 @@
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
 #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
-
-    if (Get<Commissioner>().IsActive())
+    if (Get<Commissioner>().IsActive() && (Tlv::Find<CommissionerSessionIdTlv>(aTlvs, aLength) == nullptr))
     {
-        const Tlv *end          = reinterpret_cast<const Tlv *>(aTlvs + aLength);
-        bool       hasSessionId = false;
-
-        for (const Tlv *cur = reinterpret_cast<const Tlv *>(aTlvs); cur < end; cur = cur->GetNext())
-        {
-            VerifyOrExit((cur + 1) <= end, error = kErrorInvalidArgs);
-
-            if (cur->GetType() == Tlv::kCommissionerSessionId)
-            {
-                hasSessionId = true;
-                break;
-            }
-        }
-
-        if (!hasSessionId)
-        {
-            SuccessOrExit(error = Tlv::Append<CommissionerSessionIdTlv>(*message, Get<Commissioner>().GetSessionId()));
-        }
+        SuccessOrExit(error = Tlv::Append<CommissionerSessionIdTlv>(*message, Get<Commissioner>().GetSessionId()));
     }
-
-#endif // OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
+#endif
 
     SuccessOrExit(error = AppendDatasetToMessage(aDatasetInfo, *message));
 
@@ -521,62 +481,62 @@
 
     length = 0;
 
-    if (aDatasetComponents.IsActiveTimestampPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kActiveTimestamp>())
     {
         datasetTlvs[length++] = Tlv::kActiveTimestamp;
     }
 
-    if (aDatasetComponents.IsPendingTimestampPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kPendingTimestamp>())
     {
         datasetTlvs[length++] = Tlv::kPendingTimestamp;
     }
 
-    if (aDatasetComponents.IsNetworkKeyPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kNetworkKey>())
     {
         datasetTlvs[length++] = Tlv::kNetworkKey;
     }
 
-    if (aDatasetComponents.IsNetworkNamePresent())
+    if (aDatasetComponents.IsPresent<Dataset::kNetworkName>())
     {
         datasetTlvs[length++] = Tlv::kNetworkName;
     }
 
-    if (aDatasetComponents.IsExtendedPanIdPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kExtendedPanId>())
     {
         datasetTlvs[length++] = Tlv::kExtendedPanId;
     }
 
-    if (aDatasetComponents.IsMeshLocalPrefixPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kMeshLocalPrefix>())
     {
         datasetTlvs[length++] = Tlv::kMeshLocalPrefix;
     }
 
-    if (aDatasetComponents.IsDelayPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kDelay>())
     {
         datasetTlvs[length++] = Tlv::kDelayTimer;
     }
 
-    if (aDatasetComponents.IsPanIdPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kPanId>())
     {
         datasetTlvs[length++] = Tlv::kPanId;
     }
 
-    if (aDatasetComponents.IsChannelPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kChannel>())
     {
         datasetTlvs[length++] = Tlv::kChannel;
     }
 
-    if (aDatasetComponents.IsPskcPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kPskc>())
     {
         datasetTlvs[length++] = Tlv::kPskc;
     }
 
-    if (aDatasetComponents.IsSecurityPolicyPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kSecurityPolicy>())
     {
         datasetTlvs[length++] = Tlv::kSecurityPolicy;
     }
 
-    if (aDatasetComponents.IsChannelMaskPresent())
+    if (aDatasetComponents.IsPresent<Dataset::kChannelMask>())
     {
         datasetTlvs[length++] = Tlv::kChannelMask;
     }
@@ -618,6 +578,17 @@
     return error;
 }
 
+void DatasetManager::TlvList::Add(uint8_t aTlvType)
+{
+    if (!Contains(aTlvType))
+    {
+        IgnoreError(PushBack(aTlvType));
+    }
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// ActiveDatasetManager
+
 ActiveDatasetManager::ActiveDatasetManager(Instance &aInstance)
     : DatasetManager(aInstance, Dataset::kActive, ActiveDatasetManager::HandleTimer)
 {
@@ -634,8 +605,9 @@
 
     SuccessOrExit(Read(datasetInfo));
 
-    isValid = (datasetInfo.IsNetworkKeyPresent() && datasetInfo.IsNetworkNamePresent() &&
-               datasetInfo.IsExtendedPanIdPresent() && datasetInfo.IsPanIdPresent() && datasetInfo.IsChannelPresent());
+    isValid = (datasetInfo.IsPresent<Dataset::kNetworkKey>() && datasetInfo.IsPresent<Dataset::kNetworkName>() &&
+               datasetInfo.IsPresent<Dataset::kExtendedPanId>() && datasetInfo.IsPresent<Dataset::kPanId>() &&
+               datasetInfo.IsPresent<Dataset::kChannel>());
 
 exit:
     return isValid;
@@ -650,7 +622,7 @@
     Dataset dataset;
 
     SuccessOrExit(error = dataset.ReadFromMessage(aMessage, aOffset, aLength));
-    dataset.SetTimestamp(Dataset::kActive, aTimestamp);
+    SuccessOrExit(error = dataset.Write<ActiveTimestampTlv>(aTimestamp));
     error = DatasetManager::Save(dataset);
 
 exit:
@@ -665,6 +637,9 @@
 
 void ActiveDatasetManager::HandleTimer(Timer &aTimer) { aTimer.Get<ActiveDatasetManager>().HandleTimer(); }
 
+//---------------------------------------------------------------------------------------------------------------------
+// PendingDatasetManager
+
 PendingDatasetManager::PendingDatasetManager(Instance &aInstance)
     : DatasetManager(aInstance, Dataset::kPending, PendingDatasetManager::HandleTimer)
     , mDelayTimer(aInstance)
@@ -697,11 +672,11 @@
     return error;
 }
 
-Error PendingDatasetManager::Save(const otOperationalDatasetTlvs &aDataset)
+Error PendingDatasetManager::Save(const Dataset::Tlvs &aDatasetTlvs)
 {
     Error error;
 
-    SuccessOrExit(error = DatasetManager::Save(aDataset));
+    SuccessOrExit(error = DatasetManager::Save(aDatasetTlvs));
     StartDelayTimer();
 
 exit:
@@ -728,7 +703,7 @@
     Dataset dataset;
 
     SuccessOrExit(error = dataset.ReadFromMessage(aMessage, aOffset, aLength));
-    dataset.SetTimestamp(Dataset::kPending, aTimestamp);
+    SuccessOrExit(dataset.Write<PendingTimestampTlv>(aTimestamp));
     SuccessOrExit(error = DatasetManager::Save(dataset));
     StartDelayTimer();
 
@@ -749,10 +724,7 @@
     tlv = dataset.FindTlv(Tlv::kDelayTimer);
     VerifyOrExit(tlv != nullptr);
 
-    delay = tlv->ReadValueAs<DelayTimerTlv>();
-
-    // the Timer implementation does not support the full 32 bit range
-    delay = Min(delay, Timer::kMaxDelay);
+    delay = Min(tlv->ReadValueAs<DelayTimerTlv>(), DelayTimerTlv::kMaxDelay);
 
     mDelayTimer.StartAt(dataset.GetUpdateTime(), delay);
     LogInfo("delay timer started %lu", ToUlong(delay));
@@ -763,25 +735,9 @@
 
 void PendingDatasetManager::HandleDelayTimer(void)
 {
-    Tlv    *tlv;
     Dataset dataset;
 
     IgnoreError(Read(dataset));
-
-    // if the Delay Timer value is larger than what our Timer implementation can handle, we have to compute
-    // the remainder and wait some more.
-    if ((tlv = dataset.FindTlv(Tlv::kDelayTimer)) != nullptr)
-    {
-        uint32_t elapsed = mDelayTimer.GetFireTime() - dataset.GetUpdateTime();
-        uint32_t delay   = tlv->ReadValueAs<DelayTimerTlv>();
-
-        if (elapsed < delay)
-        {
-            mDelayTimer.StartAt(mDelayTimer.GetFireTime(), delay - elapsed);
-            ExitNow();
-        }
-    }
-
     LogInfo("pending delay timer expired");
 
     dataset.ConvertToActive();
@@ -789,9 +745,6 @@
     Get<ActiveDatasetManager>().Save(dataset);
 
     Clear();
-
-exit:
-    return;
 }
 
 template <>
diff --git a/src/core/meshcop/dataset_manager.hpp b/src/core/meshcop/dataset_manager.hpp
index 36162a3..5972397 100644
--- a/src/core/meshcop/dataset_manager.hpp
+++ b/src/core/meshcop/dataset_manager.hpp
@@ -96,13 +96,13 @@
     /**
      * Retrieves the dataset from non-volatile memory.
      *
-     * @param[out]  aDataset  Where to place the dataset.
+     * @param[out]  aDatasetTlvs  Where to place the dataset.
      *
      * @retval kErrorNone      Successfully retrieved the dataset.
      * @retval kErrorNotFound  There is no corresponding dataset stored in non-volatile memory.
      *
      */
-    Error Read(otOperationalDatasetTlvs &aDataset) const { return mLocal.Read(aDataset); }
+    Error Read(Dataset::Tlvs &aDatasetTlvs) const { return mLocal.Read(aDatasetTlvs); }
 
     /**
      * Retrieves the channel mask from local dataset.
@@ -183,12 +183,6 @@
 
 protected:
     /**
-     * Default Delay Timer value for a Pending Operational Dataset (ms)
-     *
-     */
-    static constexpr uint32_t kDefaultDelayTimer = OPENTHREAD_CONFIG_TMF_PENDING_DATASET_DEFAULT_DELAY;
-
-    /**
      * Defines a generic Dataset TLV to read from a message.
      *
      */
@@ -261,13 +255,13 @@
     /**
      * Saves the Operational Dataset in non-volatile memory.
      *
-     * @param[in]  aDataset  The Operational Dataset.
+     * @param[in]  aDatasetTlvs  The Operational Dataset as `Dataset::Tlvs`.
      *
      * @retval kErrorNone             Successfully saved the dataset.
      * @retval kErrorNotImplemented   The platform does not implement settings functionality.
      *
      */
-    Error Save(const otOperationalDatasetTlvs &aDataset);
+    Error Save(const Dataset::Tlvs &aDatasetTlvs);
 
     /**
      * Sets the Operational Dataset for the partition.
@@ -339,6 +333,15 @@
     bool         mTimestampValid : 1;
 
 private:
+    static constexpr uint8_t kMaxGetTypes = 64; // Max number of types in MGMT_GET.req
+
+    class TlvList : public Array<uint8_t, kMaxGetTypes>
+    {
+    public:
+        TlvList(void) = default;
+        void Add(uint8_t aTlvType);
+    };
+
     static void HandleMgmtSetResponse(void                *aContext,
                                       otMessage           *aMessage,
                                       const otMessageInfo *aMessageInfo,
@@ -353,8 +356,7 @@
     void  SendSet(void);
     void  SendGetResponse(const Coap::Message    &aRequest,
                           const Ip6::MessageInfo &aMessageInfo,
-                          uint8_t                *aTlvs,
-                          uint8_t                 aLength) const;
+                          const TlvList          &aTlvList) const;
 
 #if OPENTHREAD_FTD
     void SendSetResponse(const Coap::Message &aRequest, const Ip6::MessageInfo &aMessageInfo, StateTlv::State aState);
@@ -459,13 +461,13 @@
     /**
      * Sets the Operational Dataset in non-volatile memory.
      *
-     * @param[in]  aDataset  The Operational Dataset.
+     * @param[in]  aDatasetTlvs  The Operational Dataset as `Dataset::Tlvs`.
      *
      * @retval kErrorNone            Successfully saved the dataset.
      * @retval kErrorNotImplemented  The platform does not implement settings functionality.
      *
      */
-    Error Save(const otOperationalDatasetTlvs &aDataset) { return DatasetManager::Save(aDataset); }
+    Error Save(const Dataset::Tlvs &aDatasetTlvs) { return DatasetManager::Save(aDatasetTlvs); }
 
 #if OPENTHREAD_FTD
 
@@ -558,13 +560,13 @@
      *
      * Also starts the Delay Timer.
      *
-     * @param[in]  aDataset  The Operational Dataset.
+     * @param[in]  aDatasetTlvs  The Operational Dataset as a sequence of TLVs.
      *
      * @retval kErrorNone            Successfully saved the dataset.
      * @retval kErrorNotImplemented  The platform does not implement settings functionality.
      *
      */
-    Error Save(const otOperationalDatasetTlvs &aDataset);
+    Error Save(const Dataset::Tlvs &aDatasetTlvs);
 
     /**
      * Sets the Operational Dataset for the partition.
diff --git a/src/core/meshcop/dataset_manager_ftd.cpp b/src/core/meshcop/dataset_manager_ftd.cpp
index 5f74842..f845408 100644
--- a/src/core/meshcop/dataset_manager_ftd.cpp
+++ b/src/core/meshcop/dataset_manager_ftd.cpp
@@ -64,11 +64,18 @@
 
 Error DatasetManager::AppendMleDatasetTlv(Message &aMessage) const
 {
-    Dataset dataset;
+    Mle::Tlv::Type mleTlvType = IsActiveDataset() ? Mle::Tlv::kActiveDataset : Mle::Tlv::kPendingDataset;
+    Dataset        dataset;
 
     IgnoreError(Read(dataset));
 
-    return dataset.AppendMleDatasetTlv(GetType(), aMessage);
+    // Remove the Active or Pending Timestamp TLV from Dataset before
+    // appending to the message. The timestamp is appended as its own
+    // MLE TLV to the message.
+
+    dataset.RemoveTlv(IsActiveDataset() ? Tlv::kActiveTimestamp : Tlv::kPendingTimestamp);
+
+    return Tlv::AppendTlv(aMessage, mleTlvType, dataset.GetBytes(), static_cast<uint8_t>(dataset.GetSize()));
 }
 
 Error DatasetManager::HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
@@ -104,7 +111,7 @@
     // verify the request includes a timestamp that is ahead of the locally stored value
     SuccessOrExit(Tlv::Find<ActiveTimestampTlv>(aMessage, activeTimestamp));
 
-    if (GetType() == Dataset::kPending)
+    if (IsPendingDataset())
     {
         Timestamp pendingTimestamp;
 
@@ -155,7 +162,7 @@
     }
 
     // check active timestamp rollback
-    if (GetType() == Dataset::kPending && (!hasNetworkKey || !doesAffectNetworkKey))
+    if (IsPendingDataset() && (!hasNetworkKey || !doesAffectNetworkKey))
     {
         // no change to network key, active timestamp must be ahead
         const Timestamp *localActiveTimestamp = Get<ActiveDatasetManager>().GetTimestamp();
@@ -175,7 +182,7 @@
     }
 
     // verify an MGMT_ACTIVE_SET.req from a Commissioner does not affect connectivity
-    VerifyOrExit(!isUpdateFromCommissioner || GetType() == Dataset::kPending || !doesAffectConnectivity);
+    VerifyOrExit(!isUpdateFromCommissioner || IsPendingDataset() || !doesAffectConnectivity);
 
     if (isUpdateFromCommissioner)
     {
@@ -184,7 +191,7 @@
         IgnoreError(Get<ActiveDatasetManager>().Read(dataset));
     }
 
-    if (GetType() == Dataset::kPending || !doesAffectConnectivity)
+    if (IsPendingDataset() || !doesAffectConnectivity)
     {
         offset = aMessage.GetOffset();
 
@@ -202,11 +209,11 @@
 
             case Tlv::kDelayTimer:
             {
-                uint32_t delayTimer = datasetTlv.ReadValueAs<DelayTimerTlv>();
+                uint32_t delayTimer = Min(datasetTlv.ReadValueAs<DelayTimerTlv>(), DelayTimerTlv::kMaxDelay);
 
-                if (doesAffectNetworkKey && delayTimer < kDefaultDelayTimer)
+                if (doesAffectNetworkKey && delayTimer < DelayTimerTlv::kDefaultDelay)
                 {
-                    delayTimer = kDefaultDelayTimer;
+                    delayTimer = DelayTimerTlv::kDefaultDelay;
                 }
                 else
                 {
@@ -284,7 +291,7 @@
     SuccessOrExit(error = aMessage.Read(aOffset, this, sizeof(Tlv)));
     VerifyOrExit(GetLength() <= Dataset::kMaxValueSize, error = kErrorParse);
     SuccessOrExit(error = aMessage.Read(aOffset + sizeof(Tlv), mValue, GetLength()));
-    VerifyOrExit(Tlv::IsValid(*this), error = kErrorParse);
+    VerifyOrExit(Dataset::IsTlvValid(*this), error = kErrorParse);
 
 exit:
     return error;
@@ -436,7 +443,7 @@
 
     IgnoreError(dataset.Write<DelayTimerTlv>(Get<Leader>().GetDelayTimerMinimal()));
 
-    dataset.SetTimestamp(Dataset::kPending, aTimestamp);
+    IgnoreError(dataset.Write<PendingTimestampTlv>(aTimestamp));
     IgnoreError(DatasetManager::Save(dataset));
 
     StartDelayTimer();
diff --git a/src/core/meshcop/dataset_updater.cpp b/src/core/meshcop/dataset_updater.cpp
index c4b1145..a6a3dce 100644
--- a/src/core/meshcop/dataset_updater.cpp
+++ b/src/core/meshcop/dataset_updater.cpp
@@ -61,7 +61,7 @@
     VerifyOrExit(!Get<Mle::Mle>().IsDisabled(), error = kErrorInvalidState);
     VerifyOrExit(mDataset == nullptr, error = kErrorBusy);
 
-    VerifyOrExit(!aDataset.IsActiveTimestampPresent() && !aDataset.IsPendingTimestampPresent(),
+    VerifyOrExit(!aDataset.IsPresent<Dataset::kActiveTimestamp>() && !aDataset.IsPresent<Dataset::kPendingTimestamp>(),
                  error = kErrorInvalidArgs);
 
     message = Get<MessagePool>().Allocate(Message::kTypeOther);
@@ -119,7 +119,7 @@
 
     IgnoreError(dataset.SetFrom(requestedDataset));
 
-    if (!requestedDataset.IsDelayPresent())
+    if (!requestedDataset.IsPresent<Dataset::kDelay>())
     {
         uint32_t delay = kDefaultDelay;
 
@@ -139,14 +139,14 @@
         }
 
         timestamp.AdvanceRandomTicks();
-        dataset.SetTimestamp(Dataset::kPending, timestamp);
+        IgnoreError(dataset.Write<PendingTimestampTlv>(timestamp));
     }
 
     {
         Timestamp timestamp = dataset.FindTlv(Tlv::kActiveTimestamp)->ReadValueAs<ActiveTimestampTlv>();
 
         timestamp.AdvanceRandomTicks();
-        dataset.SetTimestamp(Dataset::kActive, timestamp);
+        IgnoreError(dataset.Write<ActiveTimestampTlv>(timestamp));
     }
 
     SuccessOrExit(error = Get<PendingDatasetManager>().Save(dataset));
@@ -190,8 +190,8 @@
             Timestamp requestedDatasetTimestamp;
             Timestamp activeDatasetTimestamp;
 
-            requestedDataset.GetActiveTimestamp(requestedDatasetTimestamp);
-            dataset.GetActiveTimestamp(activeDatasetTimestamp);
+            requestedDataset.Get<MeshCoP::Dataset::kActiveTimestamp>(requestedDatasetTimestamp);
+            dataset.Get<MeshCoP::Dataset::kActiveTimestamp>(activeDatasetTimestamp);
             if (Timestamp::Compare(requestedDatasetTimestamp, activeDatasetTimestamp) <= 0)
             {
                 Finish(kErrorAlready);
diff --git a/src/core/meshcop/joiner.cpp b/src/core/meshcop/joiner.cpp
index fb0f3e6..f8fb30e 100644
--- a/src/core/meshcop/joiner.cpp
+++ b/src/core/meshcop/joiner.cpp
@@ -185,7 +185,7 @@
         FreeJoinerFinalizeMessage();
     }
 
-    LogError("start joiner", error);
+    LogWarnOnError(error, "start joiner");
     return error;
 }
 
@@ -378,7 +378,7 @@
     SetState(kStateConnect);
 
 exit:
-    LogError("start secure joiner connection", error);
+    LogWarnOnError(error, "start secure joiner connection");
     return error;
 }
 
@@ -528,10 +528,10 @@
 
     datasetInfo.Clear();
 
-    SuccessOrExit(error = Tlv::Find<NetworkKeyTlv>(aMessage, datasetInfo.UpdateNetworkKey()));
+    SuccessOrExit(error = Tlv::Find<NetworkKeyTlv>(aMessage, datasetInfo.Update<Dataset::kNetworkKey>()));
 
-    datasetInfo.SetChannel(Get<Mac::Mac>().GetPanChannel());
-    datasetInfo.SetPanId(Get<Mac::Mac>().GetPanId());
+    datasetInfo.Set<Dataset::kChannel>(Get<Mac::Mac>().GetPanChannel());
+    datasetInfo.Set<Dataset::kPanId>(Get<Mac::Mac>().GetPanId());
 
     IgnoreError(Get<ActiveDatasetManager>().Save(datasetInfo));
 
@@ -543,7 +543,7 @@
     mTimer.Start(kConfigExtAddressDelay);
 
 exit:
-    LogError("process joiner entrust", error);
+    LogWarnOnError(error, "process joiner entrust");
 }
 
 void Joiner::SendJoinerEntrustResponse(const Coap::Message &aRequest, const Ip6::MessageInfo &aRequestInfo)
diff --git a/src/core/meshcop/joiner_router.cpp b/src/core/meshcop/joiner_router.cpp
index acadf1a..a5df413 100644
--- a/src/core/meshcop/joiner_router.cpp
+++ b/src/core/meshcop/joiner_router.cpp
@@ -232,7 +232,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("schedule joiner entrust", error);
+    LogWarnOnError(error, "schedule joiner entrust");
 }
 
 void JoinerRouter::HandleTimer(void) { SendDelayedJoinerEntrust(); }
diff --git a/src/core/meshcop/meshcop.cpp b/src/core/meshcop/meshcop.cpp
index b30fe27..8bce745 100644
--- a/src/core/meshcop/meshcop.cpp
+++ b/src/core/meshcop/meshcop.cpp
@@ -343,15 +343,5 @@
 }
 #endif // OPENTHREAD_FTD
 
-#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
-void LogError(const char *aActionText, Error aError)
-{
-    if (aError != kErrorNone && aError != kErrorAlready)
-    {
-        LogWarn("Failed to %s: %s", aActionText, ErrorToString(aError));
-    }
-}
-#endif
-
 } // namespace MeshCoP
 } // namespace ot
diff --git a/src/core/meshcop/meshcop.hpp b/src/core/meshcop/meshcop.hpp
index 339872b..3620927 100644
--- a/src/core/meshcop/meshcop.hpp
+++ b/src/core/meshcop/meshcop.hpp
@@ -561,22 +561,6 @@
  */
 void ComputeJoinerId(const Mac::ExtAddress &aEui64, Mac::ExtAddress &aJoinerId);
 
-#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
-/**
- * Emits a log message indicating an error during a MeshCoP action.
- *
- * Note that log message is emitted only if there is an error, i.e. @p aError is not `kErrorNone`. The log
- * message will have the format "Failed to {aActionText} : {ErrorString}".
- *
- * @param[in] aActionText   A string representing the failed action.
- * @param[in] aError        The error in sending the message.
- *
- */
-void LogError(const char *aActionText, Error aError);
-#else
-inline void LogError(const char *, Error) {}
-#endif
-
 } // namespace MeshCoP
 
 DefineCoreType(otJoinerPskd, MeshCoP::JoinerPskd);
diff --git a/src/core/meshcop/meshcop_leader.cpp b/src/core/meshcop/meshcop_leader.cpp
index 64c028d..9844147 100644
--- a/src/core/meshcop/meshcop_leader.cpp
+++ b/src/core/meshcop/meshcop_leader.cpp
@@ -58,7 +58,7 @@
 Leader::Leader(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mTimer(aInstance)
-    , mDelayTimerMinimal(kMinDelayTimer)
+    , mDelayTimerMinimal(DelayTimerTlv::kMinDelay)
     , mSessionId(Random::NonCrypto::GetUint16())
 {
 }
@@ -125,7 +125,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send petition response", error);
+    LogWarnOnError(error, "send petition response");
 }
 
 template <> void Leader::HandleTmf<kUriLeaderKeepAlive>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
@@ -192,7 +192,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send keep alive response", error);
+    LogWarnOnError(error, "send keep alive response");
 }
 
 void Leader::SendDatasetChanged(const Ip6::Address &aAddress)
@@ -211,14 +211,14 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send dataset changed", error);
+    LogWarnOnError(error, "send dataset changed");
 }
 
 Error Leader::SetDelayTimerMinimal(uint32_t aDelayTimerMinimal)
 {
     Error error = kErrorNone;
 
-    VerifyOrExit((aDelayTimerMinimal != 0 && aDelayTimerMinimal < kMinDelayTimer), error = kErrorInvalidArgs);
+    VerifyOrExit((aDelayTimerMinimal != 0 && aDelayTimerMinimal < DelayTimerTlv::kMinDelay), error = kErrorInvalidArgs);
     mDelayTimerMinimal = aDelayTimerMinimal;
 
 exit:
diff --git a/src/core/meshcop/meshcop_leader.hpp b/src/core/meshcop/meshcop_leader.hpp
index 50436b3..54fe7e9 100644
--- a/src/core/meshcop/meshcop_leader.hpp
+++ b/src/core/meshcop/meshcop_leader.hpp
@@ -104,7 +104,6 @@
     void SetEmptyCommissionerData(void);
 
 private:
-    static constexpr uint32_t kMinDelayTimer         = OPENTHREAD_CONFIG_TMF_PENDING_DATASET_MINIMUM_DELAY; // (msec)
     static constexpr uint32_t kTimeoutLeaderPetition = 50; // TIMEOUT_LEAD_PET (seconds)
 
     OT_TOOL_PACKED_BEGIN
diff --git a/src/core/meshcop/meshcop_tlvs.cpp b/src/core/meshcop/meshcop_tlvs.cpp
index 151abcd..c4ea22d 100644
--- a/src/core/meshcop/meshcop_tlvs.cpp
+++ b/src/core/meshcop/meshcop_tlvs.cpp
@@ -43,57 +43,6 @@
 namespace ot {
 namespace MeshCoP {
 
-bool Tlv::IsValid(const Tlv &aTlv)
-{
-    bool    isValid   = true;
-    uint8_t minLength = 0;
-
-    switch (aTlv.GetType())
-    {
-    case Tlv::kPanId:
-        minLength = sizeof(PanIdTlv::UintValueType);
-        break;
-    case Tlv::kExtendedPanId:
-        minLength = sizeof(ExtendedPanIdTlv::ValueType);
-        break;
-    case Tlv::kPskc:
-        minLength = sizeof(PskcTlv::ValueType);
-        break;
-    case Tlv::kNetworkKey:
-        minLength = sizeof(NetworkKeyTlv::ValueType);
-        break;
-    case Tlv::kMeshLocalPrefix:
-        minLength = sizeof(MeshLocalPrefixTlv::ValueType);
-        break;
-    case Tlv::kChannel:
-        VerifyOrExit(aTlv.GetLength() >= sizeof(ChannelTlvValue), isValid = false);
-        isValid = aTlv.ReadValueAs<ChannelTlv>().IsValid();
-        break;
-    case Tlv::kNetworkName:
-        isValid = As<NetworkNameTlv>(aTlv).IsValid();
-        break;
-
-    case Tlv::kSecurityPolicy:
-        isValid = As<SecurityPolicyTlv>(aTlv).IsValid();
-        break;
-
-    case Tlv::kChannelMask:
-        isValid = As<ChannelMaskTlv>(aTlv).IsValid();
-        break;
-
-    default:
-        break;
-    }
-
-    if (minLength > 0)
-    {
-        isValid = (aTlv.GetLength() >= minLength);
-    }
-
-exit:
-    return isValid;
-}
-
 NameData NetworkNameTlv::GetNetworkName(void) const
 {
     uint8_t len = GetLength();
@@ -158,6 +107,23 @@
     return aState == kReject ? kStateStrings[2] : kStateStrings[aState];
 }
 
+uint32_t DelayTimerTlv::CalculateRemainingDelay(const Tlv &aDelayTimerTlv, TimeMilli aUpdateTime)
+{
+    uint32_t delay   = Min(aDelayTimerTlv.ReadValueAs<DelayTimerTlv>(), kMaxDelay);
+    uint32_t elapsed = TimerMilli::GetNow() - aUpdateTime;
+
+    if (delay > elapsed)
+    {
+        delay -= elapsed;
+    }
+    else
+    {
+        delay = 0;
+    }
+
+    return delay;
+}
+
 bool ChannelMaskTlv::IsValid(void) const
 {
     uint32_t channelMask;
diff --git a/src/core/meshcop/meshcop_tlvs.hpp b/src/core/meshcop/meshcop_tlvs.hpp
index 2f2b111..084f84d 100644
--- a/src/core/meshcop/meshcop_tlvs.hpp
+++ b/src/core/meshcop/meshcop_tlvs.hpp
@@ -160,16 +160,6 @@
      */
     const Tlv *GetNext(void) const { return As<Tlv>(ot::Tlv::GetNext()); }
 
-    /**
-     * Indicates whether a TLV appears to be well-formed.
-     *
-     * @param[in]  aTlv  A reference to the TLV.
-     *
-     * @returns TRUE if the TLV appears to be well-formed, FALSE otherwise.
-     *
-     */
-    static bool IsValid(const Tlv &aTlv);
-
 } OT_TOOL_PACKED_END;
 
 /**
@@ -611,7 +601,48 @@
  * Defines Delay Timer TLV constants and types.
  *
  */
-typedef UintTlvInfo<Tlv::kDelayTimer, uint32_t> DelayTimerTlv;
+class DelayTimerTlv : public UintTlvInfo<Tlv::kDelayTimer, uint32_t>
+{
+public:
+    /**
+     * Minimum Delay Timer value (in msec).
+     *
+     */
+    static constexpr uint32_t kMinDelay = OPENTHREAD_CONFIG_TMF_PENDING_DATASET_MINIMUM_DELAY;
+
+    /**
+     * Maximum Delay Timer value (in msec).
+     *
+     */
+    static constexpr uint32_t kMaxDelay = (72 * Time::kOneHourInMsec);
+
+    /**
+     * Default Delay Timer value (in msec).
+     *
+     */
+    static constexpr uint32_t kDefaultDelay = OPENTHREAD_CONFIG_TMF_PENDING_DATASET_DEFAULT_DELAY;
+
+    /**
+     * Calculates the remaining delay in milliseconds, based on the value read from a Delay Timer TLV and the specified
+     * update time.
+     *
+     * Ensures that the calculated delay does not exceed `kMaxDelay`. Also accounts for time already elapsed since
+     * @p aUpdateTime.
+     *
+     * Caller MUST ensure that @p aDelayTimerTlv is a Delay Timer TLV, otherwise behavior is undefined.
+     *
+     * @param[in] aDelayTimerTlv   The delay timer TLV to read delay from.
+     * @param[in] aUpdateTimer     The update time of the Dataset.
+     *
+     * @return The remaining delay (in msec).
+     *
+     */
+    static uint32_t CalculateRemainingDelay(const Tlv &aDelayTimerTlv, TimeMilli aUpdateTime);
+
+    static_assert(kMinDelay <= kMaxDelay, "TMF_PENDING_DATASET_MINIMUM_DELAY is larger than max allowed");
+    static_assert(kDefaultDelay <= kMaxDelay, "TMF_PENDING_DATASET_DEFAULT_DELAY is larger than max allowed");
+    static_assert(kDefaultDelay >= kMinDelay, "TMF_PENDING_DATASET_DEFAULT_DELAY is smaller than min allowed");
+};
 
 /**
  * Implements Channel Mask TLV generation and parsing.
diff --git a/src/core/meshcop/tcat_agent.cpp b/src/core/meshcop/tcat_agent.cpp
index f8df773..325aa9a 100644
--- a/src/core/meshcop/tcat_agent.cpp
+++ b/src/core/meshcop/tcat_agent.cpp
@@ -94,7 +94,7 @@
     mAlreadyCommissioned        = false;
 
 exit:
-    LogError("start TCAT agent", error);
+    LogWarnOnError(error, "start TCAT agent");
     return error;
 }
 
@@ -224,14 +224,14 @@
 
         if (datasetError == kErrorNone)
         {
-            if (datasetInfo.IsNetworkNamePresent() && mCommissionerHasNetworkName &&
-                (datasetInfo.GetNetworkName() == mCommissionerNetworkName))
+            if (datasetInfo.IsPresent<Dataset::kNetworkName>() && mCommissionerHasNetworkName &&
+                (datasetInfo.Get<Dataset::kNetworkName>() == mCommissionerNetworkName))
             {
                 networkNamesMatch = true;
             }
 
-            if (datasetInfo.IsExtendedPanIdPresent() && mCommissionerHasExtendedPanId &&
-                (datasetInfo.GetExtendedPanId() == mCommissionerExtendedPanId))
+            if (datasetInfo.IsPresent<Dataset::kExtendedPanId>() && mCommissionerHasExtendedPanId &&
+                (datasetInfo.Get<Dataset::kExtendedPanId>() == mCommissionerExtendedPanId))
             {
                 extendedPanIdsMatch = true;
             }
@@ -461,9 +461,9 @@
 
 Error TcatAgent::HandleSetActiveOperationalDataset(const Message &aIncommingMessage, uint16_t aOffset, uint16_t aLength)
 {
-    Dataset                  dataset;
-    otOperationalDatasetTlvs datasetTlvs;
-    Error                    error;
+    Dataset       dataset;
+    Dataset::Tlvs datasetTlvs;
+    Error         error;
 
     SuccessOrExit(error = dataset.ReadFromMessage(aIncommingMessage, aOffset, aLength));
 
@@ -487,7 +487,7 @@
     Dataset::Info datasetInfo;
 
     VerifyOrExit(Get<ActiveDatasetManager>().Read(datasetInfo) == kErrorNone, error = kErrorInvalidState);
-    VerifyOrExit(datasetInfo.IsNetworkKeyPresent(), error = kErrorInvalidState);
+    VerifyOrExit(datasetInfo.IsPresent<Dataset::kNetworkKey>(), error = kErrorInvalidState);
 
 #if OPENTHREAD_CONFIG_LINK_RAW_ENABLE
     VerifyOrExit(!Get<Mac::LinkRaw>().IsEnabled(), error = kErrorInvalidState);
@@ -500,16 +500,6 @@
     return error;
 }
 
-#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
-void TcatAgent::LogError(const char *aActionText, Error aError)
-{
-    if (aError != kErrorNone)
-    {
-        LogWarn("Failed to %s: %s", aActionText, ErrorToString(aError));
-    }
-}
-#endif
-
 } // namespace MeshCoP
 } // namespace ot
 
diff --git a/src/core/meshcop/tcat_agent.hpp b/src/core/meshcop/tcat_agent.hpp
index d2dd9f3..16cf8f9 100644
--- a/src/core/meshcop/tcat_agent.hpp
+++ b/src/core/meshcop/tcat_agent.hpp
@@ -325,12 +325,6 @@
     Error HandleSetActiveOperationalDataset(const Message &aIncommingMessage, uint16_t aOffset, uint16_t aLength);
     Error HandleStartThreadInterface(void);
 
-#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
-    void LogError(const char *aActionText, Error aError);
-#else
-    void LogError(const char *, Error) {}
-#endif
-
     bool         CheckCommandClassAuthorizationFlags(CommandClassFlags aCommissionerCommandClassFlags,
                                                      CommandClassFlags aDeviceCommandClassFlags,
                                                      Dataset          *aDataset) const;
diff --git a/src/core/net/dhcp6_client.cpp b/src/core/net/dhcp6_client.cpp
index e206ff6..56654fb 100644
--- a/src/core/net/dhcp6_client.cpp
+++ b/src/core/net/dhcp6_client.cpp
@@ -288,7 +288,7 @@
     if (error != kErrorNone)
     {
         FreeMessage(message);
-        LogWarn("Failed to send DHCPv6 Solicit: %s", ErrorToString(error));
+        LogWarnOnError(error, "send DHCPv6 Solicit");
     }
 }
 
diff --git a/src/core/net/dhcp6_server.cpp b/src/core/net/dhcp6_server.cpp
index 7fd072c..ea95707 100644
--- a/src/core/net/dhcp6_server.cpp
+++ b/src/core/net/dhcp6_server.cpp
@@ -172,11 +172,8 @@
     mPrefixAgentsCount++;
 
 exit:
-
-    if (error != kErrorNone)
-    {
-        LogNote("Failed to add DHCPv6 prefix agent: %s", ErrorToString(error));
-    }
+    LogWarnOnError(error, "add DHCPv6 prefix agent");
+    OT_UNUSED_VARIABLE(error);
 }
 
 void Server::HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
diff --git a/src/core/net/dns_types.cpp b/src/core/net/dns_types.cpp
index eecc9ff..b2df69a 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/num_utils.hpp"
+#include "common/numeric_limits.hpp"
 #include "common/random.hpp"
 #include "common/string.hpp"
 #include "instance/instance.hpp"
@@ -731,7 +732,11 @@
     nameLength -= (suffixLength + 1);
     VerifyOrExit(nameLength < aLabelsSize, error = kErrorNoBufs);
 
-    memcpy(aLabels, aName, nameLength);
+    if (aLabels != aName)
+    {
+        memmove(aLabels, aName, nameLength);
+    }
+
     aLabels[nameLength] = kNullChar;
     error               = kErrorNone;
 
@@ -1343,5 +1348,35 @@
     return valid;
 }
 
+void NsecRecord::TypeBitMap::AddType(uint16_t aType)
+{
+    if ((aType >> 8) == mBlockNumber)
+    {
+        uint8_t  type  = static_cast<uint8_t>(aType & 0xff);
+        uint8_t  index = (type / kBitsPerByte);
+        uint16_t mask  = (0x80 >> (type % kBitsPerByte));
+
+        mBitmaps[index] |= mask;
+        mBitmapLength = Max<uint8_t>(mBitmapLength, index + 1);
+    }
+}
+
+bool NsecRecord::TypeBitMap::ContainsType(uint16_t aType) const
+{
+    bool     contains = false;
+    uint8_t  type     = static_cast<uint8_t>(aType & 0xff);
+    uint8_t  index    = (type / kBitsPerByte);
+    uint16_t mask     = (0x80 >> (type % kBitsPerByte));
+
+    VerifyOrExit((aType >> 8) == mBlockNumber);
+
+    VerifyOrExit(index < mBitmapLength);
+
+    contains = (mBitmaps[index] & mask);
+
+exit:
+    return contains;
+}
+
 } // namespace Dns
 } // namespace ot
diff --git a/src/core/net/dns_types.hpp b/src/core/net/dns_types.hpp
index e590a48..1083ee5 100644
--- a/src/core/net/dns_types.hpp
+++ b/src/core/net/dns_types.hpp
@@ -1028,6 +1028,9 @@
      * Both @p aName and @p aSuffixName MUST follow the same style regarding inclusion of trailing dot ('.'). Otherwise
      * `kErrorParse` is returned.
      *
+     * The @p aLabels buffer may be the same as @p aName for in-place label extraction. In this case, the
+     * implementation avoids unnecessary character copies.
+     *
      * @param[in]   aName           The name to extract labels from.
      * @param[in]   aSuffixName     The suffix name (e.g., can be domain name).
      * @param[out]  aLabels         Pointer to buffer to copy the extracted labels.
@@ -1047,6 +1050,9 @@
      * Both @p aName and @p aSuffixName MUST follow the same style regarding inclusion of trailing dot ('.'). Otherwise
      * `kErrorParse` is returned.
      *
+     * The @p aLabels buffer may be the same as @p aName for in-place label extraction. In this case, the
+     * implementation avoids unnecessary character copies.
+     *
      * @tparam      kLabelsBufferSize   Size of the buffer string.
      *
      * @param[in]   aName           The name to extract labels from.
@@ -1065,6 +1071,28 @@
     }
 
     /**
+     * Strips a given suffix name (e.g., a domain name) from a given DNS name string, updating it in place.
+     *
+     * First checks that @p Name ends with the given @p aSuffixName, otherwise `kErrorParse` is returned.
+     *
+     * Both @p aName and @p aSuffixName MUST follow the same style regarding inclusion of trailing dot ('.'). Otherwise
+     * `kErrorParse` is returned.
+     *
+     * @tparam kNameBufferSize     The size of name buffer.
+     *
+     * @param[in]  aName           The name buffer to strip the @p aSuffixName from.
+     * @param[in]  aSuffixName     The suffix name (e.g., can be domain name).
+     *
+     * @retval kErrorNone          Successfully stripped the suffix name from @p aName.
+     * @retval kErrorParse         @p aName does not contain @p aSuffixName.
+     *
+     */
+    template <uint16_t kNameBufferSize> static Error StripName(char (&aName)[kNameBufferSize], const char *aSuffixName)
+    {
+        return ExtractLabels(aName, aSuffixName, aName, kNameBufferSize);
+    }
+
+    /**
      * Tests if a DNS name is a sub-domain of a given domain.
      *
      * Both @p aName and @p aDomain can end without dot ('.').
@@ -1336,6 +1364,7 @@
     static constexpr uint16_t kTypeAaaa  = 28;  ///< IPv6 address record.
     static constexpr uint16_t kTypeSrv   = 33;  ///< SRV locator record.
     static constexpr uint16_t kTypeOpt   = 41;  ///< Option record.
+    static constexpr uint16_t kTypeNsec  = 47;  ///< NSEC record.
     static constexpr uint16_t kTypeAny   = 255; ///< ANY record.
 
     // Resource Record Class Codes.
@@ -2746,6 +2775,104 @@
 } OT_TOOL_PACKED_END;
 
 /**
+ * Implements body format of NSEC record (RFC 3845) for use with mDNS.
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+class NsecRecord : public ResourceRecord
+{
+public:
+    static constexpr uint16_t kType = kTypeNsec; ///< The NSEC record type.
+
+    /**
+     * Represents NSEC Type Bit Map field (RFC 3845 - section 2.1.2)
+     *
+     */
+    OT_TOOL_PACKED_BEGIN
+    class TypeBitMap : public Clearable<TypeBitMap>
+    {
+    public:
+        static constexpr uint8_t kMinSize = 2; ///< Minimum size of a valid `TypeBitMap` (with zero length).
+
+        static constexpr uint8_t kMaxLength = 32; ///< Maximum BitmapLength value.
+
+        /**
+         * Gets the Window Block Number
+         *
+         * @returns The Window Block Number.
+         *
+         */
+        uint8_t GetBlockNumber(void) const { return mBlockNumber; }
+
+        /**
+         * Sets the Window Block Number
+         *
+         * @param[in] aBlockNumber The Window Block Number.
+         *
+         */
+        void SetBlockNumber(uint8_t aBlockNumber) { mBlockNumber = aBlockNumber; }
+
+        /**
+         * Gets the Bitmap length
+         *
+         * @returns The Bitmap length
+         *
+         */
+        uint8_t GetBitmapLength(void) { return mBitmapLength; }
+
+        /**
+         * Gets the total size (number of bytes) of the `TypeBitMap` field.
+         *
+         * @returns The size of the `TypeBitMap`
+         *
+         */
+        uint16_t GetSize(void) const { return (sizeof(mBlockNumber) + sizeof(mBitmapLength) + mBitmapLength); }
+
+        /**
+         * Adds a resource record type to the Bitmap.
+         *
+         * As the types are added to the Bitmap the Bitmap length gets updated accordingly.
+         *
+         * The type space is split into 256 window blocks, each representing the low-order 8 bits of the 16-bit type
+         * value. If @p aType does not match the currently set Window Block Number, no action is performed.
+         *
+         * @param[in] aType   The resource record type to add.
+         *
+         */
+        void AddType(uint16_t aType);
+
+        /**
+         * Indicates whether a given resource record type is present in the Bitmap.
+         *
+         * If @p aType does not match the currently set Window Block Number, this method returns `false`..
+         *
+         * @param[in] aType   The resource record type to check.
+         *
+         * @retval TRUE   The @p aType is present in the Bitmap.
+         * @retval FALSE  The @p aType is not present in the Bitmap.
+         *
+         */
+        bool ContainsType(uint16_t aType) const;
+
+    private:
+        uint8_t mBlockNumber;
+        uint8_t mBitmapLength;
+        uint8_t mBitmaps[kMaxLength];
+    } OT_TOOL_PACKED_END;
+
+    /**
+     * Initializes the NSEC Resource Record by setting its type and class.
+     *
+     * Other record fields (TTL, length remain unchanged/uninitialized.
+     *
+     * @param[in] aClass  The class of the resource record (default is `kClassInternet`).
+     *
+     */
+    void Init(uint16_t aClass = kClassInternet) { ResourceRecord::Init(kTypeNsec, aClass); }
+
+} OT_TOOL_PACKED_END;
+
+/**
  * Implements Question format.
  *
  */
diff --git a/src/core/net/dnssd.cpp b/src/core/net/dnssd.cpp
index c795fa8..5e068b9 100644
--- a/src/core/net/dnssd.cpp
+++ b/src/core/net/dnssd.cpp
@@ -28,12 +28,12 @@
 
 /**
  * @file
- *   This file implements infrastructure DNS-SD (mDNS) platform APIs.
+ *   This file implements infrastructure DNS-SD module.
  */
 
 #include "dnssd.hpp"
 
-#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
 
 #include "common/code_utils.hpp"
 #include "common/locator_getters.hpp"
@@ -88,58 +88,184 @@
 
 Dnssd::Dnssd(Instance &aInstance)
     : InstanceLocator(aInstance)
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    , mUseNativeMdns(true)
+#endif
 {
 }
 
-Dnssd::State Dnssd::GetState(void) const { return MapEnum(otPlatDnssdGetState(&GetInstance())); }
+Dnssd::State Dnssd::GetState(void) const
+{
+    State state;
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    {
+        state = Get<Dns::Multicast::Core>().IsEnabled() ? kReady : kStopped;
+        ExitNow();
+    }
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+    state = MapEnum(otPlatDnssdGetState(&GetInstance()));
+    ExitNow();
+#endif
+
+exit:
+    return state;
+}
 
 void Dnssd::RegisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback)
 {
-    if (IsReady())
+    VerifyOrExit(IsReady());
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     {
-        otPlatDnssdRegisterService(&GetInstance(), &aService, aRequestId, aCallback);
+        IgnoreError(Get<Dns::Multicast::Core>().RegisterService(aService, aRequestId, aCallback));
+        ExitNow();
     }
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+    otPlatDnssdRegisterService(&GetInstance(), &aService, aRequestId, aCallback);
+#endif
+
+exit:
+    return;
 }
 
 void Dnssd::UnregisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback)
 {
-    if (IsReady())
+    VerifyOrExit(IsReady());
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     {
-        otPlatDnssdUnregisterService(&GetInstance(), &aService, aRequestId, aCallback);
+        IgnoreError(Get<Dns::Multicast::Core>().UnregisterService(aService));
+        VerifyOrExit(aCallback != nullptr);
+        aCallback(&GetInstance(), aRequestId, kErrorNone);
+        ExitNow();
     }
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+    otPlatDnssdUnregisterService(&GetInstance(), &aService, aRequestId, aCallback);
+#endif
+
+exit:
+    return;
 }
 
 void Dnssd::RegisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback)
 {
-    if (IsReady())
+    VerifyOrExit(IsReady());
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     {
-        otPlatDnssdRegisterHost(&GetInstance(), &aHost, aRequestId, aCallback);
+        IgnoreError(Get<Dns::Multicast::Core>().RegisterHost(aHost, aRequestId, aCallback));
+        ExitNow();
     }
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+    otPlatDnssdRegisterHost(&GetInstance(), &aHost, aRequestId, aCallback);
+#endif
+
+exit:
+    return;
 }
 
 void Dnssd::UnregisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback)
 {
-    if (IsReady())
+    VerifyOrExit(IsReady());
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     {
-        otPlatDnssdUnregisterHost(&GetInstance(), &aHost, aRequestId, aCallback);
+        IgnoreError(Get<Dns::Multicast::Core>().UnregisterHost(aHost));
+        VerifyOrExit(aCallback != nullptr);
+        aCallback(&GetInstance(), aRequestId, kErrorNone);
+        ExitNow();
     }
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+    otPlatDnssdUnregisterHost(&GetInstance(), &aHost, aRequestId, aCallback);
+#endif
+
+exit:
+    return;
 }
 
 void Dnssd::RegisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback)
 {
-    if (IsReady())
+    VerifyOrExit(IsReady());
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     {
-        otPlatDnssdRegisterKey(&GetInstance(), &aKey, aRequestId, aCallback);
+        IgnoreError(Get<Dns::Multicast::Core>().RegisterKey(aKey, aRequestId, aCallback));
+        ExitNow();
     }
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+    otPlatDnssdRegisterKey(&GetInstance(), &aKey, aRequestId, aCallback);
+#endif
+
+exit:
+    return;
 }
 
 void Dnssd::UnregisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback)
 {
-    if (IsReady())
+    VerifyOrExit(IsReady());
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
     {
-        otPlatDnssdUnregisterKey(&GetInstance(), &aKey, aRequestId, aCallback);
+        IgnoreError(Get<Dns::Multicast::Core>().UnregisterKey(aKey));
+        VerifyOrExit(aCallback != nullptr);
+        aCallback(&GetInstance(), aRequestId, kErrorNone);
+        ExitNow();
+    }
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+    otPlatDnssdUnregisterKey(&GetInstance(), &aKey, aRequestId, aCallback);
+#endif
+
+exit:
+    return;
+}
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+void Dnssd::HandleMdnsCoreStateChange(void)
+{
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    if (mUseNativeMdns)
+#endif
+    {
+        HandleStateChange();
     }
 }
+#endif
 
 void Dnssd::HandleStateChange(void)
 {
@@ -155,4 +281,4 @@
 
 } // namespace ot
 
-#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
diff --git a/src/core/net/dnssd.hpp b/src/core/net/dnssd.hpp
index dc6f6b6..9bf19aa 100644
--- a/src/core/net/dnssd.hpp
+++ b/src/core/net/dnssd.hpp
@@ -28,7 +28,7 @@
 
 /**
  * @file
- *   This file includes definitions for infrastructure DNS-SD (mDNS) platform.
+ *   This file includes definitions for DNS-SD module.
  */
 
 #ifndef DNSSD_HPP_
@@ -36,7 +36,17 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+#if !OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE && OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+#error "Must enable either `PLATFORM_DNSSD_ENABLE` or `MULTICAST_DNS_ENABLE` and not both."
+#endif
+#else
+#if !OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || !OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+#error "`PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION` requires both `PLATFORM_DNSSD_ENABLE` or `MULTICAST_DNS_ENABLE`.".
+#endif
+#endif // !OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
 
 #include <openthread/platform/dnssd.h>
 
@@ -51,7 +61,10 @@
  * @addtogroup core-dns
  *
  * @brief
- *   This module includes definitions for DNS-SD (mDNS) platform.
+ *   This module includes definitions for DNS-SD (mDNS) APIs used by other modules in OT (e.g. advertising proxy).
+ *
+ *   The DNS-SD is implemented either using the native mDNS module in OpenThread or using `otPlatDnssd` platform
+ *   APIs (delegating the DNS-SD implementation to platform layer).
  *
  * @{
  *
@@ -60,7 +73,7 @@
 extern "C" void otPlatDnssdStateHandleStateChange(otInstance *aInstance);
 
 /**
- * Represents DNS-SD (mDNS) platform.
+ * Represents DNS-SD module.
  *
  */
 class Dnssd : public InstanceLocator, private NonCopyable
@@ -261,8 +274,41 @@
      */
     void UnregisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback);
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    /**
+     * Handles native mDNS state change.
+     *
+     * This is used to notify `Dnssd` when `Multicast::Dns::Core` gets enabled or disabled.
+     *
+     */
+    void HandleMdnsCoreStateChange(void);
+#endif
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    /**
+     * Selects whether to use the native mDNS or the platform `otPlatDnssd` APIs.
+     *
+     * @param[in] aUseMdns    TRUE to use the native mDNS module, FALSE to use platform APIs.
+     *
+     */
+    void SetUseNativeMdns(bool aUseMdns) { mUseNativeMdns = aUseMdns; }
+
+    /**
+     * Indicates whether the `Dnssd` is using the native mDNS or the platform `otPlatDnssd` APIs.
+     *
+     * @retval TRUE    `Dnssd` is using the native mDSN module.
+     * @retval FALSE   `Dnssd` is using the platform `otPlatDnssd` APIs.
+     *
+     */
+    bool ShouldUseNativeMdns(void) const { return mUseNativeMdns; }
+#endif
+
 private:
     void HandleStateChange(void);
+
+#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
+    bool mUseNativeMdns;
+#endif
 };
 
 /**
@@ -277,6 +323,6 @@
 
 } // namespace ot
 
-#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
+#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
 
 #endif // DNSSD_HPP_
diff --git a/src/core/net/dnssd_server.cpp b/src/core/net/dnssd_server.cpp
index 385eb88..1bfc9f1 100644
--- a/src/core/net/dnssd_server.cpp
+++ b/src/core/net/dnssd_server.cpp
@@ -170,7 +170,7 @@
             ExitNow();
         }
 
-        LogWarn("Error forwarding to upstream: %s", ErrorToString(error));
+        LogWarnOnError(error, "forwarding to upstream");
 
         rcode = Header::kResponseServerFailure;
 
@@ -666,11 +666,6 @@
     }
 }
 
-uint8_t Server::GetNameLength(const char *aName)
-{
-    return static_cast<uint8_t>(StringLength(aName, Name::kMaxNameLength));
-}
-
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 void Server::Response::Log(void) const
 {
@@ -974,10 +969,7 @@
     SuccessOrAssert(aQuery.Read(aQuery.GetLength() - sizeof(ProxyQueryInfo), *this));
 }
 
-void Server::ProxyQueryInfo::RemoveFrom(ProxyQuery &aQuery) const
-{
-    SuccessOrAssert(aQuery.SetLength(aQuery.GetLength() - sizeof(ProxyQueryInfo)));
-}
+void Server::ProxyQueryInfo::RemoveFrom(ProxyQuery &aQuery) const { aQuery.RemoveFooter(sizeof(ProxyQueryInfo)); }
 
 void Server::ProxyQueryInfo::UpdateIn(ProxyQuery &aQuery) const
 {
diff --git a/src/core/net/dnssd_server.hpp b/src/core/net/dnssd_server.hpp
index 3b6b679..4afde1d 100644
--- a/src/core/net/dnssd_server.hpp
+++ b/src/core/net/dnssd_server.hpp
@@ -397,11 +397,10 @@
         NameOffsets      mOffsets;
     };
 
-    bool           IsRunning(void) const { return mSocket.IsBound(); }
-    static void    HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void           HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    void           ProcessQuery(Request &aRequest);
-    static uint8_t GetNameLength(const char *aName);
+    bool        IsRunning(void) const { return mSocket.IsBound(); }
+    static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
+    void        HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    void        ProcessQuery(Request &aRequest);
 
     void        ResolveByProxy(Response &aResponse, const Ip6::MessageInfo &aMessageInfo);
     void        RemoveQueryAndPrepareResponse(ProxyQuery &aQuery, const ProxyQueryInfo &aInfo, Response &aResponse);
diff --git a/src/core/net/icmp6.cpp b/src/core/net/icmp6.cpp
index 5bcea06..3dba7ca 100644
--- a/src/core/net/icmp6.cpp
+++ b/src/core/net/icmp6.cpp
@@ -171,6 +171,9 @@
     case OT_ICMP6_ECHO_HANDLER_ALL:
         rval = true;
         break;
+    case OT_ICMP6_ECHO_HANDLER_RLOC_ALOC_ONLY:
+        rval = aMessageInfo.GetSockAddr().GetIid().IsLocator();
+        break;
     }
 
     return rval;
@@ -184,8 +187,7 @@
     MessageInfo replyMessageInfo;
     uint16_t    dataOffset;
 
-    // always handle Echo Request destined for RLOC or ALOC
-    VerifyOrExit(ShouldHandleEchoRequest(aMessageInfo) || aMessageInfo.GetSockAddr().GetIid().IsLocator());
+    VerifyOrExit(ShouldHandleEchoRequest(aMessageInfo));
 
     LogInfo("Received Echo Request");
 
diff --git a/src/core/net/ip6.cpp b/src/core/net/ip6.cpp
index 3a6472a..d5b2e27 100644
--- a/src/core/net/ip6.cpp
+++ b/src/core/net/ip6.cpp
@@ -616,7 +616,7 @@
     return error;
 }
 
-Error Ip6::HandleFragment(Message &aMessage, MessageInfo &aMessageInfo)
+Error Ip6::HandleFragment(Message &aMessage)
 {
     Error          error = kErrorNone;
     Header         header, headerBuffer;
@@ -703,8 +703,7 @@
 
         mReassemblyList.Dequeue(*message);
 
-        IgnoreError(HandleDatagram(OwnedPtr<Message>(message), aMessageInfo.mLinkInfo,
-                                   /* aIsReassembled */ true));
+        IgnoreError(HandleDatagram(OwnedPtr<Message>(message), /* aIsReassembled */ true));
     }
 
 exit:
@@ -715,7 +714,7 @@
             mReassemblyList.DequeueAndFree(*message);
         }
 
-        LogWarn("Reassembly failed: %s", ErrorToString(error));
+        LogWarnOnError(error, "reassemble");
     }
 
     if (isFragmented)
@@ -766,16 +765,12 @@
     messageInfo.SetPeerAddr(header.GetSource());
     messageInfo.SetSockAddr(header.GetDestination());
     messageInfo.SetHopLimit(header.GetHopLimit());
-    messageInfo.SetLinkInfo(nullptr);
 
     error = mIcmp.SendError(aIcmpType, aIcmpCode, messageInfo, aMessage);
 
 exit:
-
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to send ICMP error: %s", ErrorToString(error));
-    }
+    LogWarnOnError(error, "send ICMP");
+    OT_UNUSED_VARIABLE(error);
 }
 
 #else
@@ -788,10 +783,8 @@
     return kErrorNone;
 }
 
-Error Ip6::HandleFragment(Message &aMessage, MessageInfo &aMessageInfo)
+Error Ip6::HandleFragment(Message &aMessage)
 {
-    OT_UNUSED_VARIABLE(aMessageInfo);
-
     Error          error = kErrorNone;
     FragmentHeader fragmentHeader;
 
@@ -829,7 +822,7 @@
         case kProtoFragment:
             IgnoreError(PassToHost(aMessagePtr, aMessageInfo, aNextHeader,
                                    /* aApplyFilter */ false, aReceive, Message::kCopyToUse));
-            SuccessOrExit(error = HandleFragment(*aMessagePtr, aMessageInfo));
+            SuccessOrExit(error = HandleFragment(*aMessagePtr));
             break;
 
         case kProtoDstOpts:
@@ -920,11 +913,7 @@
     }
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogNote("Failed to handle payload: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "handle payload");
     return error;
 }
 
@@ -959,17 +948,6 @@
 
     if (mIsReceiveIp6FilterEnabled && aApplyFilter)
     {
-#if !OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
-        // Do not pass messages sent to an RLOC/ALOC, except
-        // Service Locator
-
-        bool isLocator = Get<Mle::Mle>().IsMeshLocalAddress(aMessageInfo.GetSockAddr()) &&
-                         aMessageInfo.GetSockAddr().GetIid().IsLocator();
-
-        VerifyOrExit(!isLocator || aMessageInfo.GetSockAddr().GetIid().IsAnycastServiceLocator(),
-                     error = kErrorNoRoute);
-#endif
-
         switch (aIpProto)
         {
         case kProtoIcmp6:
@@ -1037,6 +1015,19 @@
     }
 #endif
 
+#if OPENTHREAD_CONFIG_IP6_RESTRICT_FORWARDING_LARGER_SCOPE_MCAST_WITH_LOCAL_SRC
+    // Some platforms (e.g. Android) currently doesn't restrict link-local/mesh-local source
+    // addresses when forwarding multicast packets.
+    // For a multicast packet sent from link-local/mesh-local address to scope larger
+    // than realm-local, set the hop limit to 1 before sending to host, so this packet
+    // will not be forwarded by host.
+    if (aMessageInfo.GetSockAddr().IsMulticastLargerThanRealmLocal() &&
+        (aMessageInfo.GetPeerAddr().IsLinkLocal() || (Get<Mle::Mle>().IsMeshLocalAddress(aMessageInfo.GetPeerAddr()))))
+    {
+        messagePtr->Write<uint8_t>(Header::kHopLimitFieldOffset, 1);
+    }
+#endif
+
     // Pass message to callback transferring its ownership.
     mReceiveIp6DatagramCallback.Invoke(messagePtr.Release());
 
@@ -1081,7 +1072,7 @@
     return error;
 }
 
-Error Ip6::HandleDatagram(OwnedPtr<Message> aMessagePtr, const void *aLinkMessageInfo, bool aIsReassembled)
+Error Ip6::HandleDatagram(OwnedPtr<Message> aMessagePtr, bool aIsReassembled)
 {
     Error       error;
     MessageInfo messageInfo;
@@ -1102,7 +1093,6 @@
     messageInfo.SetSockAddr(header.GetDestination());
     messageInfo.SetHopLimit(header.GetHopLimit());
     messageInfo.SetEcn(header.GetEcn());
-    messageInfo.SetLinkInfo(aLinkMessageInfo);
 
     // Determine `forwardThread`, `forwardHost` and `receive`
     // based on the destination address.
@@ -1190,7 +1180,7 @@
 
         Get<MeshForwarder>().LogMessage(MeshForwarder::kMessageReceive, *messagePtr);
 
-        IgnoreError(HandleDatagram(messagePtr.PassOwnership(), aLinkMessageInfo, aIsReassembled));
+        IgnoreError(HandleDatagram(messagePtr.PassOwnership(), aIsReassembled));
 
         receive     = false;
         forwardHost = false;
diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp
index 53330b3..de04eef 100644
--- a/src/core/net/ip6.hpp
+++ b/src/core/net/ip6.hpp
@@ -207,7 +207,6 @@
      * Processes a received IPv6 datagram.
      *
      * @param[in]  aMessage          An owned pointer to a message.
-     * @param[in]  aLinkMessageInfo  A pointer to link-specific message information.
      *
      * @retval kErrorNone     Successfully processed the message.
      * @retval kErrorDrop     Message was well-formed but not fully processed due to packet processing rules.
@@ -216,9 +215,7 @@
      * @retval kErrorParse    Encountered a malformed header when processing the message.
      *
      */
-    Error HandleDatagram(OwnedPtr<Message> aMessagePtr,
-                         const void       *aLinkMessageInfo = nullptr,
-                         bool              aIsReassembled   = false);
+    Error HandleDatagram(OwnedPtr<Message> aMessagePtr, bool aIsReassembled = false);
 
     /**
      * Registers a callback to provide received raw IPv6 datagrams.
@@ -378,7 +375,7 @@
                                  uint8_t           &aNextHeader,
                                  bool              &aReceive);
     Error FragmentDatagram(Message &aMessage, uint8_t aIpProto);
-    Error HandleFragment(Message &aMessage, MessageInfo &aMessageInfo);
+    Error HandleFragment(Message &aMessage);
 #if OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE
     void CleanupFragmentationBuffer(void);
     void HandleTimeTick(void);
diff --git a/src/core/net/ip6_mpl.cpp b/src/core/net/ip6_mpl.cpp
index 61be9ea..698c66f 100644
--- a/src/core/net/ip6_mpl.cpp
+++ b/src/core/net/ip6_mpl.cpp
@@ -465,10 +465,7 @@
     IgnoreError(aMessage.Read(length - sizeof(*this), *this));
 }
 
-void Mpl::Metadata::RemoveFrom(Message &aMessage) const
-{
-    SuccessOrAssert(aMessage.SetLength(aMessage.GetLength() - sizeof(*this)));
-}
+void Mpl::Metadata::RemoveFrom(Message &aMessage) const { aMessage.RemoveFooter(sizeof(*this)); }
 
 void Mpl::Metadata::UpdateIn(Message &aMessage) const { aMessage.Write(aMessage.GetLength() - sizeof(*this), *this); }
 
diff --git a/src/core/net/mdns.cpp b/src/core/net/mdns.cpp
new file mode 100644
index 0000000..ddf6995
--- /dev/null
+++ b/src/core/net/mdns.cpp
@@ -0,0 +1,6230 @@
+/*
+ *  Copyright (c) 2024, 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 "mdns.hpp"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+#include "common/code_utils.hpp"
+#include "common/locator_getters.hpp"
+#include "common/log.hpp"
+#include "common/numeric_limits.hpp"
+#include "common/type_traits.hpp"
+#include "instance/instance.hpp"
+
+/**
+ * @file
+ *   This file implements the Multicast DNS (mDNS) per RFC 6762.
+ */
+
+namespace ot {
+namespace Dns {
+namespace Multicast {
+
+RegisterLogModule("MulticastDns");
+
+//---------------------------------------------------------------------------------------------------------------------
+// otPlatMdns callbacks
+
+extern "C" void otPlatMdnsHandleReceive(otInstance                  *aInstance,
+                                        otMessage                   *aMessage,
+                                        bool                         aIsUnicast,
+                                        const otPlatMdnsAddressInfo *aAddress)
+{
+    AsCoreType(aInstance).Get<Core>().HandleMessage(AsCoreType(aMessage), aIsUnicast, AsCoreType(aAddress));
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core
+
+const char Core::kLocalDomain[]         = "local.";
+const char Core::kUdpServiceLabel[]     = "_udp";
+const char Core::kTcpServiceLabel[]     = "_tcp";
+const char Core::kSubServiceLabel[]     = "_sub";
+const char Core::kServicesDnssdLabels[] = "_services._dns-sd._udp";
+
+Core::Core(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mIsEnabled(false)
+    , mIsQuestionUnicastAllowed(kDefaultQuAllowed)
+    , mMaxMessageSize(kMaxMessageSize)
+    , mInfraIfIndex(0)
+    , mMultiPacketRxMessages(aInstance)
+    , mNextProbeTxTime(TimerMilli::GetNow() - 1)
+    , mEntryTimer(aInstance)
+    , mEntryTask(aInstance)
+    , mTxMessageHistory(aInstance)
+    , mConflictCallback(nullptr)
+    , mNextQueryTxTime(TimerMilli::GetNow() - 1)
+    , mCacheTimer(aInstance)
+    , mCacheTask(aInstance)
+{
+}
+
+Error Core::SetEnabled(bool aEnable, uint32_t aInfraIfIndex)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(aEnable != mIsEnabled, error = kErrorAlready);
+    SuccessOrExit(error = otPlatMdnsSetListeningEnabled(&GetInstance(), aEnable, aInfraIfIndex));
+
+    mIsEnabled    = aEnable;
+    mInfraIfIndex = aInfraIfIndex;
+
+    if (mIsEnabled)
+    {
+        LogInfo("Enabling on infra-if-index %lu", ToUlong(mInfraIfIndex));
+    }
+    else
+    {
+        LogInfo("Disabling");
+    }
+
+    if (!mIsEnabled)
+    {
+        mHostEntries.Clear();
+        mServiceEntries.Clear();
+        mServiceTypes.Clear();
+        mMultiPacketRxMessages.Clear();
+        mTxMessageHistory.Clear();
+        mEntryTimer.Stop();
+
+        mBrowseCacheList.Clear();
+        mSrvCacheList.Clear();
+        mTxtCacheList.Clear();
+        mIp6AddrCacheList.Clear();
+        mIp4AddrCacheList.Clear();
+        mCacheTimer.Stop();
+    }
+
+    Get<Dnssd>().HandleMdnsCoreStateChange();
+
+exit:
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF
+void Core::HandleInfraIfStateChanged(void)
+{
+    IgnoreError(SetEnabled(Get<BorderRouter::InfraIf>().IsRunning(), Get<BorderRouter::InfraIf>().GetIfIndex()));
+}
+#endif
+
+template <typename EntryType, typename ItemInfo>
+Error Core::Register(const ItemInfo &aItemInfo, RequestId aRequestId, RegisterCallback aCallback)
+{
+    Error      error = kErrorNone;
+    EntryType *entry;
+
+    VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
+
+    entry = GetEntryList<EntryType>().FindMatching(aItemInfo);
+
+    if (entry == nullptr)
+    {
+        entry = EntryType::AllocateAndInit(GetInstance(), aItemInfo);
+        OT_ASSERT(entry != nullptr);
+        GetEntryList<EntryType>().Push(*entry);
+    }
+
+    entry->Register(aItemInfo, Callback(aRequestId, aCallback));
+
+exit:
+    return error;
+}
+
+template <typename EntryType, typename ItemInfo> Error Core::Unregister(const ItemInfo &aItemInfo)
+{
+    Error      error = kErrorNone;
+    EntryType *entry;
+
+    VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
+
+    entry = GetEntryList<EntryType>().FindMatching(aItemInfo);
+
+    if (entry != nullptr)
+    {
+        entry->Unregister(aItemInfo);
+    }
+
+exit:
+    return error;
+}
+
+Error Core::RegisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback)
+{
+    return Register<HostEntry>(aHost, aRequestId, aCallback);
+}
+
+Error Core::UnregisterHost(const Host &aHost) { return Unregister<HostEntry>(aHost); }
+
+Error Core::RegisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback)
+{
+    return Register<ServiceEntry>(aService, aRequestId, aCallback);
+}
+
+Error Core::UnregisterService(const Service &aService) { return Unregister<ServiceEntry>(aService); }
+
+Error Core::RegisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback)
+{
+    return IsKeyForService(aKey) ? Register<ServiceEntry>(aKey, aRequestId, aCallback)
+                                 : Register<HostEntry>(aKey, aRequestId, aCallback);
+}
+
+Error Core::UnregisterKey(const Key &aKey)
+{
+    return IsKeyForService(aKey) ? Unregister<ServiceEntry>(aKey) : Unregister<HostEntry>(aKey);
+}
+
+Core::Iterator *Core::AllocateIterator(void) { return EntryIterator::Allocate(GetInstance()); }
+
+void Core::FreeIterator(Iterator &aIterator) { static_cast<EntryIterator &>(aIterator).Free(); }
+
+Error Core::GetNextHost(Iterator &aIterator, Host &aHost, EntryState &aState) const
+{
+    return static_cast<EntryIterator &>(aIterator).GetNextHost(aHost, aState);
+}
+
+Error Core::GetNextService(Iterator &aIterator, Service &aService, EntryState &aState) const
+{
+    return static_cast<EntryIterator &>(aIterator).GetNextService(aService, aState);
+}
+
+Error Core::GetNextKey(Iterator &aIterator, Key &aKey, EntryState &aState) const
+{
+    return static_cast<EntryIterator &>(aIterator).GetNextKey(aKey, aState);
+}
+
+void Core::InvokeConflictCallback(const char *aName, const char *aServiceType)
+{
+    if (mConflictCallback != nullptr)
+    {
+        mConflictCallback(&GetInstance(), aName, aServiceType);
+    }
+}
+void Core::HandleMessage(Message &aMessage, bool aIsUnicast, const AddressInfo &aSenderAddress)
+{
+    OwnedPtr<Message>   messagePtr(&aMessage);
+    OwnedPtr<RxMessage> rxMessagePtr;
+
+    VerifyOrExit(mIsEnabled);
+
+    rxMessagePtr.Reset(RxMessage::AllocateAndInit(GetInstance(), messagePtr, aIsUnicast, aSenderAddress));
+    VerifyOrExit(!rxMessagePtr.IsNull());
+
+    if (rxMessagePtr->IsQuery())
+    {
+        // Check if this is a continuation of a multi-packet query.
+        // Initial query message sets the "Truncated" flag.
+        // Subsequent messages from the same sender contain no
+        // question and only known-answer records.
+
+        if ((rxMessagePtr->GetRecordCounts().GetFor(kQuestionSection) == 0) &&
+            (rxMessagePtr->GetRecordCounts().GetFor(kAnswerSection) > 0))
+        {
+            mMultiPacketRxMessages.AddToExisting(rxMessagePtr);
+            ExitNow();
+        }
+
+        switch (rxMessagePtr->ProcessQuery(/* aShouldProcessTruncated */ false))
+        {
+        case RxMessage::kProcessed:
+            break;
+
+        case RxMessage::kSaveAsMultiPacket:
+            // This is a truncated multi-packet query and we can
+            // answer some questions in this query. We save it in
+            // `mMultiPacketRxMessages` list and defer its response
+            // for a random time waiting to receive next messages
+            // containing additional known-answer records.
+
+            mMultiPacketRxMessages.AddNew(rxMessagePtr);
+            break;
+        }
+    }
+    else
+    {
+        rxMessagePtr->ProcessResponse();
+    }
+
+exit:
+    return;
+}
+
+void Core::HandleEntryTimer(void)
+{
+    EntryTimerContext context(GetInstance());
+
+    // We process host entries before service entries. This order
+    // ensures we can determine whether host addresses have already
+    // been appended to the Answer section (when processing service entries),
+    // preventing duplicates.
+
+    for (HostEntry &entry : mHostEntries)
+    {
+        entry.HandleTimer(context);
+    }
+
+    for (ServiceEntry &entry : mServiceEntries)
+    {
+        entry.HandleTimer(context);
+    }
+
+    for (ServiceType &serviceType : mServiceTypes)
+    {
+        serviceType.HandleTimer(context);
+    }
+
+    context.GetProbeMessage().Send();
+    context.GetResponseMessage().Send();
+
+    RemoveEmptyEntries();
+
+    if (context.GetNextTime() != context.GetNow().GetDistantFuture())
+    {
+        mEntryTimer.FireAtIfEarlier(context.GetNextTime());
+    }
+}
+
+void Core::RemoveEmptyEntries(void)
+{
+    OwningList<HostEntry>    removedHosts;
+    OwningList<ServiceEntry> removedServices;
+
+    mHostEntries.RemoveAllMatching(Entry::kRemoving, removedHosts);
+    mServiceEntries.RemoveAllMatching(Entry::kRemoving, removedServices);
+}
+
+void Core::HandleEntryTask(void)
+{
+    // `mEntryTask` serves two purposes:
+    //
+    // Invoking callbacks: This ensures `Register()` calls will always
+    // return before invoking the callback, even when entry is
+    // already in `kRegistered` state and registration is immediately
+    // successful.
+    //
+    // Removing empty entries after `Unregister()` calls: This
+    // prevents modification of `mHostEntries` and `mServiceEntries`
+    // during callback execution while we are iterating over these
+    // lists. Allows us to safely call `Register()` or `Unregister()`
+    // from callbacks without iterator invalidation.
+
+    for (HostEntry &entry : mHostEntries)
+    {
+        entry.InvokeCallbacks();
+    }
+
+    for (ServiceEntry &entry : mServiceEntries)
+    {
+        entry.InvokeCallbacks();
+    }
+
+    RemoveEmptyEntries();
+}
+
+uint32_t Core::DetermineTtl(uint32_t aTtl, uint32_t aDefaultTtl)
+{
+    return (aTtl == kUnspecifiedTtl) ? aDefaultTtl : aTtl;
+}
+
+bool Core::NameMatch(const Heap::String &aHeapString, const char *aName)
+{
+    // Compares a DNS name given as a `Heap::String` with a
+    // `aName` C string.
+
+    return !aHeapString.IsNull() && StringMatch(aHeapString.AsCString(), aName, kStringCaseInsensitiveMatch);
+}
+
+bool Core::NameMatch(const Heap::String &aFirst, const Heap::String &aSecond)
+{
+    // Compares two DNS names given as `Heap::String`.
+
+    return !aSecond.IsNull() && NameMatch(aFirst, aSecond.AsCString());
+}
+
+void Core::UpdateCacheFlushFlagIn(ResourceRecord &aResourceRecord, Section aSection)
+{
+    // Do not set the cache-flush flag is the record is
+    // appended in Authority Section in a probe message.
+
+    if (aSection != kAuthoritySection)
+    {
+        aResourceRecord.SetClass(aResourceRecord.GetClass() | kClassCacheFlushFlag);
+    }
+}
+
+void Core::UpdateRecordLengthInMessage(ResourceRecord &aRecord, Message &aMessage, uint16_t aOffset)
+{
+    // Determines the records DATA length and updates it in a message.
+    // Should be called immediately after all the fields in the
+    // record are appended to the message. `aOffset` gives the offset
+    // in the message to the start of the record.
+
+    aRecord.SetLength(aMessage.GetLength() - aOffset - sizeof(ResourceRecord));
+    aMessage.Write(aOffset, aRecord);
+}
+
+void Core::UpdateCompressOffset(uint16_t &aOffset, uint16_t aNewOffset)
+{
+    if ((aOffset == kUnspecifiedOffset) && (aNewOffset != kUnspecifiedOffset))
+    {
+        aOffset = aNewOffset;
+    }
+}
+
+bool Core::QuestionMatches(uint16_t aQuestionRrType, uint16_t aRrType)
+{
+    return (aQuestionRrType == aRrType) || (aQuestionRrType == ResourceRecord::kTypeAny);
+}
+
+bool Core::RrClassIsInternetOrAny(uint16_t aRrClass)
+{
+    aRrClass &= kClassMask;
+
+    return (aRrClass == ResourceRecord::kClassInternet) || (aRrClass == ResourceRecord::kClassAny);
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::Callback
+
+Core::Callback::Callback(RequestId aRequestId, RegisterCallback aCallback)
+    : mRequestId(aRequestId)
+    , mCallback(aCallback)
+{
+}
+
+void Core::Callback::InvokeAndClear(Instance &aInstance, Error aError)
+{
+    if (mCallback != nullptr)
+    {
+        RegisterCallback callback  = mCallback;
+        RequestId        requestId = mRequestId;
+
+        Clear();
+
+        callback(&aInstance, requestId, aError);
+    }
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::RecordCounts
+
+void Core::RecordCounts::ReadFrom(const Header &aHeader)
+{
+    mCounts[kQuestionSection]       = aHeader.GetQuestionCount();
+    mCounts[kAnswerSection]         = aHeader.GetAnswerCount();
+    mCounts[kAuthoritySection]      = aHeader.GetAuthorityRecordCount();
+    mCounts[kAdditionalDataSection] = aHeader.GetAdditionalRecordCount();
+}
+
+void Core::RecordCounts::WriteTo(Header &aHeader) const
+{
+    aHeader.SetQuestionCount(mCounts[kQuestionSection]);
+    aHeader.SetAnswerCount(mCounts[kAnswerSection]);
+    aHeader.SetAuthorityRecordCount(mCounts[kAuthoritySection]);
+    aHeader.SetAdditionalRecordCount(mCounts[kAdditionalDataSection]);
+}
+
+bool Core::RecordCounts::IsEmpty(void) const
+{
+    // Indicates whether or not all counts are zero.
+
+    bool isEmpty = true;
+
+    for (uint16_t count : mCounts)
+    {
+        if (count != 0)
+        {
+            isEmpty = false;
+            break;
+        }
+    }
+
+    return isEmpty;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::AddressArray
+
+bool Core::AddressArray::Matches(const Ip6::Address *aAddresses, uint16_t aNumAddresses) const
+{
+    bool matches = false;
+
+    VerifyOrExit(aNumAddresses == GetLength());
+
+    for (uint16_t i = 0; i < aNumAddresses; i++)
+    {
+        VerifyOrExit(Contains(aAddresses[i]));
+    }
+
+    matches = true;
+
+exit:
+    return matches;
+}
+
+void Core::AddressArray::SetFrom(const Ip6::Address *aAddresses, uint16_t aNumAddresses)
+{
+    Free();
+    SuccessOrAssert(ReserveCapacity(aNumAddresses));
+
+    for (uint16_t i = 0; i < aNumAddresses; i++)
+    {
+        IgnoreError(PushBack(aAddresses[i]));
+    }
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::RecordInfo
+
+template <typename UintType> void Core::RecordInfo::UpdateProperty(UintType &aProperty, UintType aValue)
+{
+    // Updates a property variable associated with this record. The
+    // `aProperty` is updated if the record is empty (has no value
+    // yet) or if its current value differs from the new `aValue`. If
+    // the property is changed, we prepare the record to be announced.
+
+    // This template version works with `UintType` properties. There
+    // are similar overloads for `Heap::Data` and `Heap::String` and
+    // `AddressArray` property types below.
+
+    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`");
+
+    if (!mIsPresent || (aProperty != aValue))
+    {
+        mIsPresent = true;
+        aProperty  = aValue;
+        StartAnnouncing();
+    }
+}
+
+void Core::RecordInfo::UpdateProperty(Heap::String &aStringProperty, const char *aString)
+{
+    if (!mIsPresent || !NameMatch(aStringProperty, aString))
+    {
+        mIsPresent = true;
+        SuccessOrAssert(aStringProperty.Set(aString));
+        StartAnnouncing();
+    }
+}
+
+void Core::RecordInfo::UpdateProperty(Heap::Data &aDataProperty, const uint8_t *aData, uint16_t aLength)
+{
+    if (!mIsPresent || !aDataProperty.Matches(aData, aLength))
+    {
+        mIsPresent = true;
+        SuccessOrAssert(aDataProperty.SetFrom(aData, aLength));
+        StartAnnouncing();
+    }
+}
+
+void Core::RecordInfo::UpdateProperty(AddressArray &aAddrProperty, const Ip6::Address *aAddrs, uint16_t aNumAddrs)
+{
+    if (!mIsPresent || !aAddrProperty.Matches(aAddrs, aNumAddrs))
+    {
+        mIsPresent = true;
+        aAddrProperty.SetFrom(aAddrs, aNumAddrs);
+        StartAnnouncing();
+    }
+}
+
+void Core::RecordInfo::UpdateTtl(uint32_t aTtl) { return UpdateProperty(mTtl, aTtl); }
+
+void Core::RecordInfo::StartAnnouncing(void)
+{
+    if (mIsPresent)
+    {
+        mAnnounceCounter = 0;
+        mAnnounceTime    = TimerMilli::GetNow();
+    }
+}
+
+bool Core::RecordInfo::CanAnswer(void) const { return (mIsPresent && (mTtl > 0)); }
+
+void Core::RecordInfo::ScheduleAnswer(const AnswerInfo &aInfo)
+{
+    VerifyOrExit(CanAnswer());
+
+    if (aInfo.mUnicastResponse)
+    {
+        mUnicastAnswerPending = true;
+        ExitNow();
+    }
+
+    if (!aInfo.mIsProbe)
+    {
+        // Rate-limiting multicasts to prevent excessive packet flooding
+        // (RFC 6762 section 6): We enforce a minimum interval of one
+        // second (`kMinIntervalBetweenMulticast`) between multicast
+        // transmissions of the same record. Skip the new request if the
+        // answer time is too close to the last multicast time. A querier
+        // that did not receive and cache the previous transmission will
+        // retry its request.
+
+        VerifyOrExit(GetDurationSinceLastMulticast(aInfo.mAnswerTime) >= kMinIntervalBetweenMulticast);
+    }
+
+    if (mMulticastAnswerPending)
+    {
+        VerifyOrExit(aInfo.mAnswerTime < mAnswerTime);
+    }
+
+    mMulticastAnswerPending = true;
+    mAnswerTime             = aInfo.mAnswerTime;
+
+exit:
+    return;
+}
+
+bool Core::RecordInfo::ShouldAppendTo(TxMessage &aResponse, TimeMilli aNow) const
+{
+    bool shouldAppend = false;
+
+    VerifyOrExit(mIsPresent);
+
+    switch (aResponse.GetType())
+    {
+    case TxMessage::kMulticastResponse:
+
+        if ((mAnnounceCounter < kNumberOfAnnounces) && (mAnnounceTime <= aNow))
+        {
+            shouldAppend = true;
+            ExitNow();
+        }
+
+        shouldAppend = mMulticastAnswerPending && (mAnswerTime <= aNow);
+        break;
+
+    case TxMessage::kUnicastResponse:
+        shouldAppend = mUnicastAnswerPending;
+        break;
+
+    default:
+        break;
+    }
+
+exit:
+    return shouldAppend;
+}
+
+void Core::RecordInfo::UpdateStateAfterAnswer(const TxMessage &aResponse)
+{
+    // Updates the state after a unicast or multicast response is
+    // prepared containing the record in the Answer section.
+
+    VerifyOrExit(mIsPresent);
+
+    switch (aResponse.GetType())
+    {
+    case TxMessage::kMulticastResponse:
+        VerifyOrExit(mAppendState == kAppendedInMulticastMsg);
+        VerifyOrExit(mAppendSection == kAnswerSection);
+
+        mMulticastAnswerPending = false;
+
+        if (mAnnounceCounter < kNumberOfAnnounces)
+        {
+            mAnnounceCounter++;
+
+            if (mAnnounceCounter < kNumberOfAnnounces)
+            {
+                uint32_t delay = (1U << (mAnnounceCounter - 1)) * kAnnounceInterval;
+
+                mAnnounceTime = TimerMilli::GetNow() + delay;
+            }
+            else if (mTtl == 0)
+            {
+                // We are done announcing the removed record with zero TTL.
+                mIsPresent = false;
+            }
+        }
+
+        break;
+
+    case TxMessage::kUnicastResponse:
+        VerifyOrExit(IsAppended());
+        VerifyOrExit(mAppendSection == kAnswerSection);
+        mUnicastAnswerPending = false;
+        break;
+
+    default:
+        break;
+    }
+
+exit:
+    return;
+}
+
+void Core::RecordInfo::UpdateFireTimeOn(FireTime &aFireTime)
+{
+    VerifyOrExit(mIsPresent);
+
+    if (mAnnounceCounter < kNumberOfAnnounces)
+    {
+        aFireTime.SetFireTime(mAnnounceTime);
+    }
+
+    if (mMulticastAnswerPending)
+    {
+        aFireTime.SetFireTime(mAnswerTime);
+    }
+
+    if (mIsLastMulticastValid)
+    {
+        // `mLastMulticastTime` tracks the timestamp of the last
+        // multicast of this record. To handle potential 32-bit
+        // `TimeMilli` rollover, an aging mechanism is implemented.
+        // If the record isn't multicast again within a given age
+        // interval `kLastMulticastTimeAge`, `mIsLastMulticastValid`
+        // is cleared, indicating outdated multicast information.
+
+        TimeMilli lastMulticastAgeTime = mLastMulticastTime + kLastMulticastTimeAge;
+
+        if (lastMulticastAgeTime <= TimerMilli::GetNow())
+        {
+            mIsLastMulticastValid = false;
+        }
+        else
+        {
+            aFireTime.SetFireTime(lastMulticastAgeTime);
+        }
+    }
+
+exit:
+    return;
+}
+
+void Core::RecordInfo::MarkAsAppended(TxMessage &aTxMessage, Section aSection)
+{
+    mAppendSection = aSection;
+
+    switch (aTxMessage.GetType())
+    {
+    case TxMessage::kMulticastResponse:
+    case TxMessage::kMulticastProbe:
+
+        mAppendState = kAppendedInMulticastMsg;
+
+        if ((aSection == kAnswerSection) || (aSection == kAdditionalDataSection))
+        {
+            mLastMulticastTime    = TimerMilli::GetNow();
+            mIsLastMulticastValid = true;
+        }
+
+        break;
+
+    case TxMessage::kUnicastResponse:
+        mAppendState = kAppendedInUnicastMsg;
+        break;
+
+    case TxMessage::kMulticastQuery:
+        break;
+    }
+}
+
+void Core::RecordInfo::MarkToAppendInAdditionalData(void)
+{
+    if (mAppendState == kNotAppended)
+    {
+        mAppendState = kToAppendInAdditionalData;
+    }
+}
+
+bool Core::RecordInfo::IsAppended(void) const
+{
+    bool isAppended = false;
+
+    switch (mAppendState)
+    {
+    case kNotAppended:
+    case kToAppendInAdditionalData:
+        break;
+    case kAppendedInMulticastMsg:
+    case kAppendedInUnicastMsg:
+        isAppended = true;
+        break;
+    }
+
+    return isAppended;
+}
+
+bool Core::RecordInfo::CanAppend(void) const { return mIsPresent && !IsAppended(); }
+
+Error Core::RecordInfo::GetLastMulticastTime(TimeMilli &aLastMulticastTime) const
+{
+    Error error = kErrorNotFound;
+
+    VerifyOrExit(mIsPresent && mIsLastMulticastValid);
+    aLastMulticastTime = mLastMulticastTime;
+
+exit:
+    return error;
+}
+
+uint32_t Core::RecordInfo::GetDurationSinceLastMulticast(TimeMilli aTime) const
+{
+    uint32_t duration = NumericLimits<uint32_t>::kMax;
+
+    VerifyOrExit(mIsPresent && mIsLastMulticastValid);
+    VerifyOrExit(aTime > mLastMulticastTime, duration = 0);
+    duration = aTime - mLastMulticastTime;
+
+exit:
+    return duration;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::FireTime
+
+void Core::FireTime::SetFireTime(TimeMilli aFireTime)
+{
+    if (mHasFireTime)
+    {
+        VerifyOrExit(aFireTime < mFireTime);
+    }
+
+    mFireTime    = aFireTime;
+    mHasFireTime = true;
+
+exit:
+    return;
+}
+
+void Core::FireTime::ScheduleFireTimeOn(TimerMilli &aTimer)
+{
+    if (mHasFireTime)
+    {
+        aTimer.FireAtIfEarlier(mFireTime);
+    }
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::Entry
+
+Core::Entry::Entry(void)
+    : mState(kProbing)
+    , mProbeCount(0)
+    , mMulticastNsecPending(false)
+    , mUnicastNsecPending(false)
+    , mAppendedNsec(false)
+{
+}
+
+void Core::Entry::Init(Instance &aInstance)
+{
+    // Initializes a newly allocated entry (host or service)
+    // and starts it in `kProbing` state.
+
+    InstanceLocatorInit::Init(aInstance);
+    StartProbing();
+}
+
+void Core::Entry::SetState(State aState)
+{
+    mState = aState;
+    ScheduleCallbackTask();
+}
+
+void Core::Entry::Register(const Key &aKey, const Callback &aCallback)
+{
+    if (GetState() == kRemoving)
+    {
+        StartProbing();
+    }
+
+    mKeyRecord.UpdateTtl(DetermineTtl(aKey.mTtl, kDefaultKeyTtl));
+    mKeyRecord.UpdateProperty(mKeyData, aKey.mKeyData, aKey.mKeyDataLength);
+
+    mKeyCallback = aCallback;
+    ScheduleCallbackTask();
+}
+
+void Core::Entry::Unregister(const Key &aKey)
+{
+    OT_UNUSED_VARIABLE(aKey);
+
+    VerifyOrExit(mKeyRecord.IsPresent());
+
+    mKeyCallback.Clear();
+
+    switch (GetState())
+    {
+    case kRegistered:
+        mKeyRecord.UpdateTtl(0);
+        break;
+
+    case kProbing:
+    case kConflict:
+        ClearKey();
+        break;
+
+    case kRemoving:
+        break;
+    }
+
+exit:
+    return;
+}
+
+void Core::Entry::ClearKey(void)
+{
+    mKeyRecord.Clear();
+    mKeyData.Free();
+}
+
+void Core::Entry::SetCallback(const Callback &aCallback)
+{
+    mCallback = aCallback;
+    ScheduleCallbackTask();
+}
+
+void Core::Entry::ScheduleCallbackTask(void)
+{
+    switch (GetState())
+    {
+    case kRegistered:
+    case kConflict:
+        VerifyOrExit(!mCallback.IsEmpty() || !mKeyCallback.IsEmpty());
+        Get<Core>().mEntryTask.Post();
+        break;
+
+    case kProbing:
+    case kRemoving:
+        break;
+    }
+
+exit:
+    return;
+}
+
+void Core::Entry::InvokeCallbacks(void)
+{
+    Error error = kErrorNone;
+
+    switch (GetState())
+    {
+    case kConflict:
+        error = kErrorDuplicated;
+        OT_FALL_THROUGH;
+
+    case kRegistered:
+        mKeyCallback.InvokeAndClear(GetInstance(), error);
+        mCallback.InvokeAndClear(GetInstance(), error);
+        break;
+
+    case kProbing:
+    case kRemoving:
+        break;
+    }
+}
+
+void Core::Entry::StartProbing(void)
+{
+    SetState(kProbing);
+    mProbeCount = 0;
+    SetFireTime(Get<Core>().RandomizeFirstProbeTxTime());
+    ScheduleTimer();
+}
+
+void Core::Entry::SetStateToConflict(void)
+{
+    switch (GetState())
+    {
+    case kProbing:
+    case kRegistered:
+        SetState(kConflict);
+        break;
+    case kConflict:
+    case kRemoving:
+        break;
+    }
+}
+
+void Core::Entry::SetStateToRemoving(void)
+{
+    VerifyOrExit(GetState() != kRemoving);
+    SetState(kRemoving);
+
+exit:
+    return;
+}
+
+void Core::Entry::ClearAppendState(void)
+{
+    mKeyRecord.MarkAsNotAppended();
+    mAppendedNsec = false;
+}
+
+void Core::Entry::UpdateRecordsState(const TxMessage &aResponse)
+{
+    mKeyRecord.UpdateStateAfterAnswer(aResponse);
+
+    if (mAppendedNsec)
+    {
+        switch (aResponse.GetType())
+        {
+        case TxMessage::kMulticastResponse:
+            mMulticastNsecPending = false;
+            break;
+        case TxMessage::kUnicastResponse:
+            mUnicastNsecPending = false;
+            break;
+        default:
+            break;
+        }
+    }
+}
+
+void Core::Entry::ScheduleNsecAnswer(const AnswerInfo &aInfo)
+{
+    // Schedules NSEC record to be included in a response message.
+    // Used to answer to query for a record that is not present.
+
+    VerifyOrExit(GetState() == kRegistered);
+
+    if (aInfo.mUnicastResponse)
+    {
+        mUnicastNsecPending = true;
+    }
+    else
+    {
+        if (mMulticastNsecPending)
+        {
+            VerifyOrExit(aInfo.mAnswerTime < mNsecAnswerTime);
+        }
+
+        mMulticastNsecPending = true;
+        mNsecAnswerTime       = aInfo.mAnswerTime;
+    }
+
+exit:
+    return;
+}
+
+bool Core::Entry::ShouldAnswerNsec(TimeMilli aNow) const { return mMulticastNsecPending && (mNsecAnswerTime <= aNow); }
+
+void Core::Entry::AnswerNonProbe(const AnswerInfo &aInfo, RecordAndType *aRecords, uint16_t aRecordsLength)
+{
+    // Schedule answers for all matching records in `aRecords` array
+    // to a given non-probe question.
+
+    bool allEmptyOrZeroTtl = true;
+    bool answerNsec        = true;
+
+    for (uint16_t index = 0; index < aRecordsLength; index++)
+    {
+        RecordInfo &record = aRecords[index].mRecord;
+
+        if (!record.CanAnswer())
+        {
+            // Cannot answer if record is not present or has zero TTL.
+            continue;
+        }
+
+        allEmptyOrZeroTtl = false;
+
+        if (QuestionMatches(aInfo.mQuestionRrType, aRecords[index].mType))
+        {
+            answerNsec = false;
+            record.ScheduleAnswer(aInfo);
+        }
+    }
+
+    // If all records are removed or have zero TTL (we are still
+    // sending "Goodbye" announces), we should not provide any answer
+    // even NSEC.
+
+    if (!allEmptyOrZeroTtl && answerNsec)
+    {
+        ScheduleNsecAnswer(aInfo);
+    }
+}
+
+void Core::Entry::AnswerProbe(const AnswerInfo &aInfo, RecordAndType *aRecords, uint16_t aRecordsLength)
+{
+    bool       allEmptyOrZeroTtl = true;
+    bool       shouldDelay       = false;
+    TimeMilli  now               = TimerMilli::GetNow();
+    AnswerInfo info              = aInfo;
+
+    info.mAnswerTime = now;
+
+    OT_ASSERT(info.mIsProbe);
+
+    for (uint16_t index = 0; index < aRecordsLength; index++)
+    {
+        RecordInfo &record = aRecords[index].mRecord;
+        TimeMilli   lastMulticastTime;
+
+        if (!record.CanAnswer())
+        {
+            continue;
+        }
+
+        allEmptyOrZeroTtl = false;
+
+        if (!info.mUnicastResponse)
+        {
+            // Rate limiting multicast probe responses
+            //
+            // We delay the response if all records were multicast
+            // recently within an interval `kMinIntervalProbeResponse`
+            // (250 msec).
+
+            if (record.GetDurationSinceLastMulticast(now) >= kMinIntervalProbeResponse)
+            {
+                shouldDelay = false;
+            }
+            else if (record.GetLastMulticastTime(lastMulticastTime) == kErrorNone)
+            {
+                info.mAnswerTime = Max(info.mAnswerTime, lastMulticastTime + kMinIntervalProbeResponse);
+            }
+        }
+    }
+
+    if (allEmptyOrZeroTtl)
+    {
+        // All records are removed or being removed.
+
+        // Enhancement for future: If someone is probing for
+        // our name, we can stop announcement of removed records
+        // to let the new probe requester take over the name.
+
+        ExitNow();
+    }
+
+    if (!shouldDelay)
+    {
+        info.mAnswerTime = now;
+    }
+
+    for (uint16_t index = 0; index < aRecordsLength; index++)
+    {
+        aRecords[index].mRecord.ScheduleAnswer(info);
+    }
+
+exit:
+    return;
+}
+
+void Core::Entry::DetermineNextFireTime(void)
+{
+    mKeyRecord.UpdateFireTimeOn(*this);
+
+    if (mMulticastNsecPending)
+    {
+        SetFireTime(mNsecAnswerTime);
+    }
+}
+
+void Core::Entry::ScheduleTimer(void) { ScheduleFireTimeOn(Get<Core>().mEntryTimer); }
+
+template <typename EntryType> void Core::Entry::HandleTimer(EntryTimerContext &aContext)
+{
+    EntryType *thisAsEntryType = static_cast<EntryType *>(this);
+
+    thisAsEntryType->ClearAppendState();
+
+    VerifyOrExit(HasFireTime());
+    VerifyOrExit(GetFireTime() <= aContext.GetNow());
+    ClearFireTime();
+
+    switch (GetState())
+    {
+    case kProbing:
+        if (mProbeCount < kNumberOfProbes)
+        {
+            mProbeCount++;
+            SetFireTime(aContext.GetNow() + kProbeWaitTime);
+            thisAsEntryType->PrepareProbe(aContext.GetProbeMessage());
+            break;
+        }
+
+        SetState(kRegistered);
+        thisAsEntryType->StartAnnouncing();
+
+        OT_FALL_THROUGH;
+
+    case kRegistered:
+        thisAsEntryType->PrepareResponse(aContext.GetResponseMessage(), aContext.GetNow());
+        break;
+
+    case kConflict:
+    case kRemoving:
+        ExitNow();
+    }
+
+    thisAsEntryType->DetermineNextFireTime();
+
+exit:
+    if (HasFireTime())
+    {
+        aContext.UpdateNextTime(GetFireTime());
+    }
+}
+
+void Core::Entry::AppendQuestionTo(TxMessage &aTxMessage) const
+{
+    Message &message = aTxMessage.SelectMessageFor(kQuestionSection);
+    uint16_t rrClass = ResourceRecord::kClassInternet;
+    Question question;
+
+    if ((mProbeCount == 1) && Get<Core>().IsQuestionUnicastAllowed())
+    {
+        rrClass |= kClassQuestionUnicastFlag;
+    }
+
+    question.SetType(ResourceRecord::kTypeAny);
+    question.SetClass(rrClass);
+    SuccessOrAssert(message.Append(question));
+
+    aTxMessage.IncrementRecordCount(kQuestionSection);
+}
+
+void Core::Entry::AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection, NameAppender aNameAppender)
+{
+    Message       *message;
+    ResourceRecord record;
+
+    VerifyOrExit(mKeyRecord.CanAppend());
+    mKeyRecord.MarkAsAppended(aTxMessage, aSection);
+
+    message = &aTxMessage.SelectMessageFor(aSection);
+
+    // Use the `aNameAppender` function to allow sub-class
+    // to append the proper name.
+
+    aNameAppender(*this, aTxMessage, aSection);
+
+    record.Init(ResourceRecord::kTypeKey);
+    record.SetTtl(mKeyRecord.GetTtl());
+    record.SetLength(mKeyData.GetLength());
+    UpdateCacheFlushFlagIn(record, aSection);
+
+    SuccessOrAssert(message->Append(record));
+    SuccessOrAssert(message->AppendBytes(mKeyData.GetBytes(), mKeyData.GetLength()));
+
+    aTxMessage.IncrementRecordCount(aSection);
+
+exit:
+    return;
+}
+
+void Core::Entry::AppendNsecRecordTo(TxMessage       &aTxMessage,
+                                     Section          aSection,
+                                     const TypeArray &aTypes,
+                                     NameAppender     aNameAppender)
+{
+    Message               &message = aTxMessage.SelectMessageFor(aSection);
+    NsecRecord             nsec;
+    NsecRecord::TypeBitMap bitmap;
+    uint16_t               offset;
+
+    nsec.Init();
+    nsec.SetTtl(kNsecTtl);
+    UpdateCacheFlushFlagIn(nsec, aSection);
+
+    bitmap.Clear();
+
+    for (uint16_t type : aTypes)
+    {
+        bitmap.AddType(type);
+    }
+
+    aNameAppender(*this, aTxMessage, aSection);
+
+    offset = message.GetLength();
+    SuccessOrAssert(message.Append(nsec));
+
+    // Next Domain Name (should be same as record name).
+    aNameAppender(*this, aTxMessage, aSection);
+
+    SuccessOrAssert(message.AppendBytes(&bitmap, bitmap.GetSize()));
+
+    UpdateRecordLengthInMessage(nsec, message, offset);
+    aTxMessage.IncrementRecordCount(aSection);
+
+    mAppendedNsec = true;
+}
+
+Error Core::Entry::CopyKeyInfoTo(Key &aKey, EntryState &aState) const
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(mKeyRecord.IsPresent(), error = kErrorNotFound);
+
+    aKey.mKeyData       = mKeyData.GetBytes();
+    aKey.mKeyDataLength = mKeyData.GetLength();
+    aKey.mClass         = ResourceRecord::kClassInternet;
+    aKey.mTtl           = mKeyRecord.GetTtl();
+    aKey.mInfraIfIndex  = Get<Core>().mInfraIfIndex;
+    aState              = static_cast<EntryState>(GetState());
+
+exit:
+    return error;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::HostEntry
+
+Core::HostEntry::HostEntry(void)
+    : mNext(nullptr)
+    , mNameOffset(kUnspecifiedOffset)
+{
+}
+
+Error Core::HostEntry::Init(Instance &aInstance, const char *aName)
+{
+    Entry::Init(aInstance);
+
+    return mName.Set(aName);
+}
+
+bool Core::HostEntry::Matches(const Name &aName) const
+{
+    return aName.Matches(/* aFirstLabel */ nullptr, mName.AsCString(), kLocalDomain);
+}
+
+bool Core::HostEntry::Matches(const Host &aHost) const { return NameMatch(mName, aHost.mHostName); }
+
+bool Core::HostEntry::Matches(const Key &aKey) const { return !IsKeyForService(aKey) && NameMatch(mName, aKey.mName); }
+
+bool Core::HostEntry::Matches(const Heap::String &aName) const { return NameMatch(mName, aName); }
+
+bool Core::HostEntry::IsEmpty(void) const { return !mAddrRecord.IsPresent() && !mKeyRecord.IsPresent(); }
+
+void Core::HostEntry::Register(const Host &aHost, const Callback &aCallback)
+{
+    if (GetState() == kRemoving)
+    {
+        StartProbing();
+    }
+
+    SetCallback(aCallback);
+
+    if (aHost.mAddressesLength == 0)
+    {
+        // If host is registered with no addresses, treat it
+        // as host being unregistered and announce removal of
+        // the old addresses.
+        Unregister(aHost);
+        ExitNow();
+    }
+
+    mAddrRecord.UpdateTtl(DetermineTtl(aHost.mTtl, kDefaultTtl));
+    mAddrRecord.UpdateProperty(mAddresses, AsCoreTypePtr(aHost.mAddresses), aHost.mAddressesLength);
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+
+exit:
+    return;
+}
+
+void Core::HostEntry::Register(const Key &aKey, const Callback &aCallback)
+{
+    Entry::Register(aKey, aCallback);
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+}
+
+void Core::HostEntry::Unregister(const Host &aHost)
+{
+    OT_UNUSED_VARIABLE(aHost);
+
+    VerifyOrExit(mAddrRecord.IsPresent());
+
+    ClearCallback();
+
+    switch (GetState())
+    {
+    case kRegistered:
+        mAddrRecord.UpdateTtl(0);
+        DetermineNextFireTime();
+        ScheduleTimer();
+        break;
+
+    case kProbing:
+    case kConflict:
+        ClearHost();
+        ScheduleToRemoveIfEmpty();
+        break;
+
+    case kRemoving:
+        break;
+    }
+
+exit:
+    return;
+}
+
+void Core::HostEntry::Unregister(const Key &aKey)
+{
+    Entry::Unregister(aKey);
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+
+    ScheduleToRemoveIfEmpty();
+}
+
+void Core::HostEntry::ClearHost(void)
+{
+    mAddrRecord.Clear();
+    mAddresses.Free();
+}
+
+void Core::HostEntry::ScheduleToRemoveIfEmpty(void)
+{
+    if (IsEmpty())
+    {
+        SetStateToRemoving();
+        Get<Core>().mEntryTask.Post();
+    }
+}
+
+void Core::HostEntry::HandleConflict(void)
+{
+    State oldState = GetState();
+
+    SetStateToConflict();
+    VerifyOrExit(oldState == kRegistered);
+    Get<Core>().InvokeConflictCallback(mName.AsCString(), nullptr);
+
+exit:
+    return;
+}
+
+void Core::HostEntry::AnswerQuestion(const AnswerInfo &aInfo)
+{
+    RecordAndType records[] = {
+        {mAddrRecord, ResourceRecord::kTypeAaaa},
+        {mKeyRecord, ResourceRecord::kTypeKey},
+    };
+
+    VerifyOrExit(GetState() == kRegistered);
+
+    if (aInfo.mIsProbe)
+    {
+        AnswerProbe(aInfo, records, GetArrayLength(records));
+    }
+    else
+    {
+        AnswerNonProbe(aInfo, records, GetArrayLength(records));
+    }
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+
+exit:
+    return;
+}
+
+void Core::HostEntry::HandleTimer(EntryTimerContext &aContext) { Entry::HandleTimer<HostEntry>(aContext); }
+
+void Core::HostEntry::ClearAppendState(void)
+{
+    // Clears `HostEntry` records and all tracked saved name
+    // compression offsets.
+
+    Entry::ClearAppendState();
+
+    mAddrRecord.MarkAsNotAppended();
+
+    mNameOffset = kUnspecifiedOffset;
+}
+
+void Core::HostEntry::PrepareProbe(TxMessage &aProbe)
+{
+    bool prepareAgain = false;
+
+    do
+    {
+        aProbe.SaveCurrentState();
+
+        AppendNameTo(aProbe, kQuestionSection);
+        AppendQuestionTo(aProbe);
+
+        AppendAddressRecordsTo(aProbe, kAuthoritySection);
+        AppendKeyRecordTo(aProbe, kAuthoritySection);
+
+        aProbe.CheckSizeLimitToPrepareAgain(prepareAgain);
+
+    } while (prepareAgain);
+}
+
+void Core::HostEntry::StartAnnouncing(void)
+{
+    mAddrRecord.StartAnnouncing();
+    mKeyRecord.StartAnnouncing();
+}
+
+void Core::HostEntry::PrepareResponse(TxMessage &aResponse, TimeMilli aNow)
+{
+    bool prepareAgain = false;
+
+    do
+    {
+        aResponse.SaveCurrentState();
+        PrepareResponseRecords(aResponse, aNow);
+        aResponse.CheckSizeLimitToPrepareAgain(prepareAgain);
+
+    } while (prepareAgain);
+
+    UpdateRecordsState(aResponse);
+}
+
+void Core::HostEntry::PrepareResponseRecords(TxMessage &aResponse, TimeMilli aNow)
+{
+    bool appendNsec = false;
+
+    if (mAddrRecord.ShouldAppendTo(aResponse, aNow))
+    {
+        AppendAddressRecordsTo(aResponse, kAnswerSection);
+        appendNsec = true;
+    }
+
+    if (mKeyRecord.ShouldAppendTo(aResponse, aNow))
+    {
+        AppendKeyRecordTo(aResponse, kAnswerSection);
+        appendNsec = true;
+    }
+
+    if (appendNsec || ShouldAnswerNsec(aNow))
+    {
+        AppendNsecRecordTo(aResponse, kAdditionalDataSection);
+    }
+}
+
+void Core::HostEntry::UpdateRecordsState(const TxMessage &aResponse)
+{
+    // Updates state after a response is prepared.
+
+    Entry::UpdateRecordsState(aResponse);
+    mAddrRecord.UpdateStateAfterAnswer(aResponse);
+
+    if (IsEmpty())
+    {
+        SetStateToRemoving();
+    }
+}
+
+void Core::HostEntry::DetermineNextFireTime(void)
+{
+    VerifyOrExit(GetState() == kRegistered);
+
+    Entry::DetermineNextFireTime();
+    mAddrRecord.UpdateFireTimeOn(*this);
+
+exit:
+    return;
+}
+
+void Core::HostEntry::AppendAddressRecordsTo(TxMessage &aTxMessage, Section aSection)
+{
+    Message *message;
+
+    VerifyOrExit(mAddrRecord.CanAppend());
+    mAddrRecord.MarkAsAppended(aTxMessage, aSection);
+
+    message = &aTxMessage.SelectMessageFor(aSection);
+
+    for (const Ip6::Address &address : mAddresses)
+    {
+        AaaaRecord aaaaRecord;
+
+        aaaaRecord.Init();
+        aaaaRecord.SetTtl(mAddrRecord.GetTtl());
+        aaaaRecord.SetAddress(address);
+        UpdateCacheFlushFlagIn(aaaaRecord, aSection);
+
+        AppendNameTo(aTxMessage, aSection);
+        SuccessOrAssert(message->Append(aaaaRecord));
+
+        aTxMessage.IncrementRecordCount(aSection);
+    }
+
+exit:
+    return;
+}
+
+void Core::HostEntry::AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection)
+{
+    Entry::AppendKeyRecordTo(aTxMessage, aSection, &AppendEntryName);
+}
+
+void Core::HostEntry::AppendNsecRecordTo(TxMessage &aTxMessage, Section aSection)
+{
+    TypeArray types;
+
+    if (mAddrRecord.IsPresent() && (mAddrRecord.GetTtl() > 0))
+    {
+        types.Add(ResourceRecord::kTypeAaaa);
+    }
+
+    if (mKeyRecord.IsPresent() && (mKeyRecord.GetTtl() > 0))
+    {
+        types.Add(ResourceRecord::kTypeKey);
+    }
+
+    if (!types.IsEmpty())
+    {
+        Entry::AppendNsecRecordTo(aTxMessage, aSection, types, &AppendEntryName);
+    }
+}
+
+void Core::HostEntry::AppendEntryName(Entry &aEntry, TxMessage &aTxMessage, Section aSection)
+{
+    static_cast<HostEntry &>(aEntry).AppendNameTo(aTxMessage, aSection);
+}
+
+void Core::HostEntry::AppendNameTo(TxMessage &aTxMessage, Section aSection)
+{
+    AppendOutcome outcome;
+
+    outcome = aTxMessage.AppendMultipleLabels(aSection, mName.AsCString(), mNameOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    aTxMessage.AppendDomainName(aSection);
+
+exit:
+    return;
+}
+
+Error Core::HostEntry::CopyInfoTo(Host &aHost, EntryState &aState) const
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(mAddrRecord.IsPresent(), error = kErrorNotFound);
+
+    aHost.mHostName        = mName.AsCString();
+    aHost.mAddresses       = mAddresses.AsCArray();
+    aHost.mAddressesLength = mAddresses.GetLength();
+    aHost.mTtl             = mAddrRecord.GetTtl();
+    aHost.mInfraIfIndex    = Get<Core>().mInfraIfIndex;
+    aState                 = static_cast<EntryState>(GetState());
+
+exit:
+    return error;
+}
+
+Error Core::HostEntry::CopyInfoTo(Key &aKey, EntryState &aState) const
+{
+    Error error;
+
+    SuccessOrExit(error = CopyKeyInfoTo(aKey, aState));
+
+    aKey.mName        = mName.AsCString();
+    aKey.mServiceType = nullptr;
+
+exit:
+    return error;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::ServiceEntry
+
+const uint8_t Core::ServiceEntry::kEmptyTxtData[] = {0};
+
+Core::ServiceEntry::ServiceEntry(void)
+    : mNext(nullptr)
+    , mPriority(0)
+    , mWeight(0)
+    , mPort(0)
+    , mServiceNameOffset(kUnspecifiedOffset)
+    , mServiceTypeOffset(kUnspecifiedOffset)
+    , mSubServiceTypeOffset(kUnspecifiedOffset)
+    , mHostNameOffset(kUnspecifiedOffset)
+    , mIsAddedInServiceTypes(false)
+{
+}
+
+Error Core::ServiceEntry::Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType)
+{
+    Error error;
+
+    Entry::Init(aInstance);
+
+    SuccessOrExit(error = mServiceInstance.Set(aServiceInstance));
+    SuccessOrExit(error = mServiceType.Set(aServiceType));
+
+exit:
+    return error;
+}
+
+Error Core::ServiceEntry::Init(Instance &aInstance, const Service &aService)
+{
+    return Init(aInstance, aService.mServiceInstance, aService.mServiceType);
+}
+
+Error Core::ServiceEntry::Init(Instance &aInstance, const Key &aKey)
+{
+    return Init(aInstance, aKey.mName, aKey.mServiceType);
+}
+
+bool Core::ServiceEntry::Matches(const Name &aFullName) const
+{
+    return aFullName.Matches(mServiceInstance.AsCString(), mServiceType.AsCString(), kLocalDomain);
+}
+
+bool Core::ServiceEntry::MatchesServiceType(const Name &aServiceType) const
+{
+    // When matching service type, PTR record should be
+    // present with non-zero TTL (checked by `CanAnswer()`).
+
+    return mPtrRecord.CanAnswer() && aServiceType.Matches(nullptr, mServiceType.AsCString(), kLocalDomain);
+}
+
+bool Core::ServiceEntry::Matches(const Service &aService) const
+{
+    return NameMatch(mServiceInstance, aService.mServiceInstance) && NameMatch(mServiceType, aService.mServiceType);
+}
+
+bool Core::ServiceEntry::Matches(const Key &aKey) const
+{
+    return IsKeyForService(aKey) && NameMatch(mServiceInstance, aKey.mName) &&
+           NameMatch(mServiceType, aKey.mServiceType);
+}
+
+bool Core::ServiceEntry::IsEmpty(void) const { return !mPtrRecord.IsPresent() && !mKeyRecord.IsPresent(); }
+
+bool Core::ServiceEntry::CanAnswerSubType(const char *aSubLabel) const
+{
+    bool           canAnswer = false;
+    const SubType *subType;
+
+    VerifyOrExit(mPtrRecord.CanAnswer());
+
+    subType = mSubTypes.FindMatching(aSubLabel);
+    VerifyOrExit(subType != nullptr);
+
+    canAnswer = subType->mPtrRecord.CanAnswer();
+
+exit:
+    return canAnswer;
+}
+
+void Core::ServiceEntry::Register(const Service &aService, const Callback &aCallback)
+{
+    uint32_t ttl = DetermineTtl(aService.mTtl, kDefaultTtl);
+
+    if (GetState() == kRemoving)
+    {
+        StartProbing();
+    }
+
+    SetCallback(aCallback);
+
+    // Register sub-types PTRs.
+
+    // First we check for any removed sub-types. We keep removed
+    // sub-types marked with zero TTL so to announce their removal
+    // before fully removing them from the list.
+
+    for (SubType &subType : mSubTypes)
+    {
+        uint32_t subTypeTtl = subType.IsContainedIn(aService) ? ttl : 0;
+
+        subType.mPtrRecord.UpdateTtl(subTypeTtl);
+    }
+
+    // Next we add any new sub-types in `aService`.
+
+    for (uint16_t i = 0; i < aService.mSubTypeLabelsLength; i++)
+    {
+        const char *label = aService.mSubTypeLabels[i];
+
+        if (!mSubTypes.ContainsMatching(label))
+        {
+            SubType *newSubType = SubType::AllocateAndInit(label);
+
+            OT_ASSERT(newSubType != nullptr);
+            mSubTypes.Push(*newSubType);
+
+            newSubType->mPtrRecord.UpdateTtl(ttl);
+        }
+    }
+
+    // Register base PTR service.
+
+    mPtrRecord.UpdateTtl(ttl);
+
+    // Register SRV record info.
+
+    mSrvRecord.UpdateTtl(ttl);
+    mSrvRecord.UpdateProperty(mHostName, aService.mHostName);
+    mSrvRecord.UpdateProperty(mPriority, aService.mPriority);
+    mSrvRecord.UpdateProperty(mWeight, aService.mWeight);
+    mSrvRecord.UpdateProperty(mPort, aService.mPort);
+
+    // Register TXT record info.
+
+    mTxtRecord.UpdateTtl(ttl);
+
+    if ((aService.mTxtData == nullptr) || (aService.mTxtDataLength == 0))
+    {
+        mTxtRecord.UpdateProperty(mTxtData, kEmptyTxtData, sizeof(kEmptyTxtData));
+    }
+    else
+    {
+        mTxtRecord.UpdateProperty(mTxtData, aService.mTxtData, aService.mTxtDataLength);
+    }
+
+    UpdateServiceTypes();
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+}
+
+void Core::ServiceEntry::Register(const Key &aKey, const Callback &aCallback)
+{
+    Entry::Register(aKey, aCallback);
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+}
+
+void Core::ServiceEntry::Unregister(const Service &aService)
+{
+    OT_UNUSED_VARIABLE(aService);
+
+    VerifyOrExit(mPtrRecord.IsPresent());
+
+    ClearCallback();
+
+    switch (GetState())
+    {
+    case kRegistered:
+        for (SubType &subType : mSubTypes)
+        {
+            subType.mPtrRecord.UpdateTtl(0);
+        }
+
+        mPtrRecord.UpdateTtl(0);
+        mSrvRecord.UpdateTtl(0);
+        mTxtRecord.UpdateTtl(0);
+        DetermineNextFireTime();
+        ScheduleTimer();
+        break;
+
+    case kProbing:
+    case kConflict:
+        ClearService();
+        ScheduleToRemoveIfEmpty();
+        break;
+
+    case kRemoving:
+        break;
+    }
+
+    UpdateServiceTypes();
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::Unregister(const Key &aKey)
+{
+    Entry::Unregister(aKey);
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+
+    ScheduleToRemoveIfEmpty();
+}
+
+void Core::ServiceEntry::ClearService(void)
+{
+    mPtrRecord.Clear();
+    mSrvRecord.Clear();
+    mTxtRecord.Clear();
+    mSubTypes.Free();
+    mHostName.Free();
+    mTxtData.Free();
+}
+
+void Core::ServiceEntry::ScheduleToRemoveIfEmpty(void)
+{
+    OwningList<SubType> removedSubTypes;
+
+    mSubTypes.RemoveAllMatching(EmptyChecker(), removedSubTypes);
+
+    if (IsEmpty())
+    {
+        SetStateToRemoving();
+        Get<Core>().mEntryTask.Post();
+    }
+}
+
+void Core::ServiceEntry::HandleConflict(void)
+{
+    State oldState = GetState();
+
+    SetStateToConflict();
+    UpdateServiceTypes();
+
+    VerifyOrExit(oldState == kRegistered);
+    Get<Core>().InvokeConflictCallback(mServiceInstance.AsCString(), mServiceType.AsCString());
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AnswerServiceNameQuestion(const AnswerInfo &aInfo)
+{
+    RecordAndType records[] = {
+        {mSrvRecord, ResourceRecord::kTypeSrv},
+        {mTxtRecord, ResourceRecord::kTypeTxt},
+        {mKeyRecord, ResourceRecord::kTypeKey},
+    };
+
+    VerifyOrExit(GetState() == kRegistered);
+
+    if (aInfo.mIsProbe)
+    {
+        AnswerProbe(aInfo, records, GetArrayLength(records));
+    }
+    else
+    {
+        AnswerNonProbe(aInfo, records, GetArrayLength(records));
+    }
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AnswerServiceTypeQuestion(const AnswerInfo &aInfo, const char *aSubLabel)
+{
+    VerifyOrExit(GetState() == kRegistered);
+
+    if (aSubLabel == nullptr)
+    {
+        mPtrRecord.ScheduleAnswer(aInfo);
+    }
+    else
+    {
+        SubType *subType = mSubTypes.FindMatching(aSubLabel);
+
+        VerifyOrExit(subType != nullptr);
+        subType->mPtrRecord.ScheduleAnswer(aInfo);
+    }
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+
+exit:
+    return;
+}
+
+bool Core::ServiceEntry::ShouldSuppressKnownAnswer(uint32_t aTtl, const char *aSubLabel) const
+{
+    // Check `aTtl` of a matching record in known-answer section of
+    // a query with the corresponding PTR record's TTL and suppress
+    // answer if it is at least at least half the correct value.
+
+    bool     shouldSuppress = false;
+    uint32_t ttl;
+
+    if (aSubLabel == nullptr)
+    {
+        ttl = mPtrRecord.GetTtl();
+    }
+    else
+    {
+        const SubType *subType = mSubTypes.FindMatching(aSubLabel);
+
+        VerifyOrExit(subType != nullptr);
+        ttl = subType->mPtrRecord.GetTtl();
+    }
+
+    shouldSuppress = (aTtl > ttl / 2);
+
+exit:
+    return shouldSuppress;
+}
+
+void Core::ServiceEntry::HandleTimer(EntryTimerContext &aContext) { Entry::HandleTimer<ServiceEntry>(aContext); }
+
+void Core::ServiceEntry::ClearAppendState(void)
+{
+    // Clear the append state for all `ServiceEntry` records,
+    // along with all tracked name compression offsets.
+
+    Entry::ClearAppendState();
+
+    mPtrRecord.MarkAsNotAppended();
+    mSrvRecord.MarkAsNotAppended();
+    mTxtRecord.MarkAsNotAppended();
+
+    mServiceNameOffset    = kUnspecifiedOffset;
+    mServiceTypeOffset    = kUnspecifiedOffset;
+    mSubServiceTypeOffset = kUnspecifiedOffset;
+    mHostNameOffset       = kUnspecifiedOffset;
+
+    for (SubType &subType : mSubTypes)
+    {
+        subType.mPtrRecord.MarkAsNotAppended();
+        subType.mSubServiceNameOffset = kUnspecifiedOffset;
+    }
+}
+
+void Core::ServiceEntry::PrepareProbe(TxMessage &aProbe)
+{
+    bool prepareAgain = false;
+
+    do
+    {
+        HostEntry *hostEntry = nullptr;
+
+        aProbe.SaveCurrentState();
+
+        DiscoverOffsetsAndHost(hostEntry);
+
+        AppendServiceNameTo(aProbe, kQuestionSection);
+        AppendQuestionTo(aProbe);
+
+        // Append records (if present) in authority section
+
+        AppendSrvRecordTo(aProbe, kAuthoritySection);
+        AppendTxtRecordTo(aProbe, kAuthoritySection);
+        AppendKeyRecordTo(aProbe, kAuthoritySection);
+
+        aProbe.CheckSizeLimitToPrepareAgain(prepareAgain);
+
+    } while (prepareAgain);
+}
+
+void Core::ServiceEntry::StartAnnouncing(void)
+{
+    for (SubType &subType : mSubTypes)
+    {
+        subType.mPtrRecord.StartAnnouncing();
+    }
+
+    mPtrRecord.StartAnnouncing();
+    mSrvRecord.StartAnnouncing();
+    mTxtRecord.StartAnnouncing();
+    mKeyRecord.StartAnnouncing();
+
+    UpdateServiceTypes();
+}
+
+void Core::ServiceEntry::PrepareResponse(TxMessage &aResponse, TimeMilli aNow)
+{
+    bool prepareAgain = false;
+
+    do
+    {
+        aResponse.SaveCurrentState();
+        PrepareResponseRecords(aResponse, aNow);
+        aResponse.CheckSizeLimitToPrepareAgain(prepareAgain);
+
+    } while (prepareAgain);
+
+    UpdateRecordsState(aResponse);
+}
+
+void Core::ServiceEntry::PrepareResponseRecords(TxMessage &aResponse, TimeMilli aNow)
+{
+    bool       appendNsec = false;
+    HostEntry *hostEntry  = nullptr;
+
+    DiscoverOffsetsAndHost(hostEntry);
+
+    // We determine records to include in Additional Data section
+    // per RFC 6763 section 12:
+    //
+    // - For base PTR, we include SRV, TXT, and host addresses.
+    // - For SRV, we include host addresses only (TXT record not
+    //   recommended).
+    //
+    // Records already appended in Answer section are excluded from
+    // Additional Data. Host Entries are processed before Service
+    // Entries which ensures address inclusion accuracy.
+    // `MarkToAppendInAdditionalData()` marks a record for potential
+    // Additional Data inclusion, but this is skipped if the record
+    // is already appended in the Answer section.
+
+    if (mPtrRecord.ShouldAppendTo(aResponse, aNow))
+    {
+        AppendPtrRecordTo(aResponse, kAnswerSection);
+
+        if (mPtrRecord.GetTtl() > 0)
+        {
+            mSrvRecord.MarkToAppendInAdditionalData();
+            mTxtRecord.MarkToAppendInAdditionalData();
+
+            if (hostEntry != nullptr)
+            {
+                hostEntry->mAddrRecord.MarkToAppendInAdditionalData();
+            }
+        }
+    }
+
+    for (SubType &subType : mSubTypes)
+    {
+        if (subType.mPtrRecord.ShouldAppendTo(aResponse, aNow))
+        {
+            AppendPtrRecordTo(aResponse, kAnswerSection, &subType);
+        }
+    }
+
+    if (mSrvRecord.ShouldAppendTo(aResponse, aNow))
+    {
+        AppendSrvRecordTo(aResponse, kAnswerSection);
+        appendNsec = true;
+
+        if ((mSrvRecord.GetTtl() > 0) && (hostEntry != nullptr))
+        {
+            hostEntry->mAddrRecord.MarkToAppendInAdditionalData();
+        }
+    }
+
+    if (mTxtRecord.ShouldAppendTo(aResponse, aNow))
+    {
+        AppendTxtRecordTo(aResponse, kAnswerSection);
+        appendNsec = true;
+    }
+
+    if (mKeyRecord.ShouldAppendTo(aResponse, aNow))
+    {
+        AppendKeyRecordTo(aResponse, kAnswerSection);
+        appendNsec = true;
+    }
+
+    // Append records in Additional Data section
+
+    if (mSrvRecord.ShouldAppendInAdditionalDataSection())
+    {
+        AppendSrvRecordTo(aResponse, kAdditionalDataSection);
+    }
+
+    if (mTxtRecord.ShouldAppendInAdditionalDataSection())
+    {
+        AppendTxtRecordTo(aResponse, kAdditionalDataSection);
+    }
+
+    if ((hostEntry != nullptr) && (hostEntry->mAddrRecord.ShouldAppendInAdditionalDataSection()))
+    {
+        hostEntry->AppendAddressRecordsTo(aResponse, kAdditionalDataSection);
+    }
+
+    if (appendNsec || ShouldAnswerNsec(aNow))
+    {
+        AppendNsecRecordTo(aResponse, kAdditionalDataSection);
+    }
+}
+
+void Core::ServiceEntry::UpdateRecordsState(const TxMessage &aResponse)
+{
+    OwningList<SubType> removedSubTypes;
+
+    Entry::UpdateRecordsState(aResponse);
+
+    mPtrRecord.UpdateStateAfterAnswer(aResponse);
+    mSrvRecord.UpdateStateAfterAnswer(aResponse);
+    mTxtRecord.UpdateStateAfterAnswer(aResponse);
+
+    for (SubType &subType : mSubTypes)
+    {
+        subType.mPtrRecord.UpdateStateAfterAnswer(aResponse);
+    }
+
+    mSubTypes.RemoveAllMatching(EmptyChecker(), removedSubTypes);
+
+    if (IsEmpty())
+    {
+        SetStateToRemoving();
+    }
+}
+
+void Core::ServiceEntry::DetermineNextFireTime(void)
+{
+    VerifyOrExit(GetState() == kRegistered);
+
+    Entry::DetermineNextFireTime();
+
+    mPtrRecord.UpdateFireTimeOn(*this);
+    mSrvRecord.UpdateFireTimeOn(*this);
+    mTxtRecord.UpdateFireTimeOn(*this);
+
+    for (SubType &subType : mSubTypes)
+    {
+        subType.mPtrRecord.UpdateFireTimeOn(*this);
+    }
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::DiscoverOffsetsAndHost(HostEntry *&aHostEntry)
+{
+    // Discovers the `HostEntry` associated with this `ServiceEntry`
+    // and name compression offsets from the previously appended
+    // entries.
+
+    aHostEntry = Get<Core>().mHostEntries.FindMatching(mHostName);
+
+    if ((aHostEntry != nullptr) && (aHostEntry->GetState() != GetState()))
+    {
+        aHostEntry = nullptr;
+    }
+
+    if (aHostEntry != nullptr)
+    {
+        UpdateCompressOffset(mHostNameOffset, aHostEntry->mNameOffset);
+    }
+
+    for (ServiceEntry &other : Get<Core>().mServiceEntries)
+    {
+        // We only need to search up to `this` entry in the list,
+        // since entries after `this` are not yet processed and not
+        // yet appended in the response or the probe message.
+
+        if (&other == this)
+        {
+            break;
+        }
+
+        if (other.GetState() != GetState())
+        {
+            // Validate that both entries are in the same state,
+            // ensuring their records are appended in the same
+            // message, i.e., a probe or a response message.
+
+            continue;
+        }
+
+        if (NameMatch(mHostName, other.mHostName))
+        {
+            UpdateCompressOffset(mHostNameOffset, other.mHostNameOffset);
+        }
+
+        if (NameMatch(mServiceType, other.mServiceType))
+        {
+            UpdateCompressOffset(mServiceTypeOffset, other.mServiceTypeOffset);
+
+            if (GetState() == kProbing)
+            {
+                // No need to search for sub-type service offsets when
+                // we are still probing.
+
+                continue;
+            }
+
+            UpdateCompressOffset(mSubServiceTypeOffset, other.mSubServiceTypeOffset);
+
+            for (SubType &subType : mSubTypes)
+            {
+                const SubType *otherSubType = other.mSubTypes.FindMatching(subType.mLabel.AsCString());
+
+                if (otherSubType != nullptr)
+                {
+                    UpdateCompressOffset(subType.mSubServiceNameOffset, otherSubType->mSubServiceNameOffset);
+                }
+            }
+        }
+    }
+}
+
+void Core::ServiceEntry::UpdateServiceTypes(void)
+{
+    // This method updates the `mServiceTypes` list adding or
+    // removing this `ServiceEntry` info.
+    //
+    // It is called whenever the `ServiceEntry` state gets changed
+    // or a PTR record is added or removed. The service is valid
+    // when entry is registered and we have a PTR with non-zero
+    // TTL.
+
+    bool         shouldAdd = (GetState() == kRegistered) && mPtrRecord.CanAnswer();
+    ServiceType *serviceType;
+
+    VerifyOrExit(shouldAdd != mIsAddedInServiceTypes);
+
+    mIsAddedInServiceTypes = shouldAdd;
+
+    serviceType = Get<Core>().mServiceTypes.FindMatching(mServiceType);
+
+    if (shouldAdd && (serviceType == nullptr))
+    {
+        serviceType = ServiceType::AllocateAndInit(GetInstance(), mServiceType.AsCString());
+        OT_ASSERT(serviceType != nullptr);
+        Get<Core>().mServiceTypes.Push(*serviceType);
+    }
+
+    VerifyOrExit(serviceType != nullptr);
+
+    if (shouldAdd)
+    {
+        serviceType->IncrementNumEntries();
+    }
+    else
+    {
+        serviceType->DecrementNumEntries();
+
+        if (serviceType->GetNumEntries() == 0)
+        {
+            // If there are no more `ServiceEntry` with
+            // this service type, we remove the it from
+            // the `mServiceTypes` list. It is safe to
+            // remove here as this method will never be
+            // called while we are iterating over the
+            // `mServiceTypes` list.
+
+            Get<Core>().mServiceTypes.RemoveMatching(*serviceType);
+        }
+    }
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AppendSrvRecordTo(TxMessage &aTxMessage, Section aSection)
+{
+    Message  *message;
+    SrvRecord srv;
+    uint16_t  offset;
+
+    VerifyOrExit(mSrvRecord.CanAppend());
+    mSrvRecord.MarkAsAppended(aTxMessage, aSection);
+
+    message = &aTxMessage.SelectMessageFor(aSection);
+
+    srv.Init();
+    srv.SetTtl(mSrvRecord.GetTtl());
+    srv.SetPriority(mPriority);
+    srv.SetWeight(mWeight);
+    srv.SetPort(mPort);
+    UpdateCacheFlushFlagIn(srv, aSection);
+
+    AppendServiceNameTo(aTxMessage, aSection);
+    offset = message->GetLength();
+    SuccessOrAssert(message->Append(srv));
+    AppendHostNameTo(aTxMessage, aSection);
+    UpdateRecordLengthInMessage(srv, *message, offset);
+
+    aTxMessage.IncrementRecordCount(aSection);
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AppendTxtRecordTo(TxMessage &aTxMessage, Section aSection)
+{
+    Message  *message;
+    TxtRecord txt;
+
+    VerifyOrExit(mTxtRecord.CanAppend());
+    mTxtRecord.MarkAsAppended(aTxMessage, aSection);
+
+    message = &aTxMessage.SelectMessageFor(aSection);
+
+    txt.Init();
+    txt.SetTtl(mTxtRecord.GetTtl());
+    txt.SetLength(mTxtData.GetLength());
+    UpdateCacheFlushFlagIn(txt, aSection);
+
+    AppendServiceNameTo(aTxMessage, aSection);
+    SuccessOrAssert(message->Append(txt));
+    SuccessOrAssert(message->AppendBytes(mTxtData.GetBytes(), mTxtData.GetLength()));
+
+    aTxMessage.IncrementRecordCount(aSection);
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AppendPtrRecordTo(TxMessage &aTxMessage, Section aSection, SubType *aSubType)
+{
+    // Appends PTR record for base service (when `aSubType == nullptr`) or
+    // for the given `aSubType`.
+
+    Message    *message;
+    RecordInfo &ptrRecord = (aSubType == nullptr) ? mPtrRecord : aSubType->mPtrRecord;
+    PtrRecord   ptr;
+    uint16_t    offset;
+
+    VerifyOrExit(ptrRecord.CanAppend());
+    ptrRecord.MarkAsAppended(aTxMessage, aSection);
+
+    message = &aTxMessage.SelectMessageFor(aSection);
+
+    ptr.Init();
+    ptr.SetTtl(ptrRecord.GetTtl());
+
+    if (aSubType == nullptr)
+    {
+        AppendServiceTypeTo(aTxMessage, aSection);
+    }
+    else
+    {
+        AppendSubServiceNameTo(aTxMessage, aSection, *aSubType);
+    }
+
+    offset = message->GetLength();
+    SuccessOrAssert(message->Append(ptr));
+    AppendServiceNameTo(aTxMessage, aSection);
+    UpdateRecordLengthInMessage(ptr, *message, offset);
+
+    aTxMessage.IncrementRecordCount(aSection);
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection)
+{
+    Entry::AppendKeyRecordTo(aTxMessage, aSection, &AppendEntryName);
+}
+
+void Core::ServiceEntry::AppendNsecRecordTo(TxMessage &aTxMessage, Section aSection)
+{
+    TypeArray types;
+
+    if (mSrvRecord.IsPresent() && (mSrvRecord.GetTtl() > 0))
+    {
+        types.Add(ResourceRecord::kTypeSrv);
+    }
+
+    if (mTxtRecord.IsPresent() && (mTxtRecord.GetTtl() > 0))
+    {
+        types.Add(ResourceRecord::kTypeTxt);
+    }
+
+    if (mKeyRecord.IsPresent() && (mKeyRecord.GetTtl() > 0))
+    {
+        types.Add(ResourceRecord::kTypeKey);
+    }
+
+    if (!types.IsEmpty())
+    {
+        Entry::AppendNsecRecordTo(aTxMessage, aSection, types, &AppendEntryName);
+    }
+}
+
+void Core::ServiceEntry::AppendEntryName(Entry &aEntry, TxMessage &aTxMessage, Section aSection)
+{
+    static_cast<ServiceEntry &>(aEntry).AppendServiceNameTo(aTxMessage, aSection);
+}
+
+void Core::ServiceEntry::AppendServiceNameTo(TxMessage &aTxMessage, Section aSection)
+{
+    AppendOutcome outcome;
+
+    outcome = aTxMessage.AppendLabel(aSection, mServiceInstance.AsCString(), mServiceNameOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    AppendServiceTypeTo(aTxMessage, aSection);
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AppendServiceTypeTo(TxMessage &aTxMessage, Section aSection)
+{
+    aTxMessage.AppendServiceType(aSection, mServiceType.AsCString(), mServiceTypeOffset);
+}
+
+void Core::ServiceEntry::AppendSubServiceTypeTo(TxMessage &aTxMessage, Section aSection)
+{
+    AppendOutcome outcome;
+
+    outcome = aTxMessage.AppendLabel(aSection, kSubServiceLabel, mSubServiceTypeOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    AppendServiceTypeTo(aTxMessage, aSection);
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AppendSubServiceNameTo(TxMessage &aTxMessage, Section aSection, SubType &aSubType)
+{
+    AppendOutcome outcome;
+
+    outcome = aTxMessage.AppendLabel(aSection, aSubType.mLabel.AsCString(), aSubType.mSubServiceNameOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    AppendSubServiceTypeTo(aTxMessage, aSection);
+
+exit:
+    return;
+}
+
+void Core::ServiceEntry::AppendHostNameTo(TxMessage &aTxMessage, Section aSection)
+{
+    AppendOutcome outcome;
+
+    outcome = aTxMessage.AppendMultipleLabels(aSection, mHostName.AsCString(), mHostNameOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    aTxMessage.AppendDomainName(aSection);
+
+exit:
+    return;
+}
+
+Error Core::ServiceEntry::CopyInfoTo(Service &aService, EntryState &aState, EntryIterator &aIterator) const
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(mPtrRecord.IsPresent(), error = kErrorNotFound);
+
+    aIterator.mSubTypeArray.Free();
+
+    for (const SubType &subType : mSubTypes)
+    {
+        SuccessOrAssert(aIterator.mSubTypeArray.PushBack(subType.mLabel.AsCString()));
+    }
+
+    aService.mHostName            = mHostName.AsCString();
+    aService.mServiceInstance     = mServiceInstance.AsCString();
+    aService.mServiceType         = mServiceType.AsCString();
+    aService.mSubTypeLabels       = aIterator.mSubTypeArray.AsCArray();
+    aService.mSubTypeLabelsLength = aIterator.mSubTypeArray.GetLength();
+    aService.mTxtData             = mTxtData.GetBytes();
+    aService.mTxtDataLength       = mTxtData.GetLength();
+    aService.mPort                = mPort;
+    aService.mPriority            = mPriority;
+    aService.mWeight              = mWeight;
+    aService.mTtl                 = mPtrRecord.GetTtl();
+    aService.mInfraIfIndex        = Get<Core>().mInfraIfIndex;
+    aState                        = static_cast<EntryState>(GetState());
+
+exit:
+    return error;
+}
+
+Error Core::ServiceEntry::CopyInfoTo(Key &aKey, EntryState &aState) const
+{
+    Error error;
+
+    SuccessOrExit(error = CopyKeyInfoTo(aKey, aState));
+
+    aKey.mName        = mServiceInstance.AsCString();
+    aKey.mServiceType = mServiceType.AsCString();
+
+exit:
+    return error;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::ServiceEntry::SubType
+
+Error Core::ServiceEntry::SubType::Init(const char *aLabel)
+{
+    mSubServiceNameOffset = kUnspecifiedOffset;
+
+    return mLabel.Set(aLabel);
+}
+
+bool Core::ServiceEntry::SubType::Matches(const EmptyChecker &aChecker) const
+{
+    OT_UNUSED_VARIABLE(aChecker);
+
+    return !mPtrRecord.IsPresent();
+}
+
+bool Core::ServiceEntry::SubType::IsContainedIn(const Service &aService) const
+{
+    bool contains = false;
+
+    for (uint16_t i = 0; i < aService.mSubTypeLabelsLength; i++)
+    {
+        if (NameMatch(mLabel, aService.mSubTypeLabels[i]))
+        {
+            contains = true;
+            break;
+        }
+    }
+
+    return contains;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::ServiceType
+
+Error Core::ServiceType::Init(Instance &aInstance, const char *aServiceType)
+{
+    Error error;
+
+    InstanceLocatorInit::Init(aInstance);
+
+    mNext       = nullptr;
+    mNumEntries = 0;
+    SuccessOrExit(error = mServiceType.Set(aServiceType));
+
+    mServicesPtr.UpdateTtl(kServicesPtrTtl);
+    mServicesPtr.StartAnnouncing();
+
+    mServicesPtr.UpdateFireTimeOn(*this);
+    ScheduleFireTimeOn(Get<Core>().mEntryTimer);
+
+exit:
+    return error;
+}
+
+bool Core::ServiceType::Matches(const Name &aServiceTypeName) const
+{
+    return aServiceTypeName.Matches(/* aFirstLabel */ nullptr, mServiceType.AsCString(), kLocalDomain);
+}
+
+bool Core::ServiceType::Matches(const Heap::String &aServiceType) const
+{
+    return NameMatch(aServiceType, mServiceType);
+}
+
+void Core::ServiceType::ClearAppendState(void) { mServicesPtr.MarkAsNotAppended(); }
+
+void Core::ServiceType::AnswerQuestion(const AnswerInfo &aInfo)
+{
+    VerifyOrExit(mServicesPtr.CanAnswer());
+    mServicesPtr.ScheduleAnswer(aInfo);
+    mServicesPtr.UpdateFireTimeOn(*this);
+    ScheduleFireTimeOn(Get<Core>().mEntryTimer);
+
+exit:
+    return;
+}
+
+bool Core::ServiceType::ShouldSuppressKnownAnswer(uint32_t aTtl) const
+{
+    // Check `aTtl` of a matching record in known-answer section of
+    // a query with the corresponding PTR record's TTL and suppress
+    // answer if it is at least at least half the correct value.
+
+    return (aTtl > mServicesPtr.GetTtl() / 2);
+}
+
+void Core::ServiceType::HandleTimer(EntryTimerContext &aContext)
+{
+    ClearAppendState();
+
+    VerifyOrExit(HasFireTime());
+    VerifyOrExit(GetFireTime() <= aContext.GetNow());
+    ClearFireTime();
+
+    PrepareResponse(aContext.GetResponseMessage(), aContext.GetNow());
+
+    mServicesPtr.UpdateFireTimeOn(*this);
+
+exit:
+    if (HasFireTime())
+    {
+        aContext.UpdateNextTime(GetFireTime());
+    }
+}
+
+void Core::ServiceType::PrepareResponse(TxMessage &aResponse, TimeMilli aNow)
+{
+    bool prepareAgain = false;
+
+    do
+    {
+        aResponse.SaveCurrentState();
+        PrepareResponseRecords(aResponse, aNow);
+        aResponse.CheckSizeLimitToPrepareAgain(prepareAgain);
+
+    } while (prepareAgain);
+
+    mServicesPtr.UpdateStateAfterAnswer(aResponse);
+}
+
+void Core::ServiceType::PrepareResponseRecords(TxMessage &aResponse, TimeMilli aNow)
+{
+    uint16_t serviceTypeOffset = kUnspecifiedOffset;
+
+    VerifyOrExit(mServicesPtr.ShouldAppendTo(aResponse, aNow));
+
+    // Discover compress offset for `mServiceType` if previously
+    // appended from any `ServiceEntry`.
+
+    for (const ServiceEntry &serviceEntry : Get<Core>().mServiceEntries)
+    {
+        if (serviceEntry.GetState() != Entry::kRegistered)
+        {
+            continue;
+        }
+
+        if (NameMatch(mServiceType, serviceEntry.mServiceType))
+        {
+            UpdateCompressOffset(serviceTypeOffset, serviceEntry.mServiceTypeOffset);
+
+            if (serviceTypeOffset != kUnspecifiedOffset)
+            {
+                break;
+            }
+        }
+    }
+
+    AppendPtrRecordTo(aResponse, serviceTypeOffset);
+
+exit:
+    return;
+}
+
+void Core::ServiceType::AppendPtrRecordTo(TxMessage &aResponse, uint16_t aServiceTypeOffset)
+{
+    Message  *message;
+    PtrRecord ptr;
+    uint16_t  offset;
+
+    VerifyOrExit(mServicesPtr.CanAppend());
+    mServicesPtr.MarkAsAppended(aResponse, kAnswerSection);
+
+    message = &aResponse.SelectMessageFor(kAnswerSection);
+
+    ptr.Init();
+    ptr.SetTtl(mServicesPtr.GetTtl());
+
+    aResponse.AppendServicesDnssdName(kAnswerSection);
+    offset = message->GetLength();
+    SuccessOrAssert(message->Append(ptr));
+    aResponse.AppendServiceType(kAnswerSection, mServiceType.AsCString(), aServiceTypeOffset);
+    UpdateRecordLengthInMessage(ptr, *message, offset);
+
+    aResponse.IncrementRecordCount(kAnswerSection);
+
+exit:
+    return;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::TxMessage
+
+Core::TxMessage::TxMessage(Instance &aInstance, Type aType)
+    : InstanceLocator(aInstance)
+{
+    Init(aType);
+}
+
+Core::TxMessage::TxMessage(Instance &aInstance, Type aType, const AddressInfo &aUnicastDest)
+    : TxMessage(aInstance, aType)
+{
+    mUnicastDest = aUnicastDest;
+}
+
+void Core::TxMessage::Init(Type aType)
+{
+    Header header;
+
+    mRecordCounts.Clear();
+    mSavedRecordCounts.Clear();
+    mSavedMsgLength      = 0;
+    mSavedExtraMsgLength = 0;
+    mDomainOffset        = kUnspecifiedOffset;
+    mUdpOffset           = kUnspecifiedOffset;
+    mTcpOffset           = kUnspecifiedOffset;
+    mServicesDnssdOffset = kUnspecifiedOffset;
+    mType                = aType;
+
+    // Allocate messages. The main `mMsgPtr` is always allocated.
+    // The Authority and Addition section messages are allocated
+    // the first time they are used.
+
+    mMsgPtr.Reset(Get<MessagePool>().Allocate(Message::kTypeOther));
+    OT_ASSERT(!mMsgPtr.IsNull());
+
+    mExtraMsgPtr.Reset();
+
+    header.Clear();
+
+    switch (aType)
+    {
+    case kMulticastProbe:
+    case kMulticastQuery:
+        header.SetType(Header::kTypeQuery);
+        break;
+    case kMulticastResponse:
+    case kUnicastResponse:
+        header.SetType(Header::kTypeResponse);
+        break;
+    }
+
+    SuccessOrAssert(mMsgPtr->Append(header));
+}
+
+Message &Core::TxMessage::SelectMessageFor(Section aSection)
+{
+    // Selects the `Message` to use for a given `aSection` based
+    // the message type.
+
+    Message *message      = nullptr;
+    Section  mainSection  = kAnswerSection;
+    Section  extraSection = kAdditionalDataSection;
+
+    switch (mType)
+    {
+    case kMulticastProbe:
+        mainSection  = kQuestionSection;
+        extraSection = kAuthoritySection;
+        break;
+
+    case kMulticastQuery:
+        mainSection  = kQuestionSection;
+        extraSection = kAnswerSection;
+        break;
+
+    case kMulticastResponse:
+    case kUnicastResponse:
+        break;
+    }
+
+    if (aSection == mainSection)
+    {
+        message = mMsgPtr.Get();
+    }
+    else if (aSection == extraSection)
+    {
+        if (mExtraMsgPtr.IsNull())
+        {
+            mExtraMsgPtr.Reset(Get<MessagePool>().Allocate(Message::kTypeOther));
+            OT_ASSERT(!mExtraMsgPtr.IsNull());
+        }
+
+        message = mExtraMsgPtr.Get();
+    }
+
+    OT_ASSERT(message != nullptr);
+
+    return *message;
+}
+
+Core::AppendOutcome Core::TxMessage::AppendLabel(Section aSection, const char *aLabel, uint16_t &aCompressOffset)
+{
+    return AppendLabels(aSection, aLabel, kIsSingleLabel, aCompressOffset);
+}
+
+Core::AppendOutcome Core::TxMessage::AppendMultipleLabels(Section     aSection,
+                                                          const char *aLabels,
+                                                          uint16_t   &aCompressOffset)
+{
+    return AppendLabels(aSection, aLabels, !kIsSingleLabel, aCompressOffset);
+}
+
+Core::AppendOutcome Core::TxMessage::AppendLabels(Section     aSection,
+                                                  const char *aLabels,
+                                                  bool        aIsSingleLabel,
+                                                  uint16_t   &aCompressOffset)
+{
+    // Appends DNS name label(s) to the message in the specified section,
+    // using compression if possible.
+    //
+    // - If a valid `aCompressOffset` is given (indicating name was appended before)
+    //   a compressed pointer label is used, and `kAppendedFullNameAsCompressed`
+    //   is returned.
+    // - Otherwise, `aLabels` is appended, `aCompressOffset` is also updated for
+    //   future compression, and `kAppendedLabels` is returned.
+    //
+    // `aIsSingleLabel` indicates that `aLabels` string should be appended
+    // as a single label. This is useful for service instance label which
+    // can itself contain the dot `.` character.
+
+    AppendOutcome outcome = kAppendedLabels;
+    Message      &message = SelectMessageFor(aSection);
+
+    if (aCompressOffset != kUnspecifiedOffset)
+    {
+        SuccessOrAssert(Name::AppendPointerLabel(aCompressOffset, message));
+        outcome = kAppendedFullNameAsCompressed;
+        ExitNow();
+    }
+
+    SaveOffset(aCompressOffset, message, aSection);
+
+    if (aIsSingleLabel)
+    {
+        SuccessOrAssert(Name::AppendLabel(aLabels, message));
+    }
+    else
+    {
+        SuccessOrAssert(Name::AppendMultipleLabels(aLabels, message));
+    }
+
+exit:
+    return outcome;
+}
+
+void Core::TxMessage::AppendServiceType(Section aSection, const char *aServiceType, uint16_t &aCompressOffset)
+{
+    // Appends DNS service type name to the message in the specified
+    // section, using compression if possible.
+
+    const char   *serviceLabels = aServiceType;
+    bool          isUdp         = false;
+    bool          isTcp         = false;
+    Name::Buffer  labelsBuffer;
+    AppendOutcome outcome;
+
+    if (Name::ExtractLabels(serviceLabels, kUdpServiceLabel, labelsBuffer) == kErrorNone)
+    {
+        isUdp         = true;
+        serviceLabels = labelsBuffer;
+    }
+    else if (Name::ExtractLabels(serviceLabels, kTcpServiceLabel, labelsBuffer) == kErrorNone)
+    {
+        isTcp         = true;
+        serviceLabels = labelsBuffer;
+    }
+
+    outcome = AppendMultipleLabels(aSection, serviceLabels, aCompressOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    if (isUdp)
+    {
+        outcome = AppendLabel(aSection, kUdpServiceLabel, mUdpOffset);
+    }
+    else if (isTcp)
+    {
+        outcome = AppendLabel(aSection, kTcpServiceLabel, mTcpOffset);
+    }
+
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    AppendDomainName(aSection);
+
+exit:
+    return;
+}
+
+void Core::TxMessage::AppendDomainName(Section aSection)
+{
+    Message &message = SelectMessageFor(aSection);
+
+    if (mDomainOffset != kUnspecifiedOffset)
+    {
+        SuccessOrAssert(Name::AppendPointerLabel(mDomainOffset, message));
+        ExitNow();
+    }
+
+    SaveOffset(mDomainOffset, message, aSection);
+    SuccessOrAssert(Name::AppendName(kLocalDomain, message));
+
+exit:
+    return;
+}
+
+void Core::TxMessage::AppendServicesDnssdName(Section aSection)
+{
+    Message &message = SelectMessageFor(aSection);
+
+    if (mServicesDnssdOffset != kUnspecifiedOffset)
+    {
+        SuccessOrAssert(Name::AppendPointerLabel(mServicesDnssdOffset, message));
+        ExitNow();
+    }
+
+    SaveOffset(mServicesDnssdOffset, message, aSection);
+    SuccessOrAssert(Name::AppendMultipleLabels(kServicesDnssdLabels, message));
+    AppendDomainName(aSection);
+
+exit:
+    return;
+}
+
+void Core::TxMessage::SaveOffset(uint16_t &aCompressOffset, const Message &aMessage, Section aSection)
+{
+    // Saves the current message offset in `aCompressOffset` for name
+    // compression, but only when appending to the question or answer
+    // sections.
+    //
+    // This is necessary because other sections use separate message,
+    // and their offsets can shift when records are added to the main
+    // message.
+    //
+    // While current record types guarantee name inclusion in
+    // question/answer sections before their use in other sections,
+    // this check allows future extensions.
+
+    switch (aSection)
+    {
+    case kQuestionSection:
+    case kAnswerSection:
+        aCompressOffset = aMessage.GetLength();
+        break;
+
+    case kAuthoritySection:
+    case kAdditionalDataSection:
+        break;
+    }
+}
+
+bool Core::TxMessage::IsOverSizeLimit(void) const
+{
+    uint32_t size = mMsgPtr->GetLength();
+
+    if (!mExtraMsgPtr.IsNull())
+    {
+        size += mExtraMsgPtr->GetLength();
+    }
+
+    return (size > Get<Core>().mMaxMessageSize);
+}
+
+void Core::TxMessage::SaveCurrentState(void)
+{
+    mSavedRecordCounts   = mRecordCounts;
+    mSavedMsgLength      = mMsgPtr->GetLength();
+    mSavedExtraMsgLength = mExtraMsgPtr.IsNull() ? 0 : mExtraMsgPtr->GetLength();
+}
+
+void Core::TxMessage::RestoreToSavedState(void)
+{
+    mRecordCounts = mSavedRecordCounts;
+
+    IgnoreError(mMsgPtr->SetLength(mSavedMsgLength));
+
+    if (!mExtraMsgPtr.IsNull())
+    {
+        IgnoreError(mExtraMsgPtr->SetLength(mSavedExtraMsgLength));
+    }
+}
+
+void Core::TxMessage::CheckSizeLimitToPrepareAgain(bool &aPrepareAgain)
+{
+    // Manages message size limits by re-preparing messages when
+    // necessary:
+    // - Checks if `TxMessage` exceeds the size limit.
+    // - If so, restores the `TxMessage` to its previously saved
+    //   state, sends it, and re-initializes it which will also
+    //   clear the "AppendState" of the related host and service
+    //   entries to ensure correct re-processing.
+    // - Sets `aPrepareAgain` to `true` to signal that records should
+    //   be prepared and added to the new message.
+    //
+    // We allow the `aPrepareAgain` to happen once. The very unlikely
+    // case where the `Entry` itself has so many records that its
+    // contents exceed the message size limit, is not handled, i.e.
+    // we always include all records of a single `Entry` within the same
+    // message. In future, the code can be updated to allow truncated
+    // messages.
+
+    if (aPrepareAgain)
+    {
+        aPrepareAgain = false;
+        ExitNow();
+    }
+
+    VerifyOrExit(IsOverSizeLimit());
+
+    aPrepareAgain = true;
+
+    RestoreToSavedState();
+    Send();
+    Reinit();
+
+exit:
+    return;
+}
+
+void Core::TxMessage::Send(void)
+{
+    static constexpr uint16_t kHeaderOffset = 0;
+
+    Header header;
+
+    VerifyOrExit(!mRecordCounts.IsEmpty());
+
+    SuccessOrAssert(mMsgPtr->Read(kHeaderOffset, header));
+    mRecordCounts.WriteTo(header);
+    mMsgPtr->Write(kHeaderOffset, header);
+
+    if (!mExtraMsgPtr.IsNull())
+    {
+        SuccessOrAssert(mMsgPtr->AppendBytesFromMessage(*mExtraMsgPtr, 0, mExtraMsgPtr->GetLength()));
+    }
+
+    Get<Core>().mTxMessageHistory.Add(*mMsgPtr);
+
+    // We pass ownership of message to the platform layer.
+
+    switch (mType)
+    {
+    case kMulticastProbe:
+    case kMulticastQuery:
+    case kMulticastResponse:
+        otPlatMdnsSendMulticast(&GetInstance(), mMsgPtr.Release(), Get<Core>().mInfraIfIndex);
+        break;
+
+    case kUnicastResponse:
+        otPlatMdnsSendUnicast(&GetInstance(), mMsgPtr.Release(), &mUnicastDest);
+        break;
+    }
+
+exit:
+    return;
+}
+
+void Core::TxMessage::Reinit(void)
+{
+    Init(GetType());
+
+    // After re-initializing `TxMessage`, we clear the "AppendState"
+    // on all related host and service entries, and service types
+    // or all cache entries (depending on the `GetType()`).
+
+    switch (GetType())
+    {
+    case kMulticastProbe:
+    case kMulticastResponse:
+    case kUnicastResponse:
+        for (HostEntry &entry : Get<Core>().mHostEntries)
+        {
+            if (ShouldClearAppendStateOnReinit(entry))
+            {
+                entry.ClearAppendState();
+            }
+        }
+
+        for (ServiceEntry &entry : Get<Core>().mServiceEntries)
+        {
+            if (ShouldClearAppendStateOnReinit(entry))
+            {
+                entry.ClearAppendState();
+            }
+        }
+
+        for (ServiceType &serviceType : Get<Core>().mServiceTypes)
+        {
+            if ((GetType() == kMulticastResponse) || (GetType() == kUnicastResponse))
+            {
+                serviceType.ClearAppendState();
+            }
+        }
+
+        break;
+
+    case kMulticastQuery:
+
+        for (BrowseCache &browseCache : Get<Core>().mBrowseCacheList)
+        {
+            browseCache.ClearCompressOffsets();
+        }
+
+        for (SrvCache &srvCache : Get<Core>().mSrvCacheList)
+        {
+            srvCache.ClearCompressOffsets();
+        }
+
+        for (TxtCache &txtCache : Get<Core>().mTxtCacheList)
+        {
+            txtCache.ClearCompressOffsets();
+        }
+
+        // `Ip6AddrCache` entries do not track any append state or
+        // compress offset since the host name should not be used
+        // in any other query question.
+
+        break;
+    }
+}
+
+bool Core::TxMessage::ShouldClearAppendStateOnReinit(const Entry &aEntry) const
+{
+    // Determines whether we should clear "append state" on `aEntry`
+    // when re-initializing the `TxMessage`. If message is a probe, we
+    // check that entry is in `kProbing` state, if message is a
+    // unicast/multicast response, we check for `kRegistered` state.
+
+    bool shouldClear = false;
+
+    switch (aEntry.GetState())
+    {
+    case Entry::kProbing:
+        shouldClear = (GetType() == kMulticastProbe);
+        break;
+
+    case Entry::kRegistered:
+        shouldClear = (GetType() == kMulticastResponse) || (GetType() == kUnicastResponse);
+        break;
+
+    case Entry::kConflict:
+    case Entry::kRemoving:
+        shouldClear = true;
+        break;
+    }
+
+    return shouldClear;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::TimerContext
+
+Core::TimerContext::TimerContext(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mNow(TimerMilli::GetNow())
+    , mNextTime(mNow.GetDistantFuture())
+{
+}
+
+void Core::TimerContext::UpdateNextTime(TimeMilli aTime)
+{
+    if (aTime <= mNow)
+    {
+        mNextTime = mNow;
+    }
+    else
+    {
+        mNextTime = Min(mNextTime, aTime);
+    }
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::EntryTimerContext
+
+Core::EntryTimerContext::EntryTimerContext(Instance &aInstance)
+    : TimerContext(aInstance)
+    , mProbeMessage(aInstance, TxMessage::kMulticastProbe)
+    , mResponseMessage(aInstance, TxMessage::kMulticastResponse)
+{
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// Core::RxMessage
+
+Error Core::RxMessage::Init(Instance          &aInstance,
+                            OwnedPtr<Message> &aMessagePtr,
+                            bool               aIsUnicast,
+                            const AddressInfo &aSenderAddress)
+{
+    static const Section kSections[] = {kAnswerSection, kAuthoritySection, kAdditionalDataSection};
+
+    Error    error = kErrorNone;
+    Header   header;
+    uint16_t offset;
+    uint16_t numRecords;
+
+    InstanceLocatorInit::Init(aInstance);
+
+    mNext = nullptr;
+
+    VerifyOrExit(!aMessagePtr.IsNull(), error = kErrorInvalidArgs);
+
+    offset = aMessagePtr->GetOffset();
+
+    SuccessOrExit(error = aMessagePtr->Read(offset, header));
+    offset += sizeof(Header);
+
+    // RFC 6762 Section 18: Query type (OPCODE) must be zero
+    // (standard query). All other flags must be ignored. Messages
+    // with non-zero RCODE MUST be silently ignored.
+
+    VerifyOrExit(header.GetQueryType() == Header::kQueryTypeStandard, error = kErrorParse);
+    VerifyOrExit(header.GetResponseCode() == Header::kResponseSuccess, error = kErrorParse);
+
+    mIsQuery       = (header.GetType() == Header::kTypeQuery);
+    mIsUnicast     = aIsUnicast;
+    mTruncated     = header.IsTruncationFlagSet();
+    mSenderAddress = aSenderAddress;
+
+    if (aSenderAddress.mPort != kUdpPort)
+    {
+        if (mIsQuery)
+        {
+            // Section 6.7 Legacy Unicast
+            LogInfo("We do not yet support legacy unicast message (source port not matching mDNS port)");
+            ExitNow(error = kErrorNotCapable);
+        }
+        else
+        {
+            // The source port in a response MUST be mDNS port.
+            // Otherwise response message MUST be silently ignored.
+
+            ExitNow(error = kErrorParse);
+        }
+    }
+
+    if (mIsUnicast && mIsQuery)
+    {
+        // Direct Unicast Queries to Port 5353 (RFC 6762 - section 5.5).
+        // Responders SHOULD check that the source address in the query
+        // packet matches the local subnet for that link and silently ignore
+        // the packet if not.
+
+        LogInfo("We do not yet support unicast query to mDNS port");
+        ExitNow(error = kErrorNotCapable);
+    }
+
+    mRecordCounts.ReadFrom(header);
+
+    // Parse questions
+
+    mStartOffset[kQuestionSection] = offset;
+
+    SuccessOrAssert(mQuestions.ReserveCapacity(mRecordCounts.GetFor(kQuestionSection)));
+
+    for (numRecords = mRecordCounts.GetFor(kQuestionSection); numRecords > 0; numRecords--)
+    {
+        Question         *question = mQuestions.PushBack();
+        ot::Dns::Question record;
+        uint16_t          rrClass;
+
+        OT_ASSERT(question != nullptr);
+
+        question->mNameOffset = offset;
+
+        SuccessOrExit(error = Name::ParseName(*aMessagePtr, offset));
+        SuccessOrExit(error = aMessagePtr->Read(offset, record));
+        offset += sizeof(record);
+
+        question->mRrType = record.GetType();
+
+        rrClass                      = record.GetClass();
+        question->mUnicastResponse   = rrClass & kClassQuestionUnicastFlag;
+        question->mIsRrClassInternet = RrClassIsInternetOrAny(rrClass);
+    }
+
+    // Parse and validate records in Answer, Authority and Additional
+    // Data sections.
+
+    for (Section section : kSections)
+    {
+        mStartOffset[section] = offset;
+        SuccessOrExit(error = ResourceRecord::ParseRecords(*aMessagePtr, offset, mRecordCounts.GetFor(section)));
+    }
+
+    // Determine which questions are probes by searching in the
+    // Authority section for records matching the question name.
+
+    for (Question &question : mQuestions)
+    {
+        Name name(*aMessagePtr, question.mNameOffset);
+
+        offset     = mStartOffset[kAuthoritySection];
+        numRecords = mRecordCounts.GetFor(kAuthoritySection);
+
+        if (ResourceRecord::FindRecord(*aMessagePtr, offset, numRecords, name) == kErrorNone)
+        {
+            question.mIsProbe = true;
+        }
+    }
+
+    mIsSelfOriginating = Get<Core>().mTxMessageHistory.Contains(*aMessagePtr);
+
+    mMessagePtr = aMessagePtr.PassOwnership();
+
+exit:
+    if (error != kErrorNone)
+    {
+        LogInfo("Failed to parse message from %s, error:%s", aSenderAddress.GetAddress().ToString().AsCString(),
+                ErrorToString(error));
+    }
+
+    return error;
+}
+
+void Core::RxMessage::ClearProcessState(void)
+{
+    for (Question &question : mQuestions)
+    {
+        question.ClearProcessState();
+    }
+}
+
+Core::RxMessage::ProcessOutcome Core::RxMessage::ProcessQuery(bool aShouldProcessTruncated)
+{
+    ProcessOutcome outcome             = kProcessed;
+    bool           shouldDelay         = false;
+    bool           canAnswer           = false;
+    bool           needUnicastResponse = false;
+    TimeMilli      answerTime;
+
+    for (Question &question : mQuestions)
+    {
+        question.ClearProcessState();
+
+        ProcessQuestion(question);
+
+        // Check if we can answer every question in the query and all
+        // answers are for unique records (where we own the name). This
+        // determines whether we need to add any random delay before
+        // responding.
+
+        if (!question.mCanAnswer || !question.mIsUnique)
+        {
+            shouldDelay = true;
+        }
+
+        if (question.mCanAnswer)
+        {
+            canAnswer = true;
+
+            if (question.mUnicastResponse)
+            {
+                needUnicastResponse = true;
+            }
+        }
+    }
+
+    VerifyOrExit(canAnswer);
+
+    if (mTruncated && !aShouldProcessTruncated)
+    {
+        outcome = kSaveAsMultiPacket;
+        ExitNow();
+    }
+
+    answerTime = TimerMilli::GetNow();
+
+    if (shouldDelay)
+    {
+        answerTime += Random::NonCrypto::GetUint32InRange(kMinResponseDelay, kMaxResponseDelay);
+    }
+
+    for (const Question &question : mQuestions)
+    {
+        AnswerQuestion(question, answerTime);
+    }
+
+    if (needUnicastResponse)
+    {
+        SendUnicastResponse(mSenderAddress);
+    }
+
+exit:
+    return outcome;
+}
+
+void Core::RxMessage::ProcessQuestion(Question &aQuestion)
+{
+    Name name(*mMessagePtr, aQuestion.mNameOffset);
+
+    VerifyOrExit(aQuestion.mIsRrClassInternet);
+
+    // Check if question name matches "_services._dns-sd._udp" (all services)
+
+    if (name.Matches(/* aFirstLabel */ nullptr, kServicesDnssdLabels, kLocalDomain))
+    {
+        VerifyOrExit(QuestionMatches(aQuestion.mRrType, ResourceRecord::kTypePtr));
+        VerifyOrExit(!Get<Core>().mServiceTypes.IsEmpty());
+
+        aQuestion.mCanAnswer             = true;
+        aQuestion.mIsForAllServicesDnssd = true;
+
+        ExitNow();
+    }
+
+    // Check if question name matches a `HostEntry` or a `ServiceEntry`
+
+    aQuestion.mEntry = Get<Core>().mHostEntries.FindMatching(name);
+
+    if (aQuestion.mEntry == nullptr)
+    {
+        aQuestion.mEntry        = Get<Core>().mServiceEntries.FindMatching(name);
+        aQuestion.mIsForService = (aQuestion.mEntry != nullptr);
+    }
+
+    if (aQuestion.mEntry != nullptr)
+    {
+        switch (aQuestion.mEntry->GetState())
+        {
+        case Entry::kProbing:
+            if (aQuestion.mIsProbe)
+            {
+                // Handling probe conflicts deviates from RFC 6762.
+                // We allow the conflict to happen and report it
+                // let the caller handle it. In future, TSR can
+                // help select the winner.
+            }
+            break;
+
+        case Entry::kRegistered:
+            aQuestion.mCanAnswer = true;
+            aQuestion.mIsUnique  = true;
+            break;
+
+        case Entry::kConflict:
+        case Entry::kRemoving:
+            break;
+        }
+    }
+    else
+    {
+        // Check if question matches a service type or sub-type. We
+        // can answer PTR or ANY questions. There may be multiple
+        // service entries matching the question. We find and save
+        // the first match. `AnswerServiceTypeQuestion()` will start
+        // from the saved entry and finds all the other matches.
+
+        bool              isSubType;
+        Name::LabelBuffer subLabel;
+        Name              baseType;
+
+        VerifyOrExit(QuestionMatches(aQuestion.mRrType, ResourceRecord::kTypePtr));
+
+        isSubType = ParseQuestionNameAsSubType(aQuestion, subLabel, baseType);
+
+        if (!isSubType)
+        {
+            baseType = name;
+        }
+
+        for (ServiceEntry &serviceEntry : Get<Core>().mServiceEntries)
+        {
+            if ((serviceEntry.GetState() != Entry::kRegistered) || !serviceEntry.MatchesServiceType(baseType))
+            {
+                continue;
+            }
+
+            if (isSubType && !serviceEntry.CanAnswerSubType(subLabel))
+            {
+                continue;
+            }
+
+            aQuestion.mCanAnswer     = true;
+            aQuestion.mEntry         = &serviceEntry;
+            aQuestion.mIsForService  = true;
+            aQuestion.mIsServiceType = true;
+            ExitNow();
+        }
+    }
+
+exit:
+    return;
+}
+
+void Core::RxMessage::AnswerQuestion(const Question &aQuestion, TimeMilli aAnswerTime)
+{
+    HostEntry    *hostEntry;
+    ServiceEntry *serviceEntry;
+    AnswerInfo    answerInfo;
+
+    VerifyOrExit(aQuestion.mCanAnswer);
+
+    answerInfo.mQuestionRrType  = aQuestion.mRrType;
+    answerInfo.mAnswerTime      = aAnswerTime;
+    answerInfo.mIsProbe         = aQuestion.mIsProbe;
+    answerInfo.mUnicastResponse = aQuestion.mUnicastResponse;
+
+    if (aQuestion.mIsForAllServicesDnssd)
+    {
+        AnswerAllServicesQuestion(aQuestion, answerInfo);
+        ExitNow();
+    }
+
+    hostEntry    = aQuestion.mIsForService ? nullptr : static_cast<HostEntry *>(aQuestion.mEntry);
+    serviceEntry = aQuestion.mIsForService ? static_cast<ServiceEntry *>(aQuestion.mEntry) : nullptr;
+
+    if (hostEntry != nullptr)
+    {
+        hostEntry->AnswerQuestion(answerInfo);
+        ExitNow();
+    }
+
+    // Question is for `ServiceEntry`
+
+    VerifyOrExit(serviceEntry != nullptr);
+
+    if (!aQuestion.mIsServiceType)
+    {
+        serviceEntry->AnswerServiceNameQuestion(answerInfo);
+    }
+    else
+    {
+        AnswerServiceTypeQuestion(aQuestion, answerInfo, *serviceEntry);
+    }
+
+exit:
+    return;
+}
+
+void Core::RxMessage::AnswerServiceTypeQuestion(const Question   &aQuestion,
+                                                const AnswerInfo &aInfo,
+                                                ServiceEntry     &aFirstEntry)
+{
+    Name              serviceType(*mMessagePtr, aQuestion.mNameOffset);
+    Name              baseType;
+    Name::LabelBuffer labelBuffer;
+    const char       *subLabel;
+
+    if (ParseQuestionNameAsSubType(aQuestion, labelBuffer, baseType))
+    {
+        subLabel = labelBuffer;
+    }
+    else
+    {
+        baseType = serviceType;
+        subLabel = nullptr;
+    }
+
+    for (ServiceEntry *serviceEntry = &aFirstEntry; serviceEntry != nullptr; serviceEntry = serviceEntry->GetNext())
+    {
+        bool shouldSuppress = false;
+
+        if ((serviceEntry->GetState() != Entry::kRegistered) || !serviceEntry->MatchesServiceType(baseType))
+        {
+            continue;
+        }
+
+        if ((subLabel != nullptr) && !serviceEntry->CanAnswerSubType(subLabel))
+        {
+            continue;
+        }
+
+        // Check for known-answer in this `RxMessage` and all its
+        // related messages in case it is multi-packet query.
+
+        for (const RxMessage *rxMessage = this; rxMessage != nullptr; rxMessage = rxMessage->GetNext())
+        {
+            if (rxMessage->ShouldSuppressKnownAnswer(serviceType, subLabel, *serviceEntry))
+            {
+                shouldSuppress = true;
+                break;
+            }
+        }
+
+        if (!shouldSuppress)
+        {
+            serviceEntry->AnswerServiceTypeQuestion(aInfo, subLabel);
+        }
+    }
+}
+
+bool Core::RxMessage::ShouldSuppressKnownAnswer(const Name         &aServiceType,
+                                                const char         *aSubLabel,
+                                                const ServiceEntry &aServiceEntry) const
+{
+    bool     shouldSuppress = false;
+    uint16_t offset         = mStartOffset[kAnswerSection];
+    uint16_t numRecords     = mRecordCounts.GetFor(kAnswerSection);
+
+    while (ResourceRecord::FindRecord(*mMessagePtr, offset, numRecords, aServiceType) == kErrorNone)
+    {
+        Error     error;
+        PtrRecord ptr;
+
+        error = ResourceRecord::ReadRecord(*mMessagePtr, offset, ptr);
+
+        if (error == kErrorNotFound)
+        {
+            // `ReadRecord()` will update the `offset` to skip over
+            // the entire record if it does not match the expected
+            // record type (PTR in this case).
+            continue;
+        }
+
+        SuccessOrExit(error);
+
+        // `offset` is now pointing to PTR name
+
+        if (aServiceEntry.Matches(Name(*mMessagePtr, offset)))
+        {
+            shouldSuppress = aServiceEntry.ShouldSuppressKnownAnswer(ptr.GetTtl(), aSubLabel);
+            ExitNow();
+        }
+
+        // Parse the name and skip over it and update `offset`
+        // to the start of the next record.
+
+        SuccessOrExit(Name::ParseName(*mMessagePtr, offset));
+    }
+
+exit:
+    return shouldSuppress;
+}
+
+bool Core::RxMessage::ParseQuestionNameAsSubType(const Question    &aQuestion,
+                                                 Name::LabelBuffer &aSubLabel,
+                                                 Name              &aServiceType) const
+{
+    bool     isSubType = false;
+    uint16_t offset    = aQuestion.mNameOffset;
+    uint8_t  length    = sizeof(aSubLabel);
+
+    SuccessOrExit(Name::ReadLabel(*mMessagePtr, offset, aSubLabel, length));
+    SuccessOrExit(Name::CompareLabel(*mMessagePtr, offset, kSubServiceLabel));
+    aServiceType.SetFromMessage(*mMessagePtr, offset);
+    isSubType = true;
+
+exit:
+    return isSubType;
+}
+
+void Core::RxMessage::AnswerAllServicesQuestion(const Question &aQuestion, const AnswerInfo &aInfo)
+{
+    for (ServiceType &serviceType : Get<Core>().mServiceTypes)
+    {
+        bool shouldSuppress = false;
+
+        // Check for known-answer in this `RxMessage` and all its
+        // related messages in case it is multi-packet query.
+
+        for (const RxMessage *rxMessage = this; rxMessage != nullptr; rxMessage = rxMessage->GetNext())
+        {
+            if (rxMessage->ShouldSuppressKnownAnswer(aQuestion, serviceType))
+            {
+                shouldSuppress = true;
+                break;
+            }
+        }
+
+        if (!shouldSuppress)
+        {
+            serviceType.AnswerQuestion(aInfo);
+        }
+    }
+}
+
+bool Core::RxMessage::ShouldSuppressKnownAnswer(const Question &aQuestion, const ServiceType &aServiceType) const
+{
+    // Check answer section to determine whether to suppress answering
+    // to "_services._dns-sd._udp" query with `aServiceType`
+
+    bool     shouldSuppress = false;
+    uint16_t offset         = mStartOffset[kAnswerSection];
+    uint16_t numRecords     = mRecordCounts.GetFor(kAnswerSection);
+    Name     name(*mMessagePtr, aQuestion.mNameOffset);
+
+    while (ResourceRecord::FindRecord(*mMessagePtr, offset, numRecords, name) == kErrorNone)
+    {
+        Error     error;
+        PtrRecord ptr;
+
+        error = ResourceRecord::ReadRecord(*mMessagePtr, offset, ptr);
+
+        if (error == kErrorNotFound)
+        {
+            // `ReadRecord()` will update the `offset` to skip over
+            // the entire record if it does not match the expected
+            // record type (PTR in this case).
+            continue;
+        }
+
+        SuccessOrExit(error);
+
+        // `offset` is now pointing to PTR name
+
+        if (aServiceType.Matches(Name(*mMessagePtr, offset)))
+        {
+            shouldSuppress = aServiceType.ShouldSuppressKnownAnswer(ptr.GetTtl());
+            ExitNow();
+        }
+
+        // Parse the name and skip over it and update `offset`
+        // to the start of the next record.
+
+        SuccessOrExit(Name::ParseName(*mMessagePtr, offset));
+    }
+
+exit:
+    return shouldSuppress;
+}
+
+void Core::RxMessage::SendUnicastResponse(const AddressInfo &aUnicastDest)
+{
+    TxMessage response(GetInstance(), TxMessage::kUnicastResponse, aUnicastDest);
+    TimeMilli now = TimerMilli::GetNow();
+
+    for (HostEntry &entry : Get<Core>().mHostEntries)
+    {
+        entry.ClearAppendState();
+        entry.PrepareResponse(response, now);
+    }
+
+    for (ServiceEntry &entry : Get<Core>().mServiceEntries)
+    {
+        entry.ClearAppendState();
+        entry.PrepareResponse(response, now);
+    }
+
+    for (ServiceType &serviceType : Get<Core>().mServiceTypes)
+    {
+        serviceType.ClearAppendState();
+        serviceType.PrepareResponse(response, now);
+    }
+
+    response.Send();
+}
+
+void Core::RxMessage::ProcessResponse(void)
+{
+    if (!IsSelfOriginating())
+    {
+        IterateOnAllRecordsInResponse(&RxMessage::ProcessRecordForConflict);
+    }
+
+    // We process record types in a specific order to ensure correct
+    // passive cache creation: First PTR records are processed, which
+    // may create passive SRV/TXT cache entries for discovered
+    // services. Next SRV records are processed which may create TXT
+    // cache entries for service names and IPv6 address cache entries
+    // for associated host name.
+
+    if (!Get<Core>().mBrowseCacheList.IsEmpty())
+    {
+        IterateOnAllRecordsInResponse(&RxMessage::ProcessPtrRecord);
+    }
+
+    if (!Get<Core>().mSrvCacheList.IsEmpty())
+    {
+        IterateOnAllRecordsInResponse(&RxMessage::ProcessSrvRecord);
+    }
+
+    if (!Get<Core>().mTxtCacheList.IsEmpty())
+    {
+        IterateOnAllRecordsInResponse(&RxMessage::ProcessTxtRecord);
+    }
+
+    if (!Get<Core>().mIp6AddrCacheList.IsEmpty())
+    {
+        IterateOnAllRecordsInResponse(&RxMessage::ProcessAaaaRecord);
+
+        for (Ip6AddrCache &addrCache : Get<Core>().mIp6AddrCacheList)
+        {
+            addrCache.CommitNewResponseEntries();
+        }
+    }
+
+    if (!Get<Core>().mIp4AddrCacheList.IsEmpty())
+    {
+        IterateOnAllRecordsInResponse(&RxMessage::ProcessARecord);
+
+        for (Ip4AddrCache &addrCache : Get<Core>().mIp4AddrCacheList)
+        {
+            addrCache.CommitNewResponseEntries();
+        }
+    }
+}
+
+void Core::RxMessage::IterateOnAllRecordsInResponse(RecordProcessor aRecordProcessor)
+{
+    // Iterates over all records in the response, calling
+    // `aRecordProcessor` for each.
+
+    static const Section kSections[] = {kAnswerSection, kAdditionalDataSection};
+
+    for (Section section : kSections)
+    {
+        uint16_t offset = mStartOffset[section];
+
+        for (uint16_t numRecords = mRecordCounts.GetFor(section); numRecords > 0; numRecords--)
+        {
+            Name           name(*mMessagePtr, offset);
+            ResourceRecord record;
+
+            IgnoreError(Name::ParseName(*mMessagePtr, offset));
+            IgnoreError(mMessagePtr->Read(offset, record));
+
+            if (!RrClassIsInternetOrAny(record.GetClass()))
+            {
+                continue;
+            }
+
+            (this->*aRecordProcessor)(name, record, offset);
+
+            offset += static_cast<uint16_t>(record.GetSize());
+        }
+    }
+}
+
+void Core::RxMessage::ProcessRecordForConflict(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset)
+{
+    HostEntry    *hostEntry;
+    ServiceEntry *serviceEntry;
+
+    VerifyOrExit(aRecord.GetTtl() > 0);
+
+    hostEntry = Get<Core>().mHostEntries.FindMatching(aName);
+
+    if (hostEntry != nullptr)
+    {
+        hostEntry->HandleConflict();
+    }
+
+    serviceEntry = Get<Core>().mServiceEntries.FindMatching(aName);
+
+    if (serviceEntry != nullptr)
+    {
+        serviceEntry->HandleConflict();
+    }
+
+exit:
+    OT_UNUSED_VARIABLE(aRecordOffset);
+}
+
+void Core::RxMessage::ProcessPtrRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset)
+{
+    BrowseCache *browseCache;
+
+    VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypePtr);
+
+    browseCache = Get<Core>().mBrowseCacheList.FindMatching(aName);
+    VerifyOrExit(browseCache != nullptr);
+
+    browseCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset);
+
+exit:
+    return;
+}
+
+void Core::RxMessage::ProcessSrvRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset)
+{
+    SrvCache *srvCache;
+
+    VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeSrv);
+
+    srvCache = Get<Core>().mSrvCacheList.FindMatching(aName);
+    VerifyOrExit(srvCache != nullptr);
+
+    srvCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset);
+
+exit:
+    return;
+}
+
+void Core::RxMessage::ProcessTxtRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset)
+{
+    TxtCache *txtCache;
+
+    VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeTxt);
+
+    txtCache = Get<Core>().mTxtCacheList.FindMatching(aName);
+    VerifyOrExit(txtCache != nullptr);
+
+    txtCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset);
+
+exit:
+    return;
+}
+
+void Core::RxMessage::ProcessAaaaRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset)
+{
+    Ip6AddrCache *ip6AddrCache;
+
+    VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeAaaa);
+
+    ip6AddrCache = Get<Core>().mIp6AddrCacheList.FindMatching(aName);
+    VerifyOrExit(ip6AddrCache != nullptr);
+
+    ip6AddrCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset);
+
+exit:
+    return;
+}
+
+void Core::RxMessage::ProcessARecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset)
+{
+    Ip4AddrCache *ip4AddrCache;
+
+    VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeA);
+
+    ip4AddrCache = Get<Core>().mIp4AddrCacheList.FindMatching(aName);
+    VerifyOrExit(ip4AddrCache != nullptr);
+
+    ip4AddrCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset);
+
+exit:
+    return;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::RxMessage::Question
+
+void Core::RxMessage::Question::ClearProcessState(void)
+{
+    mCanAnswer             = false;
+    mIsUnique              = false;
+    mIsForService          = false;
+    mIsServiceType         = false;
+    mIsForAllServicesDnssd = false;
+    mEntry                 = nullptr;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::MultiPacketRxMessages
+
+Core::MultiPacketRxMessages::MultiPacketRxMessages(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mTimer(aInstance)
+{
+}
+
+void Core::MultiPacketRxMessages::AddToExisting(OwnedPtr<RxMessage> &aRxMessagePtr)
+{
+    RxMsgEntry *msgEntry = mRxMsgEntries.FindMatching(aRxMessagePtr->GetSenderAddress());
+
+    VerifyOrExit(msgEntry != nullptr);
+    msgEntry->Add(aRxMessagePtr);
+
+exit:
+    return;
+}
+
+void Core::MultiPacketRxMessages::AddNew(OwnedPtr<RxMessage> &aRxMessagePtr)
+{
+    RxMsgEntry *newEntry = RxMsgEntry::Allocate(GetInstance());
+
+    OT_ASSERT(newEntry != nullptr);
+    newEntry->Add(aRxMessagePtr);
+
+    // First remove an existing entries matching same sender
+    // before adding the new entry to the list.
+
+    mRxMsgEntries.RemoveMatching(aRxMessagePtr->GetSenderAddress());
+    mRxMsgEntries.Push(*newEntry);
+}
+
+void Core::MultiPacketRxMessages::HandleTimer(void)
+{
+    TimeMilli              now      = TimerMilli::GetNow();
+    TimeMilli              nextTime = now.GetDistantFuture();
+    OwningList<RxMsgEntry> expiredEntries;
+
+    mRxMsgEntries.RemoveAllMatching(ExpireChecker(now), expiredEntries);
+
+    for (RxMsgEntry &expiredEntry : expiredEntries)
+    {
+        expiredEntry.mRxMessages.GetHead()->ProcessQuery(/* aShouldProcessTruncated */ true);
+    }
+
+    for (const RxMsgEntry &msgEntry : mRxMsgEntries)
+    {
+        nextTime = Min(nextTime, msgEntry.mProcessTime);
+    }
+
+    if (nextTime != now.GetDistantFuture())
+    {
+        mTimer.FireAtIfEarlier(nextTime);
+    }
+}
+
+void Core::MultiPacketRxMessages::Clear(void)
+{
+    mTimer.Stop();
+    mRxMsgEntries.Clear();
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::MultiPacketRxMessage::RxMsgEntry
+
+Core::MultiPacketRxMessages::RxMsgEntry::RxMsgEntry(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mNext(nullptr)
+{
+}
+
+bool Core::MultiPacketRxMessages::RxMsgEntry::Matches(const AddressInfo &aAddress) const
+{
+    bool matches = false;
+
+    VerifyOrExit(!mRxMessages.IsEmpty());
+    matches = (mRxMessages.GetHead()->GetSenderAddress() == aAddress);
+
+exit:
+    return matches;
+}
+
+bool Core::MultiPacketRxMessages::RxMsgEntry::Matches(const ExpireChecker &aExpireChecker) const
+{
+    return (mProcessTime <= aExpireChecker.mNow);
+}
+
+void Core::MultiPacketRxMessages::RxMsgEntry::Add(OwnedPtr<RxMessage> &aRxMessagePtr)
+{
+    uint16_t numMsgs = 0;
+
+    for (const RxMessage &rxMsg : mRxMessages)
+    {
+        // If a subsequent received `RxMessage` is also marked as
+        // truncated, we again delay the process time. To avoid
+        // continuous delay and piling up of messages in the list,
+        // we limit the number of messages.
+
+        numMsgs++;
+        VerifyOrExit(numMsgs < kMaxNumMessages);
+
+        OT_UNUSED_VARIABLE(rxMsg);
+    }
+
+    mProcessTime = TimerMilli::GetNow();
+
+    if (aRxMessagePtr->IsTruncated())
+    {
+        mProcessTime += Random::NonCrypto::GetUint32InRange(kMinProcessDelay, kMaxProcessDelay);
+    }
+
+    // We push the new `RxMessage` at tail of the list to keep the
+    // first query containing questions at the head of the list.
+
+    mRxMessages.PushAfterTail(*aRxMessagePtr.Release());
+
+    Get<Core>().mMultiPacketRxMessages.mTimer.FireAtIfEarlier(mProcessTime);
+
+exit:
+    return;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::TxMessageHistory
+
+Core::TxMessageHistory::TxMessageHistory(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mTimer(aInstance)
+{
+}
+
+void Core::TxMessageHistory::Clear(void)
+{
+    mHashEntries.Clear();
+    mTimer.Stop();
+}
+
+void Core::TxMessageHistory::Add(const Message &aMessage)
+{
+    Hash       hash;
+    HashEntry *entry;
+
+    CalculateHash(aMessage, hash);
+
+    entry = mHashEntries.FindMatching(hash);
+
+    if (entry == nullptr)
+    {
+        entry = HashEntry::Allocate();
+        OT_ASSERT(entry != nullptr);
+        entry->mHash = hash;
+        mHashEntries.Push(*entry);
+    }
+
+    entry->mExpireTime = TimerMilli::GetNow() + kExpireInterval;
+    mTimer.FireAtIfEarlier(entry->mExpireTime);
+}
+
+bool Core::TxMessageHistory::Contains(const Message &aMessage) const
+{
+    Hash hash;
+
+    CalculateHash(aMessage, hash);
+    return mHashEntries.ContainsMatching(hash);
+}
+
+void Core::TxMessageHistory::CalculateHash(const Message &aMessage, Hash &aHash)
+{
+    Crypto::Sha256 sha256;
+
+    sha256.Start();
+    sha256.Update(aMessage, /* aOffset */ 0, aMessage.GetLength());
+    sha256.Finish(aHash);
+}
+
+void Core::TxMessageHistory::HandleTimer(void)
+{
+    TimeMilli             now      = TimerMilli::GetNow();
+    TimeMilli             nextTime = now.GetDistantFuture();
+    OwningList<HashEntry> expiredEntries;
+
+    mHashEntries.RemoveAllMatching(ExpireChecker(now), expiredEntries);
+
+    for (const HashEntry &entry : mHashEntries)
+    {
+        nextTime = Min(nextTime, entry.mExpireTime);
+    }
+
+    if (nextTime != now.GetDistantFuture())
+    {
+        mTimer.FireAtIfEarlier(nextTime);
+    }
+}
+
+template <typename CacheType, typename BrowserResolverType>
+Error Core::Start(const BrowserResolverType &aBrowserOrResolver)
+{
+    Error      error = kErrorNone;
+    CacheType *cacheEntry;
+
+    VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
+    VerifyOrExit(aBrowserOrResolver.mCallback != nullptr, error = kErrorInvalidArgs);
+
+    cacheEntry = GetCacheList<CacheType>().FindMatching(aBrowserOrResolver);
+
+    if (cacheEntry == nullptr)
+    {
+        cacheEntry = CacheType::AllocateAndInit(GetInstance(), aBrowserOrResolver);
+        OT_ASSERT(cacheEntry != nullptr);
+
+        GetCacheList<CacheType>().Push(*cacheEntry);
+    }
+
+    error = cacheEntry->Add(aBrowserOrResolver);
+
+exit:
+    return error;
+}
+
+template <typename CacheType, typename BrowserResolverType>
+Error Core::Stop(const BrowserResolverType &aBrowserOrResolver)
+{
+    Error      error = kErrorNone;
+    CacheType *cacheEntry;
+
+    VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
+    VerifyOrExit(aBrowserOrResolver.mCallback != nullptr, error = kErrorInvalidArgs);
+
+    cacheEntry = GetCacheList<CacheType>().FindMatching(aBrowserOrResolver);
+    VerifyOrExit(cacheEntry != nullptr);
+
+    cacheEntry->Remove(aBrowserOrResolver);
+
+exit:
+    return error;
+}
+
+Error Core::StartBrowser(const Browser &aBrowser) { return Start<BrowseCache, Browser>(aBrowser); }
+
+Error Core::StopBrowser(const Browser &aBrowser) { return Stop<BrowseCache, Browser>(aBrowser); }
+
+Error Core::StartSrvResolver(const SrvResolver &aResolver) { return Start<SrvCache, SrvResolver>(aResolver); }
+
+Error Core::StopSrvResolver(const SrvResolver &aResolver) { return Stop<SrvCache, SrvResolver>(aResolver); }
+
+Error Core::StartTxtResolver(const TxtResolver &aResolver) { return Start<TxtCache, TxtResolver>(aResolver); }
+
+Error Core::StopTxtResolver(const TxtResolver &aResolver) { return Stop<TxtCache, TxtResolver>(aResolver); }
+
+Error Core::StartIp6AddressResolver(const AddressResolver &aResolver)
+{
+    return Start<Ip6AddrCache, AddressResolver>(aResolver);
+}
+
+Error Core::StopIp6AddressResolver(const AddressResolver &aResolver)
+{
+    return Stop<Ip6AddrCache, AddressResolver>(aResolver);
+}
+
+Error Core::StartIp4AddressResolver(const AddressResolver &aResolver)
+{
+    return Start<Ip4AddrCache, AddressResolver>(aResolver);
+}
+
+Error Core::StopIp4AddressResolver(const AddressResolver &aResolver)
+{
+    return Stop<Ip4AddrCache, AddressResolver>(aResolver);
+}
+
+void Core::AddPassiveSrvTxtCache(const char *aServiceInstance, const char *aServiceType)
+{
+    ServiceName serviceName(aServiceInstance, aServiceType);
+
+    if (!mSrvCacheList.ContainsMatching(serviceName))
+    {
+        SrvCache *srvCache = SrvCache::AllocateAndInit(GetInstance(), serviceName);
+
+        OT_ASSERT(srvCache != nullptr);
+        mSrvCacheList.Push(*srvCache);
+    }
+
+    if (!mTxtCacheList.ContainsMatching(serviceName))
+    {
+        TxtCache *txtCache = TxtCache::AllocateAndInit(GetInstance(), serviceName);
+
+        OT_ASSERT(txtCache != nullptr);
+        mTxtCacheList.Push(*txtCache);
+    }
+}
+
+void Core::AddPassiveIp6AddrCache(const char *aHostName)
+{
+    if (!mIp6AddrCacheList.ContainsMatching(aHostName))
+    {
+        Ip6AddrCache *ip6AddrCache = Ip6AddrCache::AllocateAndInit(GetInstance(), aHostName);
+
+        OT_ASSERT(ip6AddrCache != nullptr);
+        mIp6AddrCacheList.Push(*ip6AddrCache);
+    }
+}
+
+void Core::HandleCacheTimer(void)
+{
+    CacheTimerContext        context(GetInstance());
+    ExpireChecker            expireChecker(context.GetNow());
+    OwningList<BrowseCache>  expiredBrowseList;
+    OwningList<SrvCache>     expiredSrvList;
+    OwningList<TxtCache>     expiredTxtList;
+    OwningList<Ip6AddrCache> expiredIp6AddrList;
+    OwningList<Ip4AddrCache> expiredIp4AddrList;
+
+    // First remove all expired entries.
+
+    mBrowseCacheList.RemoveAllMatching(expireChecker, expiredBrowseList);
+    mSrvCacheList.RemoveAllMatching(expireChecker, expiredSrvList);
+    mTxtCacheList.RemoveAllMatching(expireChecker, expiredTxtList);
+    mIp6AddrCacheList.RemoveAllMatching(expireChecker, expiredIp6AddrList);
+    mIp4AddrCacheList.RemoveAllMatching(expireChecker, expiredIp4AddrList);
+
+    // Process cache types in a specific order to optimize name
+    // compression when constructing query messages.
+
+    for (SrvCache &srvCache : mSrvCacheList)
+    {
+        srvCache.HandleTimer(context);
+    }
+
+    for (TxtCache &txtCache : mTxtCacheList)
+    {
+        txtCache.HandleTimer(context);
+    }
+
+    for (BrowseCache &browseCache : mBrowseCacheList)
+    {
+        browseCache.HandleTimer(context);
+    }
+
+    for (Ip6AddrCache &addrCache : mIp6AddrCacheList)
+    {
+        addrCache.HandleTimer(context);
+    }
+
+    for (Ip4AddrCache &addrCache : mIp4AddrCacheList)
+    {
+        addrCache.HandleTimer(context);
+    }
+
+    context.GetQueryMessage().Send();
+
+    if (context.GetNextTime() != context.GetNow().GetDistantFuture())
+    {
+        mCacheTimer.FireAtIfEarlier(context.GetNextTime());
+    }
+}
+
+void Core::HandleCacheTask(void)
+{
+    // `CacheTask` is used to remove empty/null callbacks
+    // from cache entries. and also removing "passive"
+    // cache entries that timed out.
+
+    for (BrowseCache &browseCache : mBrowseCacheList)
+    {
+        browseCache.ClearEmptyCallbacks();
+    }
+
+    for (SrvCache &srvCache : mSrvCacheList)
+    {
+        srvCache.ClearEmptyCallbacks();
+    }
+
+    for (TxtCache &txtCache : mTxtCacheList)
+    {
+        txtCache.ClearEmptyCallbacks();
+    }
+
+    for (Ip6AddrCache &addrCache : mIp6AddrCacheList)
+    {
+        addrCache.ClearEmptyCallbacks();
+    }
+
+    for (Ip4AddrCache &addrCache : mIp4AddrCacheList)
+    {
+        addrCache.ClearEmptyCallbacks();
+    }
+}
+
+TimeMilli Core::RandomizeFirstProbeTxTime(void)
+{
+    // Randomizes the transmission time of the first probe, adding a
+    // delay between 20-250 msec. Subsequent probes within a short
+    // window reuse the same delay for efficient aggregation.
+
+    TimeMilli now = TimerMilli::GetNow();
+
+    // The comparison using `(mNextProbeTxTime - now)` will work
+    // correctly even in the unlikely case that `now` has wrapped
+    // (49 days has passed) since `mNextProbeTxTime` was last set.
+
+    if ((mNextProbeTxTime - now) >= kMaxProbeDelay)
+    {
+        mNextProbeTxTime = now + Random::NonCrypto::GetUint32InRange(kMinProbeDelay, kMaxProbeDelay);
+    }
+
+    return mNextProbeTxTime;
+}
+
+TimeMilli Core::RandomizeInitialQueryTxTime(void)
+{
+    TimeMilli now = TimerMilli::GetNow();
+
+    if ((mNextQueryTxTime - now) >= kMaxInitialQueryDelay)
+    {
+        mNextQueryTxTime = now + Random::NonCrypto::GetUint32InRange(kMinInitialQueryDelay, kMaxInitialQueryDelay);
+    }
+
+    return mNextQueryTxTime;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::ResultCallback
+
+void Core::ResultCallback::Invoke(Instance &aInstance, const BrowseResult &aResult) const
+{
+    if (mSharedCallback.mBrowse != nullptr)
+    {
+        mSharedCallback.mBrowse(&aInstance, &aResult);
+    }
+}
+
+void Core::ResultCallback::Invoke(Instance &aInstance, const SrvResult &aResult) const
+{
+    if (mSharedCallback.mSrv != nullptr)
+    {
+        mSharedCallback.mSrv(&aInstance, &aResult);
+    }
+}
+
+void Core::ResultCallback::Invoke(Instance &aInstance, const TxtResult &aResult) const
+{
+    if (mSharedCallback.mTxt != nullptr)
+    {
+        mSharedCallback.mTxt(&aInstance, &aResult);
+    }
+}
+
+void Core::ResultCallback::Invoke(Instance &aInstance, const AddressResult &aResult) const
+{
+    if (mSharedCallback.mAddress != nullptr)
+    {
+        mSharedCallback.mAddress(&aInstance, &aResult);
+    }
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::CacheTimerContext
+
+Core::CacheTimerContext::CacheTimerContext(Instance &aInstance)
+    : TimerContext(aInstance)
+    , mQueryMessage(aInstance, TxMessage::kMulticastQuery)
+{
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::CacheRecordInfo
+
+Core::CacheRecordInfo::CacheRecordInfo(void)
+    : mTtl(0)
+    , mQueryCount(0)
+{
+}
+
+bool Core::CacheRecordInfo::RefreshTtl(uint32_t aTtl)
+{
+    // Called when cached record is refreshed.
+    // Returns a boolean to indicate if TTL value
+    // was changed or not.
+
+    bool changed = (aTtl != mTtl);
+
+    mLastRxTime = TimerMilli::GetNow();
+    mTtl        = aTtl;
+    mQueryCount = 0;
+
+    return changed;
+}
+
+bool Core::CacheRecordInfo::ShouldExpire(TimeMilli aNow) const { return IsPresent() && (GetExpireTime() <= aNow); }
+
+void Core::CacheRecordInfo::UpdateStateAfterQuery(TimeMilli aNow)
+{
+    VerifyOrExit(IsPresent());
+
+    // If the less than half TTL remains, then this record would not
+    // be included as "Known-Answer" in the send query, so we can
+    // count it towards queries to refresh this record.
+
+    VerifyOrExit(LessThanHalfTtlRemains(aNow));
+
+    if (mQueryCount < kNumberOfQueries)
+    {
+        mQueryCount++;
+    }
+
+exit:
+    return;
+}
+
+void Core::CacheRecordInfo::UpdateQueryAndFireTimeOn(CacheEntry &aCacheEntry)
+{
+    TimeMilli now;
+    TimeMilli expireTime;
+
+    VerifyOrExit(IsPresent());
+
+    now        = TimerMilli::GetNow();
+    expireTime = GetExpireTime();
+
+    aCacheEntry.SetFireTime(expireTime);
+
+    // Determine next query time
+
+    for (uint8_t attemptIndex = mQueryCount; attemptIndex < kNumberOfQueries; attemptIndex++)
+    {
+        TimeMilli queryTime = GetQueryTime(attemptIndex);
+
+        if (queryTime > now)
+        {
+            queryTime += Random::NonCrypto::GetUint32InRange(0, GetClampedTtl() * kQueryTtlVariation);
+            aCacheEntry.ScheduleQuery(queryTime);
+            break;
+        }
+    }
+
+exit:
+    return;
+}
+
+bool Core::CacheRecordInfo::LessThanHalfTtlRemains(TimeMilli aNow) const
+{
+    return IsPresent() && ((aNow - mLastRxTime) > TimeMilli::SecToMsec(GetClampedTtl()) / 2);
+}
+
+uint32_t Core::CacheRecordInfo::GetRemainingTtl(TimeMilli aNow) const
+{
+    uint32_t  remainingTtl = 0;
+    TimeMilli expireTime;
+
+    VerifyOrExit(IsPresent());
+
+    expireTime = GetExpireTime();
+    VerifyOrExit(aNow < expireTime);
+
+    remainingTtl = TimeMilli::MsecToSec(expireTime - aNow);
+
+exit:
+    return remainingTtl;
+}
+
+uint32_t Core::CacheRecordInfo::GetClampedTtl(void) const
+{
+    // We clamp TTL to `kMaxTtl` (one day) to prevent `TimeMilli`
+    // calculation overflow.
+
+    return Min(mTtl, kMaxTtl);
+}
+
+TimeMilli Core::CacheRecordInfo::GetExpireTime(void) const
+{
+    return mLastRxTime + TimeMilli::SecToMsec(GetClampedTtl());
+}
+
+TimeMilli Core::CacheRecordInfo::GetQueryTime(uint8_t aAttemptIndex) const
+{
+    // Queries are sent at 80%, 85%, 90% and 95% of TTL plus a random
+    // variation of 2% (added when sceduling)
+
+    static const uint32_t kTtlFactors[kNumberOfQueries] = {
+        80 * 1000 / 100,
+        85 * 1000 / 100,
+        90 * 1000 / 100,
+        95 * 1000 / 100,
+    };
+
+    OT_ASSERT(aAttemptIndex < kNumberOfQueries);
+
+    return mLastRxTime + kTtlFactors[aAttemptIndex] * GetClampedTtl();
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::CacheEntry
+
+void Core::CacheEntry::Init(Instance &aInstance, Type aType)
+{
+    InstanceLocatorInit::Init(aInstance);
+
+    mType               = aType;
+    mInitalQueries      = 0;
+    mQueryPending       = false;
+    mLastQueryTimeValid = false;
+    mIsActive           = false;
+    mDeleteTime         = TimerMilli::GetNow() + kNonActiveDeleteTimeout;
+}
+
+void Core::CacheEntry::SetIsActive(bool aIsActive)
+{
+    // Sets the active/passive state of a cache entry. An entry is
+    // considered "active" when associated with at least one
+    // resolver/browser. "Passive" entries (without a resolver/browser)
+    // continue to process mDNS responses for updates but will not send
+    // queries. Passive entries are deleted after `kNonActiveDeleteTimeout`
+    // if no resolver/browser is added.
+
+    mIsActive = aIsActive;
+
+    if (!mIsActive)
+    {
+        mQueryPending = false;
+        mDeleteTime   = TimerMilli::GetNow() + kNonActiveDeleteTimeout;
+        SetFireTime(mDeleteTime);
+    }
+}
+
+bool Core::CacheEntry::ShouldDelete(TimeMilli aNow) const { return !mIsActive && (mDeleteTime <= aNow); }
+
+void Core::CacheEntry::StartInitialQueries(void)
+{
+    mInitalQueries      = 0;
+    mLastQueryTimeValid = false;
+    mLastQueryTime      = Get<Core>().RandomizeInitialQueryTxTime();
+
+    ScheduleQuery(mLastQueryTime);
+}
+
+bool Core::CacheEntry::ShouldQuery(TimeMilli aNow) { return mQueryPending && (mNextQueryTime <= aNow); }
+
+void Core::CacheEntry::ScheduleQuery(TimeMilli aQueryTime)
+{
+    VerifyOrExit(mIsActive);
+
+    if (mQueryPending)
+    {
+        VerifyOrExit(aQueryTime < mNextQueryTime);
+    }
+
+    if (mLastQueryTimeValid)
+    {
+        aQueryTime = Max(aQueryTime, mLastQueryTime + kMinIntervalBetweenQueries);
+    }
+
+    mQueryPending  = true;
+    mNextQueryTime = aQueryTime;
+    SetFireTime(mNextQueryTime);
+
+exit:
+    return;
+}
+
+Error Core::CacheEntry::Add(const ResultCallback &aCallback)
+{
+    Error           error = kErrorNone;
+    bool            isFirst;
+    ResultCallback *callback;
+
+    callback = FindCallbackMatching(aCallback);
+    VerifyOrExit(callback == nullptr, error = kErrorAlready);
+
+    isFirst = mCallbacks.IsEmpty();
+
+    callback = ResultCallback::Allocate(aCallback);
+    OT_ASSERT(callback != nullptr);
+
+    mCallbacks.Push(*callback);
+
+    // If this is the first active resolver/browser for this cache
+    // entry, we mark it as active which allows queries to be sent We
+    // decide whether or not to send initial queries (e.g., for
+    // SRV/TXT cache entries we send initial queries if there is no
+    // record, or less than half TTL remains).
+
+    if (isFirst)
+    {
+        bool shouldStart = false;
+
+        SetIsActive(true);
+
+        switch (mType)
+        {
+        case kBrowseCache:
+            shouldStart = true;
+            break;
+        case kSrvCache:
+        case kTxtCache:
+            shouldStart = As<ServiceCache>().ShouldStartInitialQueries();
+            break;
+        case kIp6AddrCache:
+        case kIp4AddrCache:
+            shouldStart = As<AddrCache>().ShouldStartInitialQueries();
+            break;
+        }
+
+        if (shouldStart)
+        {
+            StartInitialQueries();
+        }
+
+        DetermineNextFireTime();
+        ScheduleTimer();
+    }
+
+    // Report any discovered/cached result to the newly added
+    // callback.
+
+    switch (mType)
+    {
+    case kBrowseCache:
+        As<BrowseCache>().ReportResultsTo(*callback);
+        break;
+    case kSrvCache:
+        As<SrvCache>().ReportResultTo(*callback);
+        break;
+    case kTxtCache:
+        As<TxtCache>().ReportResultTo(*callback);
+        break;
+    case kIp6AddrCache:
+    case kIp4AddrCache:
+        As<AddrCache>().ReportResultsTo(*callback);
+        break;
+    }
+
+exit:
+    return error;
+}
+
+void Core::CacheEntry::Remove(const ResultCallback &aCallback)
+{
+    ResultCallback *callback = FindCallbackMatching(aCallback);
+
+    VerifyOrExit(callback != nullptr);
+
+    // We clear the callback setting it to `nullptr` without removing
+    // it from the list here, since the `Remove()` method may be
+    // called from a callback while we are iterating over the list.
+    // Removal from the list is deferred to `mCacheTask` which will
+    // later call `ClearEmptyCallbacks()`.
+
+    callback->ClearCallback();
+    Get<Core>().mCacheTask.Post();
+
+exit:
+    return;
+}
+
+void Core::CacheEntry::ClearEmptyCallbacks(void)
+{
+    CallbackList emptyCallbacks;
+
+    mCallbacks.RemoveAllMatching(EmptyChecker(), emptyCallbacks);
+
+    if (mCallbacks.IsEmpty())
+    {
+        SetIsActive(false);
+        DetermineNextFireTime();
+        ScheduleTimer();
+    }
+}
+
+void Core::CacheEntry::HandleTimer(CacheTimerContext &aContext)
+{
+    switch (mType)
+    {
+    case kBrowseCache:
+        As<BrowseCache>().ClearCompressOffsets();
+        break;
+
+    case kSrvCache:
+    case kTxtCache:
+        As<ServiceCache>().ClearCompressOffsets();
+        break;
+
+    case kIp6AddrCache:
+    case kIp4AddrCache:
+        // `AddrCache` entries do not track any append state or
+        // compress offset since the host name would not be used
+        // in any other query question.
+        break;
+    }
+
+    VerifyOrExit(HasFireTime());
+    VerifyOrExit(GetFireTime() <= aContext.GetNow());
+    ClearFireTime();
+
+    if (ShouldDelete(aContext.GetNow()))
+    {
+        ExitNow();
+    }
+
+    if (ShouldQuery(aContext.GetNow()))
+    {
+        mQueryPending = false;
+        PrepareQuery(aContext);
+    }
+
+    switch (mType)
+    {
+    case kBrowseCache:
+        As<BrowseCache>().ProcessExpiredRecords(aContext.GetNow());
+        break;
+    case kSrvCache:
+        As<SrvCache>().ProcessExpiredRecords(aContext.GetNow());
+        break;
+    case kTxtCache:
+        As<TxtCache>().ProcessExpiredRecords(aContext.GetNow());
+        break;
+    case kIp6AddrCache:
+    case kIp4AddrCache:
+        As<AddrCache>().ProcessExpiredRecords(aContext.GetNow());
+        break;
+    }
+
+    DetermineNextFireTime();
+
+exit:
+    if (HasFireTime())
+    {
+        aContext.UpdateNextTime(GetFireTime());
+    }
+}
+
+Core::ResultCallback *Core::CacheEntry::FindCallbackMatching(const ResultCallback &aCallback)
+{
+    ResultCallback *callback = nullptr;
+
+    switch (mType)
+    {
+    case kBrowseCache:
+        callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mBrowse);
+        break;
+    case kSrvCache:
+        callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mSrv);
+        break;
+    case kTxtCache:
+        callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mTxt);
+        break;
+    case kIp6AddrCache:
+    case kIp4AddrCache:
+        callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mAddress);
+        break;
+    }
+
+    return callback;
+}
+
+void Core::CacheEntry::DetermineNextFireTime(void)
+{
+    mQueryPending = false;
+
+    if (mInitalQueries < kNumberOfInitalQueries)
+    {
+        uint32_t interval = (mInitalQueries == 0) ? 0 : (1U << (mInitalQueries - 1)) * kInitialQueryInterval;
+
+        ScheduleQuery(mLastQueryTime + interval);
+    }
+
+    if (!mIsActive)
+    {
+        SetFireTime(mDeleteTime);
+    }
+
+    // Let each cache entry type schedule query and
+    // fire times based on the state of its discovered
+    // records.
+
+    switch (mType)
+    {
+    case kBrowseCache:
+        As<BrowseCache>().DetermineRecordFireTime();
+        break;
+    case kSrvCache:
+    case kTxtCache:
+        As<ServiceCache>().DetermineRecordFireTime();
+        break;
+    case kIp6AddrCache:
+    case kIp4AddrCache:
+        As<AddrCache>().DetermineRecordFireTime();
+        break;
+    }
+}
+
+void Core::CacheEntry::ScheduleTimer(void) { ScheduleFireTimeOn(Get<Core>().mCacheTimer); }
+
+void Core::CacheEntry::PrepareQuery(CacheTimerContext &aContext)
+{
+    bool prepareAgain = false;
+
+    do
+    {
+        TxMessage &query = aContext.GetQueryMessage();
+
+        query.SaveCurrentState();
+
+        switch (mType)
+        {
+        case kBrowseCache:
+            As<BrowseCache>().PreparePtrQuestion(query, aContext.GetNow());
+            break;
+        case kSrvCache:
+            As<SrvCache>().PrepareSrvQuestion(query);
+            break;
+        case kTxtCache:
+            As<TxtCache>().PrepareTxtQuestion(query);
+            break;
+        case kIp6AddrCache:
+            As<Ip6AddrCache>().PrepareAaaaQuestion(query);
+            break;
+        case kIp4AddrCache:
+            As<Ip4AddrCache>().PrepareAQuestion(query);
+            break;
+        }
+
+        query.CheckSizeLimitToPrepareAgain(prepareAgain);
+
+    } while (prepareAgain);
+
+    mLastQueryTimeValid = true;
+    mLastQueryTime      = aContext.GetNow();
+
+    if (mInitalQueries < kNumberOfInitalQueries)
+    {
+        mInitalQueries++;
+    }
+
+    // Let the cache entry super-classes update their state
+    // after query was sent.
+
+    switch (mType)
+    {
+    case kBrowseCache:
+        As<BrowseCache>().UpdateRecordStateAfterQuery(aContext.GetNow());
+        break;
+    case kSrvCache:
+    case kTxtCache:
+        As<ServiceCache>().UpdateRecordStateAfterQuery(aContext.GetNow());
+        break;
+    case kIp6AddrCache:
+    case kIp4AddrCache:
+        As<AddrCache>().UpdateRecordStateAfterQuery(aContext.GetNow());
+        break;
+    }
+}
+
+template <typename ResultType> void Core::CacheEntry::InvokeCallbacks(const ResultType &aResult)
+{
+    for (const ResultCallback &callback : mCallbacks)
+    {
+        callback.Invoke(GetInstance(), aResult);
+    }
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::BrowseCache
+
+Error Core::BrowseCache::Init(Instance &aInstance, const char *aServiceType, const char *aSubTypeLabel)
+{
+    Error error = kErrorNone;
+
+    CacheEntry::Init(aInstance, kBrowseCache);
+    mNext = nullptr;
+
+    ClearCompressOffsets();
+    SuccessOrExit(error = mServiceType.Set(aServiceType));
+    SuccessOrExit(error = mSubTypeLabel.Set(aSubTypeLabel));
+
+exit:
+    return error;
+}
+
+Error Core::BrowseCache::Init(Instance &aInstance, const Browser &aBrowser)
+{
+    return Init(aInstance, aBrowser.mServiceType, aBrowser.mSubTypeLabel);
+}
+
+void Core::BrowseCache::ClearCompressOffsets(void)
+{
+    mServiceTypeOffset    = kUnspecifiedOffset;
+    mSubServiceTypeOffset = kUnspecifiedOffset;
+    mSubServiceNameOffset = kUnspecifiedOffset;
+}
+
+bool Core::BrowseCache::Matches(const Name &aFullName) const
+{
+    bool matches   = false;
+    bool isSubType = !mSubTypeLabel.IsNull();
+    Name name      = aFullName;
+
+    OT_ASSERT(name.IsFromMessage());
+
+    if (isSubType)
+    {
+        uint16_t       offset;
+        const Message &message = name.GetAsMessage(offset);
+
+        SuccessOrExit(Name::CompareLabel(message, offset, mSubTypeLabel.AsCString()));
+        name.SetFromMessage(message, offset);
+    }
+
+    matches = name.Matches(isSubType ? kSubServiceLabel : nullptr, mServiceType.AsCString(), kLocalDomain);
+
+exit:
+    return matches;
+}
+
+bool Core::BrowseCache::Matches(const char *aServiceType, const char *aSubTypeLabel) const
+{
+    bool matches = false;
+
+    if (aSubTypeLabel == nullptr)
+    {
+        VerifyOrExit(mSubTypeLabel.IsNull());
+    }
+    else
+    {
+        VerifyOrExit(NameMatch(mSubTypeLabel, aSubTypeLabel));
+    }
+
+    matches = NameMatch(mServiceType, aServiceType);
+
+exit:
+    return matches;
+}
+
+bool Core::BrowseCache::Matches(const Browser &aBrowser) const
+{
+    return Matches(aBrowser.mServiceType, aBrowser.mSubTypeLabel);
+}
+
+bool Core::BrowseCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); }
+
+Error Core::BrowseCache::Add(const Browser &aBrowser) { return CacheEntry::Add(ResultCallback(aBrowser.mCallback)); }
+
+void Core::BrowseCache::Remove(const Browser &aBrowser) { CacheEntry::Remove(ResultCallback(aBrowser.mCallback)); }
+
+void Core::BrowseCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset)
+{
+    // Name and record type in `aMessage` are already matched.
+
+    uint16_t     offset = aRecordOffset;
+    PtrRecord    ptr;
+    Name::Buffer fullServiceType;
+    Name::Buffer serviceInstance;
+    BrowseResult result;
+    PtrEntry    *ptrEntry;
+    bool         changed = false;
+
+    // Read the PTR record. `ReadPtrName()` validates that
+    // PTR record is well-formed.
+
+    SuccessOrExit(aMessage.Read(offset, ptr));
+    offset += sizeof(ptr);
+    SuccessOrExit(ptr.ReadPtrName(aMessage, offset, serviceInstance, fullServiceType));
+
+    VerifyOrExit(Name(fullServiceType).Matches(nullptr, mServiceType.AsCString(), kLocalDomain));
+
+    ptrEntry = mPtrEntries.FindMatching(serviceInstance);
+
+    if (ptr.GetTtl() == 0)
+    {
+        VerifyOrExit((ptrEntry != nullptr) && ptrEntry->mRecord.IsPresent());
+
+        ptrEntry->mRecord.RefreshTtl(0);
+        changed = true;
+    }
+    else
+    {
+        if (ptrEntry == nullptr)
+        {
+            ptrEntry = PtrEntry::AllocateAndInit(serviceInstance);
+            VerifyOrExit(ptrEntry != nullptr);
+            mPtrEntries.Push(*ptrEntry);
+        }
+
+        if (ptrEntry->mRecord.RefreshTtl(ptr.GetTtl()))
+        {
+            changed = true;
+        }
+    }
+
+    VerifyOrExit(changed);
+
+    if (ptrEntry->mRecord.IsPresent() && IsActive())
+    {
+        Get<Core>().AddPassiveSrvTxtCache(ptrEntry->mServiceInstance.AsCString(), mServiceType.AsCString());
+    }
+
+    ptrEntry->ConvertTo(result, *this);
+    InvokeCallbacks(result);
+
+exit:
+    DetermineNextFireTime();
+    ScheduleTimer();
+}
+
+void Core::BrowseCache::PreparePtrQuestion(TxMessage &aQuery, TimeMilli aNow)
+{
+    Question question;
+
+    DiscoverCompressOffsets();
+
+    question.SetType(ResourceRecord::kTypePtr);
+    question.SetClass(ResourceRecord::kClassInternet);
+
+    AppendServiceTypeOrSubTypeTo(aQuery, kQuestionSection);
+    SuccessOrAssert(aQuery.SelectMessageFor(kQuestionSection).Append(question));
+
+    aQuery.IncrementRecordCount(kQuestionSection);
+
+    for (const PtrEntry &ptrEntry : mPtrEntries)
+    {
+        if (!ptrEntry.mRecord.IsPresent() || ptrEntry.mRecord.LessThanHalfTtlRemains(aNow))
+        {
+            continue;
+        }
+
+        AppendKnownAnswer(aQuery, ptrEntry, aNow);
+    }
+}
+
+void Core::BrowseCache::DiscoverCompressOffsets(void)
+{
+    for (const BrowseCache &browseCache : Get<Core>().mBrowseCacheList)
+    {
+        if (&browseCache == this)
+        {
+            break;
+        }
+
+        if (NameMatch(browseCache.mServiceType, mServiceType))
+        {
+            UpdateCompressOffset(mServiceTypeOffset, browseCache.mServiceTypeOffset);
+            UpdateCompressOffset(mSubServiceTypeOffset, browseCache.mSubServiceTypeOffset);
+            VerifyOrExit(mSubServiceTypeOffset == kUnspecifiedOffset);
+        }
+    }
+
+    VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset);
+
+    for (const SrvCache &srvCache : Get<Core>().mSrvCacheList)
+    {
+        if (NameMatch(srvCache.mServiceType, mServiceType))
+        {
+            UpdateCompressOffset(mServiceTypeOffset, srvCache.mServiceTypeOffset);
+            VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset);
+        }
+    }
+
+    for (const TxtCache &txtCache : Get<Core>().mTxtCacheList)
+    {
+        if (NameMatch(txtCache.mServiceType, mServiceType))
+        {
+            UpdateCompressOffset(mServiceTypeOffset, txtCache.mServiceTypeOffset);
+            VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset);
+        }
+    }
+
+exit:
+    return;
+}
+
+void Core::BrowseCache::AppendServiceTypeOrSubTypeTo(TxMessage &aTxMessage, Section aSection)
+{
+    if (!mSubTypeLabel.IsNull())
+    {
+        AppendOutcome outcome;
+
+        outcome = aTxMessage.AppendLabel(aSection, mSubTypeLabel.AsCString(), mSubServiceNameOffset);
+        VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+        outcome = aTxMessage.AppendLabel(aSection, kSubServiceLabel, mSubServiceTypeOffset);
+        VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+    }
+
+    aTxMessage.AppendServiceType(aSection, mServiceType.AsCString(), mServiceTypeOffset);
+
+exit:
+    return;
+}
+
+void Core::BrowseCache::AppendKnownAnswer(TxMessage &aTxMessage, const PtrEntry &aPtrEntry, TimeMilli aNow)
+{
+    Message  &message = aTxMessage.SelectMessageFor(kAnswerSection);
+    PtrRecord ptr;
+    uint16_t  offset;
+
+    ptr.Init();
+    ptr.SetTtl(aPtrEntry.mRecord.GetRemainingTtl(aNow));
+
+    AppendServiceTypeOrSubTypeTo(aTxMessage, kAnswerSection);
+
+    offset = message.GetLength();
+    SuccessOrAssert(message.Append(ptr));
+
+    SuccessOrAssert(Name::AppendLabel(aPtrEntry.mServiceInstance.AsCString(), message));
+    aTxMessage.AppendServiceType(kAnswerSection, mServiceType.AsCString(), mServiceTypeOffset);
+
+    UpdateRecordLengthInMessage(ptr, message, offset);
+
+    aTxMessage.IncrementRecordCount(kAnswerSection);
+}
+
+void Core::BrowseCache::UpdateRecordStateAfterQuery(TimeMilli aNow)
+{
+    for (PtrEntry &ptrEntry : mPtrEntries)
+    {
+        ptrEntry.mRecord.UpdateStateAfterQuery(aNow);
+    }
+}
+
+void Core::BrowseCache::DetermineRecordFireTime(void)
+{
+    for (PtrEntry &ptrEntry : mPtrEntries)
+    {
+        ptrEntry.mRecord.UpdateQueryAndFireTimeOn(*this);
+    }
+}
+
+void Core::BrowseCache::ProcessExpiredRecords(TimeMilli aNow)
+{
+    OwningList<PtrEntry> expiredEntries;
+
+    mPtrEntries.RemoveAllMatching(ExpireChecker(aNow), expiredEntries);
+
+    for (PtrEntry &exiredEntry : expiredEntries)
+    {
+        BrowseResult result;
+
+        exiredEntry.mRecord.RefreshTtl(0);
+
+        exiredEntry.ConvertTo(result, *this);
+        InvokeCallbacks(result);
+    }
+}
+
+void Core::BrowseCache::ReportResultsTo(ResultCallback &aCallback) const
+{
+    for (const PtrEntry &ptrEntry : mPtrEntries)
+    {
+        if (ptrEntry.mRecord.IsPresent())
+        {
+            BrowseResult result;
+
+            ptrEntry.ConvertTo(result, *this);
+            aCallback.Invoke(GetInstance(), result);
+        }
+    }
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::BrowseCache::PtrEntry
+
+Error Core::BrowseCache::PtrEntry::Init(const char *aServiceInstance)
+{
+    mNext = nullptr;
+
+    return mServiceInstance.Set(aServiceInstance);
+}
+
+bool Core::BrowseCache::PtrEntry::Matches(const ExpireChecker &aExpireChecker) const
+{
+    return mRecord.ShouldExpire(aExpireChecker.mNow);
+}
+
+void Core::BrowseCache::PtrEntry::ConvertTo(BrowseResult &aResult, const BrowseCache &aBrowseCache) const
+{
+    ClearAllBytes(aResult);
+
+    aResult.mServiceType     = aBrowseCache.mServiceType.AsCString();
+    aResult.mSubTypeLabel    = aBrowseCache.mSubTypeLabel.AsCString();
+    aResult.mServiceInstance = mServiceInstance.AsCString();
+    aResult.mTtl             = mRecord.GetTtl();
+    aResult.mInfraIfIndex    = aBrowseCache.Get<Core>().mInfraIfIndex;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::ServiceCache
+
+Error Core::ServiceCache::Init(Instance &aInstance, Type aType, const char *aServiceInstance, const char *aServiceType)
+{
+    Error error = kErrorNone;
+
+    CacheEntry::Init(aInstance, aType);
+    ClearCompressOffsets();
+    SuccessOrExit(error = mServiceInstance.Set(aServiceInstance));
+    SuccessOrExit(error = mServiceType.Set(aServiceType));
+
+exit:
+    return error;
+}
+
+void Core::ServiceCache::ClearCompressOffsets(void)
+{
+    mServiceNameOffset = kUnspecifiedOffset;
+    mServiceTypeOffset = kUnspecifiedOffset;
+}
+
+bool Core::ServiceCache::Matches(const Name &aFullName) const
+{
+    return aFullName.Matches(mServiceInstance.AsCString(), mServiceType.AsCString(), kLocalDomain);
+}
+
+bool Core::ServiceCache::Matches(const char *aServiceInstance, const char *aServiceType) const
+{
+    return NameMatch(mServiceInstance, aServiceInstance) && NameMatch(mServiceType, aServiceType);
+}
+
+void Core::ServiceCache::PrepareQueryQuestion(TxMessage &aQuery, uint16_t aRrType)
+{
+    Message &message = aQuery.SelectMessageFor(kQuestionSection);
+    Question question;
+
+    question.SetType(aRrType);
+    question.SetClass(ResourceRecord::kClassInternet);
+
+    AppendServiceNameTo(aQuery, kQuestionSection);
+    SuccessOrAssert(message.Append(question));
+
+    aQuery.IncrementRecordCount(kQuestionSection);
+}
+
+void Core::ServiceCache::AppendServiceNameTo(TxMessage &aTxMessage, Section aSection)
+{
+    AppendOutcome outcome;
+
+    outcome = aTxMessage.AppendLabel(aSection, mServiceInstance.AsCString(), mServiceNameOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    aTxMessage.AppendServiceType(aSection, mServiceType.AsCString(), mServiceTypeOffset);
+
+exit:
+    return;
+}
+
+void Core::ServiceCache::UpdateRecordStateAfterQuery(TimeMilli aNow) { mRecord.UpdateStateAfterQuery(aNow); }
+
+void Core::ServiceCache::DetermineRecordFireTime(void) { mRecord.UpdateQueryAndFireTimeOn(*this); }
+
+bool Core::ServiceCache::ShouldStartInitialQueries(void) const
+{
+    // This is called when the first active resolver is added
+    // for this cache entry to determine whether we should
+    // send initial queries. It is possible that we were passively
+    // monitoring and have some cached record for this entry.
+    // We send initial queries if there is no record or less than
+    // half of the original TTL remains.
+
+    return !mRecord.IsPresent() || mRecord.LessThanHalfTtlRemains(TimerMilli::GetNow());
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::SrvCache
+
+Error Core::SrvCache::Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType)
+{
+    mNext     = nullptr;
+    mPort     = 0;
+    mPriority = 0;
+    mWeight   = 0;
+
+    return ServiceCache::Init(aInstance, kSrvCache, aServiceInstance, aServiceType);
+}
+
+Error Core::SrvCache::Init(Instance &aInstance, const ServiceName &aServiceName)
+{
+    return Init(aInstance, aServiceName.mServiceInstance, aServiceName.mServiceType);
+}
+
+Error Core::SrvCache::Init(Instance &aInstance, const SrvResolver &aResolver)
+{
+    return Init(aInstance, aResolver.mServiceInstance, aResolver.mServiceType);
+}
+
+bool Core::SrvCache::Matches(const Name &aFullName) const { return ServiceCache::Matches(aFullName); }
+
+bool Core::SrvCache::Matches(const ServiceName &aServiceName) const
+{
+    return ServiceCache::Matches(aServiceName.mServiceInstance, aServiceName.mServiceType);
+}
+
+bool Core::SrvCache::Matches(const SrvResolver &aResolver) const
+{
+    return ServiceCache::Matches(aResolver.mServiceInstance, aResolver.mServiceType);
+}
+
+bool Core::SrvCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); }
+
+Error Core::SrvCache::Add(const SrvResolver &aResolver) { return CacheEntry::Add(ResultCallback(aResolver.mCallback)); }
+
+void Core::SrvCache::Remove(const SrvResolver &aResolver) { CacheEntry::Remove(ResultCallback(aResolver.mCallback)); }
+
+void Core::SrvCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset)
+{
+    // Name and record type in `aMessage` are already matched.
+
+    uint16_t     offset = aRecordOffset;
+    SrvRecord    srv;
+    Name::Buffer hostFullName;
+    Name::Buffer hostName;
+    SrvResult    result;
+    bool         changed = false;
+
+    // Read the SRV record. `ReadTargetHostName()` validates that
+    // SRV record is well-formed.
+
+    SuccessOrExit(aMessage.Read(offset, srv));
+    offset += sizeof(srv);
+    SuccessOrExit(srv.ReadTargetHostName(aMessage, offset, hostFullName));
+
+    SuccessOrExit(Name::ExtractLabels(hostFullName, kLocalDomain, hostName));
+
+    if (srv.GetTtl() == 0)
+    {
+        VerifyOrExit(mRecord.IsPresent());
+
+        mHostName.Free();
+        mRecord.RefreshTtl(0);
+        changed = true;
+    }
+    else
+    {
+        if (!mRecord.IsPresent() || !NameMatch(mHostName, hostName))
+        {
+            SuccessOrAssert(mHostName.Set(hostName));
+            changed = true;
+        }
+
+        if (!mRecord.IsPresent() || (mPort != srv.GetPort()))
+        {
+            mPort   = srv.GetPort();
+            changed = true;
+        }
+
+        if (!mRecord.IsPresent() || (mPriority != srv.GetPriority()))
+        {
+            mPriority = srv.GetPriority();
+            changed   = true;
+        }
+
+        if (!mRecord.IsPresent() || (mWeight != srv.GetWeight()))
+        {
+            mWeight = srv.GetWeight();
+            changed = true;
+        }
+
+        if (mRecord.RefreshTtl(srv.GetTtl()))
+        {
+            changed = true;
+        }
+    }
+
+    VerifyOrExit(changed);
+
+    if (mRecord.IsPresent())
+    {
+        StopInitialQueries();
+
+        // If not present already, we add a passive `TxtCache` for the
+        // same service name, and an `Ip6AddrCache` for the host name.
+
+        Get<Core>().AddPassiveSrvTxtCache(mServiceInstance.AsCString(), mServiceType.AsCString());
+        Get<Core>().AddPassiveIp6AddrCache(mHostName.AsCString());
+    }
+
+    ConvertTo(result);
+    InvokeCallbacks(result);
+
+exit:
+    DetermineNextFireTime();
+    ScheduleTimer();
+}
+
+void Core::SrvCache::PrepareSrvQuestion(TxMessage &aQuery)
+{
+    DiscoverCompressOffsets();
+    PrepareQueryQuestion(aQuery, ResourceRecord::kTypeSrv);
+}
+
+void Core::SrvCache::DiscoverCompressOffsets(void)
+{
+    for (const SrvCache &srvCache : Get<Core>().mSrvCacheList)
+    {
+        if (&srvCache == this)
+        {
+            break;
+        }
+
+        if (NameMatch(srvCache.mServiceType, mServiceType))
+        {
+            UpdateCompressOffset(mServiceTypeOffset, srvCache.mServiceTypeOffset);
+        }
+
+        if (mServiceTypeOffset != kUnspecifiedOffset)
+        {
+            break;
+        }
+    }
+}
+
+void Core::SrvCache::ProcessExpiredRecords(TimeMilli aNow)
+{
+    if (mRecord.ShouldExpire(aNow))
+    {
+        SrvResult result;
+
+        mRecord.RefreshTtl(0);
+
+        ConvertTo(result);
+        InvokeCallbacks(result);
+    }
+}
+
+void Core::SrvCache::ReportResultTo(ResultCallback &aCallback) const
+{
+    if (mRecord.IsPresent())
+    {
+        SrvResult result;
+
+        ConvertTo(result);
+        aCallback.Invoke(GetInstance(), result);
+    }
+}
+
+void Core::SrvCache::ConvertTo(SrvResult &aResult) const
+{
+    ClearAllBytes(aResult);
+
+    aResult.mServiceInstance = mServiceInstance.AsCString();
+    aResult.mServiceType     = mServiceType.AsCString();
+    aResult.mHostName        = mHostName.AsCString();
+    aResult.mPort            = mPort;
+    aResult.mPriority        = mPriority;
+    aResult.mWeight          = mWeight;
+    aResult.mTtl             = mRecord.GetTtl();
+    aResult.mInfraIfIndex    = Get<Core>().mInfraIfIndex;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::TxtCache
+
+Error Core::TxtCache::Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType)
+{
+    mNext = nullptr;
+
+    return ServiceCache::Init(aInstance, kTxtCache, aServiceInstance, aServiceType);
+}
+
+Error Core::TxtCache::Init(Instance &aInstance, const ServiceName &aServiceName)
+{
+    return Init(aInstance, aServiceName.mServiceInstance, aServiceName.mServiceType);
+}
+
+Error Core::TxtCache::Init(Instance &aInstance, const TxtResolver &aResolver)
+{
+    return Init(aInstance, aResolver.mServiceInstance, aResolver.mServiceType);
+}
+
+bool Core::TxtCache::Matches(const Name &aFullName) const { return ServiceCache::Matches(aFullName); }
+
+bool Core::TxtCache::Matches(const ServiceName &aServiceName) const
+{
+    return ServiceCache::Matches(aServiceName.mServiceInstance, aServiceName.mServiceType);
+}
+
+bool Core::TxtCache::Matches(const TxtResolver &aResolver) const
+{
+    return ServiceCache::Matches(aResolver.mServiceInstance, aResolver.mServiceType);
+}
+
+bool Core::TxtCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); }
+
+Error Core::TxtCache::Add(const TxtResolver &aResolver) { return CacheEntry::Add(ResultCallback(aResolver.mCallback)); }
+
+void Core::TxtCache::Remove(const TxtResolver &aResolver) { CacheEntry::Remove(ResultCallback(aResolver.mCallback)); }
+
+void Core::TxtCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset)
+{
+    // Name and record type are already matched.
+
+    uint16_t  offset = aRecordOffset;
+    TxtRecord txt;
+    TxtResult result;
+    bool      changed = false;
+
+    SuccessOrExit(aMessage.Read(offset, txt));
+    offset += sizeof(txt);
+
+    if (txt.GetTtl() == 0)
+    {
+        VerifyOrExit(mRecord.IsPresent());
+
+        mTxtData.Free();
+        mRecord.RefreshTtl(0);
+        changed = true;
+    }
+    else
+    {
+        VerifyOrExit(txt.GetLength() > 0);
+        VerifyOrExit(aMessage.GetLength() >= offset + txt.GetLength());
+
+        if (!mRecord.IsPresent() || (mTxtData.GetLength() != txt.GetLength()) ||
+            !aMessage.CompareBytes(offset, mTxtData.GetBytes(), mTxtData.GetLength()))
+        {
+            SuccessOrAssert(mTxtData.SetFrom(aMessage, offset, txt.GetLength()));
+            changed = true;
+        }
+
+        if (mRecord.RefreshTtl(txt.GetTtl()))
+        {
+            changed = true;
+        }
+    }
+
+    VerifyOrExit(changed);
+
+    if (mRecord.IsPresent())
+    {
+        StopInitialQueries();
+    }
+
+    ConvertTo(result);
+    InvokeCallbacks(result);
+
+exit:
+    DetermineNextFireTime();
+    ScheduleTimer();
+}
+
+void Core::TxtCache::PrepareTxtQuestion(TxMessage &aQuery)
+{
+    DiscoverCompressOffsets();
+    PrepareQueryQuestion(aQuery, ResourceRecord::kTypeTxt);
+}
+
+void Core::TxtCache::DiscoverCompressOffsets(void)
+{
+    for (const SrvCache &srvCache : Get<Core>().mSrvCacheList)
+    {
+        if (!NameMatch(srvCache.mServiceType, mServiceType))
+        {
+            continue;
+        }
+
+        UpdateCompressOffset(mServiceTypeOffset, srvCache.mServiceTypeOffset);
+
+        if (NameMatch(srvCache.mServiceInstance, mServiceInstance))
+        {
+            UpdateCompressOffset(mServiceNameOffset, srvCache.mServiceNameOffset);
+        }
+
+        VerifyOrExit(mServiceNameOffset == kUnspecifiedOffset);
+    }
+
+    for (const TxtCache &txtCache : Get<Core>().mTxtCacheList)
+    {
+        if (&txtCache == this)
+        {
+            break;
+        }
+
+        if (NameMatch(txtCache.mServiceType, mServiceType))
+        {
+            UpdateCompressOffset(mServiceTypeOffset, txtCache.mServiceTypeOffset);
+        }
+
+        VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset);
+    }
+
+exit:
+    return;
+}
+
+void Core::TxtCache::ProcessExpiredRecords(TimeMilli aNow)
+{
+    if (mRecord.ShouldExpire(aNow))
+    {
+        TxtResult result;
+
+        mRecord.RefreshTtl(0);
+
+        ConvertTo(result);
+        InvokeCallbacks(result);
+    }
+}
+
+void Core::TxtCache::ReportResultTo(ResultCallback &aCallback) const
+{
+    if (mRecord.IsPresent())
+    {
+        TxtResult result;
+
+        ConvertTo(result);
+        aCallback.Invoke(GetInstance(), result);
+    }
+}
+
+void Core::TxtCache::ConvertTo(TxtResult &aResult) const
+{
+    ClearAllBytes(aResult);
+
+    aResult.mServiceInstance = mServiceInstance.AsCString();
+    aResult.mServiceType     = mServiceType.AsCString();
+    aResult.mTxtData         = mTxtData.GetBytes();
+    aResult.mTxtDataLength   = mTxtData.GetLength();
+    aResult.mTtl             = mRecord.GetTtl();
+    aResult.mInfraIfIndex    = Get<Core>().mInfraIfIndex;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::AddrCache
+
+Error Core::AddrCache::Init(Instance &aInstance, Type aType, const char *aHostName)
+{
+    CacheEntry::Init(aInstance, aType);
+
+    mNext        = nullptr;
+    mShouldFlush = false;
+
+    return mName.Set(aHostName);
+}
+
+Error Core::AddrCache::Init(Instance &aInstance, Type aType, const AddressResolver &aResolver)
+{
+    return Init(aInstance, aType, aResolver.mHostName);
+}
+
+bool Core::AddrCache::Matches(const Name &aFullName) const
+{
+    return aFullName.Matches(nullptr, mName.AsCString(), kLocalDomain);
+}
+
+bool Core::AddrCache::Matches(const char *aName) const { return NameMatch(mName, aName); }
+
+bool Core::AddrCache::Matches(const AddressResolver &aResolver) const { return Matches(aResolver.mHostName); }
+
+bool Core::AddrCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); }
+
+Error Core::AddrCache::Add(const AddressResolver &aResolver)
+{
+    return CacheEntry::Add(ResultCallback(aResolver.mCallback));
+}
+
+void Core::AddrCache::Remove(const AddressResolver &aResolver)
+{
+    CacheEntry::Remove(ResultCallback(aResolver.mCallback));
+}
+
+void Core::AddrCache::PrepareQueryQuestion(TxMessage &aQuery, uint16_t aRrType)
+{
+    Question question;
+
+    question.SetType(aRrType);
+    question.SetClass(ResourceRecord::kClassInternet);
+
+    AppendNameTo(aQuery, kQuestionSection);
+    SuccessOrAssert(aQuery.SelectMessageFor(kQuestionSection).Append(question));
+
+    aQuery.IncrementRecordCount(kQuestionSection);
+}
+
+void Core::AddrCache::AppendNameTo(TxMessage &aTxMessage, Section aSection)
+{
+    uint16_t compressOffset = kUnspecifiedOffset;
+
+    AppendOutcome outcome;
+
+    outcome = aTxMessage.AppendMultipleLabels(aSection, mName.AsCString(), compressOffset);
+    VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
+
+    aTxMessage.AppendDomainName(aSection);
+
+exit:
+    return;
+}
+
+void Core::AddrCache::UpdateRecordStateAfterQuery(TimeMilli aNow)
+{
+    for (AddrEntry &entry : mCommittedEntries)
+    {
+        entry.mRecord.UpdateStateAfterQuery(aNow);
+    }
+}
+
+void Core::AddrCache::DetermineRecordFireTime(void)
+{
+    for (AddrEntry &entry : mCommittedEntries)
+    {
+        entry.mRecord.UpdateQueryAndFireTimeOn(*this);
+    }
+}
+
+void Core::AddrCache::ProcessExpiredRecords(TimeMilli aNow)
+{
+    OwningList<AddrEntry>      expiredEntries;
+    Heap::Array<AddressAndTtl> addrArray;
+    AddressResult              result;
+
+    mCommittedEntries.RemoveAllMatching(ExpireChecker(aNow), expiredEntries);
+
+    VerifyOrExit(!expiredEntries.IsEmpty());
+
+    ConstructResult(result, addrArray);
+    InvokeCallbacks(result);
+
+exit:
+    return;
+}
+
+void Core::AddrCache::ReportResultsTo(ResultCallback &aCallback) const
+{
+    Heap::Array<AddressAndTtl> addrArray;
+    AddressResult              result;
+
+    ConstructResult(result, addrArray);
+
+    if (result.mAddressesLength > 0)
+    {
+        aCallback.Invoke(GetInstance(), result);
+    }
+}
+
+void Core::AddrCache::ConstructResult(AddressResult &aResult, Heap::Array<AddressAndTtl> &aAddrArray) const
+{
+    // Prepares an `AddressResult` populating it with discovered
+    // addresses from the `AddrCache` entry. Uses a caller-provided
+    // `Heap::Array` reference (`aAddrArray`) to ensure that the
+    // allocated array for `aResult.mAddresses` remains valid until
+    // after the `aResult` is used (passed as input to
+    // `ResultCallback`).
+
+    uint16_t addrCount = 0;
+
+    ClearAllBytes(aResult);
+    aAddrArray.Free();
+
+    for (const AddrEntry &entry : mCommittedEntries)
+    {
+        if (entry.mRecord.IsPresent())
+        {
+            addrCount++;
+        }
+    }
+
+    if (addrCount > 0)
+    {
+        SuccessOrAssert(aAddrArray.ReserveCapacity(addrCount));
+
+        for (const AddrEntry &entry : mCommittedEntries)
+        {
+            AddressAndTtl *addr;
+
+            if (!entry.mRecord.IsPresent())
+            {
+                continue;
+            }
+
+            addr = aAddrArray.PushBack();
+            OT_ASSERT(addr != nullptr);
+
+            addr->mAddress = entry.mAddress;
+            addr->mTtl     = entry.mRecord.GetTtl();
+        }
+    }
+
+    aResult.mHostName        = mName.AsCString();
+    aResult.mAddresses       = aAddrArray.AsCArray();
+    aResult.mAddressesLength = aAddrArray.GetLength();
+    aResult.mInfraIfIndex    = Get<Core>().mInfraIfIndex;
+}
+
+bool Core::AddrCache::ShouldStartInitialQueries(void) const
+{
+    // This is called when the first active resolver is added
+    // for this cache entry to determine whether we should
+    // send initial queries. It is possible that we were passively
+    // monitoring and has some cached records for this entry.
+    // We send initial queries if there is no record or less than
+    // half of original TTL remains on any record.
+
+    bool      shouldStart = false;
+    TimeMilli now         = TimerMilli::GetNow();
+
+    if (mCommittedEntries.IsEmpty())
+    {
+        shouldStart = true;
+        ExitNow();
+    }
+
+    for (const AddrEntry &entry : mCommittedEntries)
+    {
+        if (entry.mRecord.LessThanHalfTtlRemains(now))
+        {
+            shouldStart = true;
+            ExitNow();
+        }
+    }
+
+exit:
+    return shouldStart;
+}
+
+void Core::AddrCache::AddNewResponseAddress(const Ip6::Address &aAddress, uint32_t aTtl, bool aCacheFlush)
+{
+    // Adds a new address record to `mNewEntries` list. This called as
+    // the records in a received response are processed one by one.
+    // Once all records are processed `CommitNewResponseEntries()` is
+    // called to update the list of addresses.
+
+    AddrEntry *entry;
+
+    if (aCacheFlush)
+    {
+        mShouldFlush = true;
+    }
+
+    // Check for duplicate addresses in the same response.
+
+    entry = mNewEntries.FindMatching(aAddress);
+
+    if (entry == nullptr)
+    {
+        entry = AddrEntry::Allocate(aAddress);
+        OT_ASSERT(entry != nullptr);
+        mNewEntries.Push(*entry);
+    }
+
+    entry->mRecord.RefreshTtl(aTtl);
+}
+
+void Core::AddrCache::CommitNewResponseEntries(void)
+{
+    bool changed = false;
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Determine whether there is any changes to the list of addresses
+    // between the `mNewEntries` and `mCommittedEntries` lists.
+    //
+    // First, we verify if all new entries are present in the
+    // `mCommittedEntries` list with the same TTL value. Next, if we
+    // need to flush the old cache list, we check if any existing
+    // `mCommittedEntries` is absent in `mNewEntries` list.
+
+    for (const AddrEntry &newEntry : mNewEntries)
+    {
+        AddrEntry *exitingEntry = mCommittedEntries.FindMatching(newEntry.mAddress);
+
+        if (newEntry.GetTtl() == 0)
+        {
+            // New entry has zero TTL, removing the address. If we
+            // have a matching `exitingEntry` we set its TTL to zero
+            // so to remove it in the next step when updating the
+            // `mCommittedEntries` list.
+
+            if (exitingEntry != nullptr)
+            {
+                exitingEntry->mRecord.RefreshTtl(0);
+                changed = true;
+            }
+        }
+        else if ((exitingEntry == nullptr) || (exitingEntry->GetTtl() != newEntry.GetTtl()))
+        {
+            changed = true;
+        }
+    }
+
+    if (mShouldFlush && !changed)
+    {
+        for (const AddrEntry &exitingEntry : mCommittedEntries)
+        {
+            if ((exitingEntry.GetTtl() > 0) && !mNewEntries.ContainsMatching(exitingEntry.mAddress))
+            {
+                changed = true;
+                break;
+            }
+        }
+    }
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Update the `mCommittedEntries` list.
+
+    // First remove entries, if we need to flush clear everything,
+    // otherwise remove the ones with zero TTL marked in previous
+    // step. Then, add or update new entries to `mCommittedEntries`
+
+    if (mShouldFlush)
+    {
+        mCommittedEntries.Clear();
+        mShouldFlush = false;
+    }
+    else
+    {
+        OwningList<AddrEntry> removedEntries;
+
+        mCommittedEntries.RemoveAllMatching(EmptyChecker(), removedEntries);
+    }
+
+    while (!mNewEntries.IsEmpty())
+    {
+        OwnedPtr<AddrEntry> newEntry = mNewEntries.Pop();
+        AddrEntry          *entry;
+
+        if (newEntry->GetTtl() == 0)
+        {
+            continue;
+        }
+
+        entry = mCommittedEntries.FindMatching(newEntry->mAddress);
+
+        if (entry != nullptr)
+        {
+            entry->mRecord.RefreshTtl(newEntry->GetTtl());
+        }
+        else
+        {
+            mCommittedEntries.Push(*newEntry.Release());
+        }
+    }
+
+    StopInitialQueries();
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Invoke callbacks if there is any change.
+
+    if (changed)
+    {
+        Heap::Array<AddressAndTtl> addrArray;
+        AddressResult              result;
+
+        ConstructResult(result, addrArray);
+        InvokeCallbacks(result);
+    }
+
+    DetermineNextFireTime();
+    ScheduleTimer();
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::AddrCache::AddrEntry
+
+Core::AddrCache::AddrEntry::AddrEntry(const Ip6::Address &aAddress)
+    : mNext(nullptr)
+    , mAddress(aAddress)
+{
+}
+
+bool Core::AddrCache::AddrEntry::Matches(const ExpireChecker &aExpireChecker) const
+{
+    return mRecord.ShouldExpire(aExpireChecker.mNow);
+}
+
+bool Core::AddrCache::AddrEntry::Matches(EmptyChecker aChecker) const
+{
+    OT_UNUSED_VARIABLE(aChecker);
+
+    return !mRecord.IsPresent();
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::Ip6AddrCache
+
+Error Core::Ip6AddrCache::Init(Instance &aInstance, const char *aHostName)
+{
+    return AddrCache::Init(aInstance, kIp6AddrCache, aHostName);
+}
+
+Error Core::Ip6AddrCache::Init(Instance &aInstance, const AddressResolver &aResolver)
+{
+    return AddrCache::Init(aInstance, kIp6AddrCache, aResolver);
+}
+
+void Core::Ip6AddrCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset)
+{
+    // Name and record type in `aMessage` are already matched.
+
+    AaaaRecord aaaaRecord;
+
+    SuccessOrExit(aMessage.Read(aRecordOffset, aaaaRecord));
+    VerifyOrExit(aaaaRecord.GetLength() >= sizeof(Ip6::Address));
+
+    AddNewResponseAddress(aaaaRecord.GetAddress(), aaaaRecord.GetTtl(), aaaaRecord.GetClass() & kClassCacheFlushFlag);
+
+exit:
+    return;
+}
+
+void Core::Ip6AddrCache::PrepareAaaaQuestion(TxMessage &aQuery)
+{
+    PrepareQueryQuestion(aQuery, ResourceRecord::kTypeAaaa);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::Ip4AddrCache
+
+Error Core::Ip4AddrCache::Init(Instance &aInstance, const char *aHostName)
+{
+    return AddrCache::Init(aInstance, kIp4AddrCache, aHostName);
+}
+
+Error Core::Ip4AddrCache::Init(Instance &aInstance, const AddressResolver &aResolver)
+{
+    return AddrCache::Init(aInstance, kIp4AddrCache, aResolver);
+}
+
+void Core::Ip4AddrCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset)
+{
+    // Name and record type in `aMessage` are already matched.
+
+    ARecord      aRecord;
+    Ip6::Address address;
+
+    SuccessOrExit(aMessage.Read(aRecordOffset, aRecord));
+    VerifyOrExit(aRecord.GetLength() >= sizeof(Ip4::Address));
+
+    address.SetToIp4Mapped(aRecord.GetAddress());
+
+    AddNewResponseAddress(address, aRecord.GetTtl(), aRecord.GetClass() & kClassCacheFlushFlag);
+
+exit:
+    return;
+}
+
+void Core::Ip4AddrCache::PrepareAQuestion(TxMessage &aQuery) { PrepareQueryQuestion(aQuery, ResourceRecord::kTypeA); }
+
+//---------------------------------------------------------------------------------------------------------------------
+// Core::Iterator
+
+Core::EntryIterator::EntryIterator(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mType(kUnspecified)
+{
+}
+
+Error Core::EntryIterator::GetNextHost(Host &aHost, EntryState &aState)
+{
+    Error error = kErrorNotFound;
+
+    if (mType == kUnspecified)
+    {
+        mHostEntry = Get<Core>().mHostEntries.GetHead();
+        mType      = kHost;
+    }
+    else
+    {
+        VerifyOrExit(mType == kHost, error = kErrorInvalidArgs);
+    }
+
+    while (error == kErrorNotFound)
+    {
+        VerifyOrExit(mHostEntry != nullptr);
+        error      = mHostEntry->CopyInfoTo(aHost, aState);
+        mHostEntry = mHostEntry->GetNext();
+    }
+
+exit:
+    return error;
+}
+
+Error Core::EntryIterator::GetNextService(Service &aService, EntryState &aState)
+{
+    Error error = kErrorNotFound;
+
+    if (mType == kUnspecified)
+    {
+        mServiceEntry = Get<Core>().mServiceEntries.GetHead();
+        mType         = kService;
+    }
+    else
+    {
+        VerifyOrExit(mType == kService, error = kErrorInvalidArgs);
+    }
+
+    while (error == kErrorNotFound)
+    {
+        VerifyOrExit(mServiceEntry != nullptr);
+        error         = mServiceEntry->CopyInfoTo(aService, aState, *this);
+        mServiceEntry = mServiceEntry->GetNext();
+    }
+
+exit:
+    return error;
+}
+
+Error Core::EntryIterator::GetNextKey(Key &aKey, EntryState &aState)
+{
+    Error error = kErrorNotFound;
+
+    if (mType == kUnspecified)
+    {
+        mHostEntry = Get<Core>().mHostEntries.GetHead();
+        mType      = kHostKey;
+    }
+    else
+    {
+        VerifyOrExit((mType == kServiceKey) || (mType == kHostKey), error = kErrorInvalidArgs);
+    }
+
+    while ((error == kErrorNotFound) && (mType == kHostKey))
+    {
+        if (mHostEntry == nullptr)
+        {
+            mServiceEntry = Get<Core>().mServiceEntries.GetHead();
+            mType         = kServiceKey;
+            break;
+        }
+
+        error      = mHostEntry->CopyInfoTo(aKey, aState);
+        mHostEntry = mHostEntry->GetNext();
+    }
+
+    while ((error == kErrorNotFound) && (mType == kServiceKey))
+    {
+        VerifyOrExit(mServiceEntry != nullptr);
+        error         = mServiceEntry->CopyInfoTo(aKey, aState);
+        mServiceEntry = mServiceEntry->GetNext();
+    }
+
+exit:
+    return error;
+}
+
+} // namespace Multicast
+} // namespace Dns
+} // namespace ot
+
+//---------------------------------------------------------------------------------------------------------------------
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE
+
+OT_TOOL_WEAK otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    return OT_ERROR_FAILED;
+}
+
+OT_TOOL_WEAK void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+}
+
+OT_TOOL_WEAK void otPlatMdnsSendUnicast(otInstance                  *aInstance,
+                                        otMessage                   *aMessage,
+                                        const otPlatMdnsAddressInfo *aAddress)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aAddress);
+}
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
diff --git a/src/core/net/mdns.hpp b/src/core/net/mdns.hpp
new file mode 100644
index 0000000..5ee3236
--- /dev/null
+++ b/src/core/net/mdns.hpp
@@ -0,0 +1,1947 @@
+/*
+ *  Copyright (c) 2024, 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 MULTICAST_DNS_HPP_
+#define MULTICAST_DNS_HPP_
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+#include <openthread/mdns.h>
+#include <openthread/platform/mdns_socket.h>
+
+#include "common/as_core_type.hpp"
+#include "common/clearable.hpp"
+#include "common/debug.hpp"
+#include "common/equatable.hpp"
+#include "common/error.hpp"
+#include "common/heap_allocatable.hpp"
+#include "common/heap_array.hpp"
+#include "common/heap_data.hpp"
+#include "common/heap_string.hpp"
+#include "common/linked_list.hpp"
+#include "common/locator.hpp"
+#include "common/owned_ptr.hpp"
+#include "common/owning_list.hpp"
+#include "common/retain_ptr.hpp"
+#include "common/timer.hpp"
+#include "crypto/sha256.hpp"
+#include "net/dns_types.hpp"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF && !OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#error "OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF requires OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE"
+#endif
+
+/**
+ * @file
+ *   This file includes definitions for the Multicast DNS per RFC 6762.
+ *
+ */
+
+/**
+ * Represents an opaque (and empty) type for an mDNS iterator.
+ *
+ */
+struct otMdnsIterator
+{
+};
+
+namespace ot {
+namespace Dns {
+namespace Multicast {
+
+extern "C" void otPlatMdnsHandleReceive(otInstance                  *aInstance,
+                                        otMessage                   *aMessage,
+                                        bool                         aIsUnicast,
+                                        const otPlatMdnsAddressInfo *aAddress);
+
+/**
+ * Implements Multicast DNS (mDNS) core.
+ *
+ */
+class Core : public InstanceLocator, private NonCopyable
+{
+    friend void otPlatMdnsHandleReceive(otInstance                  *aInstance,
+                                        otMessage                   *aMessage,
+                                        bool                         aIsUnicast,
+                                        const otPlatMdnsAddressInfo *aAddress);
+
+public:
+    /**
+     * Initializes a `Core` instance.
+     *
+     * @param[in] aInstance  The OpenThread instance.
+     *
+     */
+    explicit Core(Instance &aInstance);
+
+    typedef otMdnsRequestId        RequestId;        ///< A request Identifier.
+    typedef otMdnsRegisterCallback RegisterCallback; ///< Registration callback.
+    typedef otMdnsConflictCallback ConflictCallback; ///< Conflict callback.
+    typedef otMdnsEntryState       EntryState;       ///< Host/Service/Key entry state.
+    typedef otMdnsHost             Host;             ///< Host information.
+    typedef otMdnsService          Service;          ///< Service information.
+    typedef otMdnsKey              Key;              ///< Key information.
+    typedef otMdnsBrowser          Browser;          ///< Browser.
+    typedef otMdnsBrowseCallback   BrowseCallback;   ///< Browser callback.
+    typedef otMdnsBrowseResult     BrowseResult;     ///< Browser result.
+    typedef otMdnsSrvResolver      SrvResolver;      ///< SRV resolver.
+    typedef otMdnsSrvCallback      SrvCallback;      ///< SRV callback.
+    typedef otMdnsSrvResult        SrvResult;        ///< SRV result.
+    typedef otMdnsTxtResolver      TxtResolver;      ///< TXT resolver.
+    typedef otMdnsTxtCallback      TxtCallback;      ///< TXT callback.
+    typedef otMdnsTxtResult        TxtResult;        ///< TXT result.
+    typedef otMdnsAddressResolver  AddressResolver;  ///< Address resolver.
+    typedef otMdnsAddressCallback  AddressCallback;  ///< Address callback
+    typedef otMdnsAddressResult    AddressResult;    ///< Address result.
+    typedef otMdnsAddressAndTtl    AddressAndTtl;    ///< Address and TTL.
+    typedef otMdnsIterator         Iterator;         ///< An entry iterator.
+
+    /**
+     * Represents a socket address info.
+     *
+     */
+    class AddressInfo : public otPlatMdnsAddressInfo, public Clearable<AddressInfo>, public Equatable<AddressInfo>
+    {
+    public:
+        /**
+         * Initializes the `AddressInfo` clearing all the fields.
+         *
+         */
+        AddressInfo(void) { Clear(); }
+
+        /**
+         * Gets the IPv6 address.
+         *
+         * @returns the IPv6 address.
+         *
+         */
+        const Ip6::Address &GetAddress(void) const { return AsCoreType(&mAddress); }
+    };
+
+    /**
+     * Enables or disables the mDNS module.
+     *
+     * mDNS module should be enabled before registration any host, service, or key entries. Disabling mDNS will
+     * immediately stop all operations and any communication (multicast or unicast tx) and remove any previously
+     * registered entries without sending any "goodbye" announcements or invoking their callback. When disabled,
+     * all browsers and resolvers are stopped and all cached information is cleared.
+     *
+     * @param[in] aEnable       Whether to enable or disable.
+     * @param[in] aInfraIfIndex The network interface index for mDNS operation. Value is ignored when disabling.
+     *
+     * @retval kErrorNone     Enabled or disabled the mDNS module successfully.
+     * @retval kErrorAlready  mDNS is already enabled on an enable request, or is already disabled on a disable request.
+     * @retval kErrorFailed   Failed to enable/disable mDNS.
+     *
+     */
+    Error SetEnabled(bool aEnable, uint32_t aInfraIfIndex);
+
+    /**
+     * Indicates whether or not mDNS module is enabled.
+     *
+     * @retval TRUE   The mDNS module is enabled.
+     * @retval FALSE  The mDNS module is disabled.
+     *
+     */
+    bool IsEnabled(void) const { return mIsEnabled; }
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF
+    /**
+     * Notifies `AdvertisingProxy` that `InfraIf` state changed.
+     *
+     */
+    void HandleInfraIfStateChanged(void);
+#endif
+
+    /**
+     * Sets whether mDNS module is allowed to send questions requesting unicast responses referred to as "QU" questions.
+     *
+     * The "QU" question request unicast response in contrast to "QM" questions which request multicast responses.
+     * When allowed, the first probe will be sent as a "QU" question.
+     *
+     * This can be used to address platform limitation where platform cannot accept unicast response received on mDNS
+     * port.
+     *
+     * @param[in] aAllow        Indicates whether or not to allow "QU" questions.
+     *
+     */
+    void SetQuestionUnicastAllowed(bool aAllow) { mIsQuestionUnicastAllowed = aAllow; }
+
+    /**
+     * Indicates whether mDNS module is allowed to send "QU" questions requesting unicast response.
+     *
+     * @retval TRUE  The mDNS module is allowed to send "QU" questions.
+     * @retval FALSE The mDNS module is not allowed to send "QU" questions.
+     *
+     */
+    bool IsQuestionUnicastAllowed(void) const { return mIsQuestionUnicastAllowed; }
+
+    /**
+     * Sets the conflict callback.
+     *
+     * @param[in] aCallback  The conflict callback. Can be `nullptr` is not needed.
+     *
+     */
+    void SetConflictCallback(ConflictCallback aCallback) { mConflictCallback = aCallback; }
+
+    /**
+     * Registers or updates a host.
+     *
+     * The fields in @p aHost follow these rules:
+     *
+     * - The `mHostName` field specifies the host name to register (e.g., "myhost"). MUST NOT contain the domain name.
+     * - The `mAddresses` is array of IPv6 addresses to register with the host. `mAddressesLength` provides the number
+     *   of entries in `mAddresses` array.
+     * - The `mAddresses` array can be empty with zero `mAddressesLength`. In this case, mDNS will treat it as if host
+     *   is unregistered and stop advertising any addresses for this the host name.
+     * - The `mTtl` specifies the TTL if non-zero. If zero, the mDNS core will choose a default TTL to use.
+     *
+     * This method can be called again for the same `mHostName` to update a previously registered host entry, for
+     * example, to change the list of addresses of the host. In this case, the mDNS module will send "goodbye"
+     * announcements for any previously registered and now removed addresses and announce any newly added addresses.
+     *
+     * The outcome of the registration request is reported back by invoking the provided @p aCallback with
+     * @p aRequestId as its input and one of the following `aError` inputs:
+     *
+     * - `kErrorNone`       indicates registration was successful
+     * - `kErrorDuplicated` indicates a name conflict, i.e., the name is already claimed by another mDNS responder.
+     *
+     * For caller convenience, the OpenThread mDNS module guarantees that the callback will be invoked after this
+     * method returns, even in cases of immediate registration success. The @p aCallback can be `nullptr` if caller
+     * does not want to be notified of the outcome.
+     *
+     * @param[in] aHost         The host to register.
+     * @param[in] aRequestId    The ID associated with this request.
+     * @param[in] aCallback     The callback function pointer to report the outcome (can be `nullptr` if not needed).
+     *
+     * @retval kErrorNone          Successfully started registration. @p aCallback will report the outcome.
+     * @retval kErrorInvalidState  mDNS module is not enabled.
+     *
+     */
+    Error RegisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback);
+
+    /**
+     * Unregisters a host.
+     *
+     * The fields in @p aHost follow these rules:
+     *
+     * - The `mHostName` field specifies the host name to unregister (e.g., "myhost"). MUST NOT contain the domain name.
+     * - The rest of the fields in @p aHost structure are ignored in an `UnregisterHost()` call.
+     *
+     * If there is no previously registered host with the same name, no action is performed.
+     *
+     * If there is a previously registered host with the same name, the mDNS module will send "goodbye" announcement
+     * for all previously advertised address records.
+     *
+     * @param[in] aHost   The host to unregister.
+     *
+     * @retval kErrorNone           Successfully unregistered host.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     *
+     */
+    Error UnregisterHost(const Host &aHost);
+
+    /**
+     * Registers or updates a service.
+     *
+     * The fields in @p aService follow these rules:
+     *
+     * - The `mServiceInstance` specifies the service instance label. It is treated as a single DNS label. It may
+     *   contain dot `.` character which is allowed in a service instance label.
+     * - The `mServiceType` specifies the service type (e.g., "_tst._udp"). It is treated as multiple dot `.` separated
+     *   labels. It MUST NOT contain the domain name.
+     * - The `mHostName` field specifies the host name of the service. MUST NOT contain the domain name.
+     * - The `mSubTypeLabels` is an array of strings representing sub-types associated with the service. Each array
+     *   entry is a sub-type label. The `mSubTypeLabels can be `nullptr` if there are no sub-types. Otherwise, the
+     *   array length is specified by `mSubTypeLabelsLength`.
+     * - The `mTxtData` and `mTxtDataLength` specify the encoded TXT data. The `mTxtData` can be `nullptr` or
+     *   `mTxtDataLength` can be zero to specify an empty TXT data. In this case mDNS module will use a single zero
+     *   byte `[ 0 ]` as empty TXT data.
+     * - The `mPort`, `mWeight`, and `mPriority` specify the service's parameters (as specified in DNS SRV record).
+     * - The `mTtl` specifies the TTL if non-zero. If zero, the mDNS module will use default TTL for service entry.
+     *
+     * This method can be called again for the same `mServiceInstance` and `mServiceType` to update a previously
+     * registered service entry, for example, to change the sub-types list or update any parameter such as port, weight,
+     * priority, TTL, or host name. The mDNS module will send announcements for any changed info, e.g., will send
+     * "goodbye" announcements for any removed sub-types and announce any newly added sub-types.
+     *
+     * Regarding the invocation of the @p aCallback, this method behaves in the same way as described in
+     * `RegisterHost()`.
+     *
+     * @param[in] aService      The service to register.
+     * @param[in] aRequestId    The ID associated with this request.
+     * @param[in] aCallback     The callback function pointer to report the outcome (can be `nullptr` if not needed).
+     *
+     * @retval kErrorNone           Successfully started registration. @p aCallback will report the outcome.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     *
+     */
+    Error RegisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback);
+
+    /**
+     * Unregisters a service.
+     *
+     * The fields in @p aService follow these rules:
+
+     * - The `mServiceInstance` specifies the service instance label. It is treated as a single DNS label. It may
+     *   contain dot `.` character which is allowed in a service instance label.
+     * - The `mServiceType` specifies the service type (e.g., "_tst._udp"). It is treated as multiple dot `.` separated
+     *   labels. It MUST NOT contain the domain name.
+     * - The rest of the fields in @p aService structure are ignored in  a`otMdnsUnregisterService()` call.
+     *
+     * If there is no previously registered service with the same name, no action is performed.
+     *
+     * If there is a previously registered service with the same name, the mDNS module will send "goodbye"
+     * announcements for all related records.
+     *
+     * @param[in] aService      The service to unregister.
+     *
+     * @retval kErrorNone            Successfully unregistered service.
+     * @retval kErrorInvalidState    mDNS module is not enabled.
+     *
+     */
+    Error UnregisterService(const Service &aService);
+
+    /**
+     * Registers or updates a key record.
+     *
+     * The fields in @p aKey follow these rules:
+     *
+     * - If the key is associated with a host entry, the `mName` field specifies the host name and the `mServiceType`
+     *    MUST be `nullptr`.
+     * - If the key is associated with a service entry, the `mName` filed specifies the service instance label (always
+     *   treated as a single label) and the `mServiceType` filed specifies the service type (e.g. "_tst._udp"). In this
+     *   case the DNS name for key record is `<mName>.<mServiceTye>`.
+     * - The `mKeyData` field contains the key record's data with `mKeyDataLength` as its length in byes.
+     * - The `mTtl` specifies the TTL if non-zero. If zero, the mDNS module will use default TTL for the key entry.
+     *
+     * This method can be called again for the same name to updated a previously registered key entry, for example,
+     * to change the key data or TTL.
+     *
+     * Regarding the invocation of the @p aCallback, this method behaves in the same way as described in
+     * `RegisterHost()`.
+     *
+     * @param[in] aKey          The key record to register.
+     * @param[in] aRequestId    The ID associated with this request.
+     * @param[in] aCallback     The callback function pointer to report the outcome (can be `nullptr` if not needed).
+     *
+     * @retval kErrorNone            Successfully started registration. @p aCallback will report the outcome.
+     * @retval kErrorInvalidState    mDNS module is not enabled.
+     *
+     */
+    Error RegisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback);
+
+    /**
+     * Unregisters a key record on mDNS.
+     *
+     * The fields in @p aKey follow these rules:
+     *
+     * - If the key is associated with a host entry, the `mName` field specifies the host name and the `mServiceType`
+     *    MUST be `nullptr`.
+     * - If the key is associated with a service entry, the `mName` filed specifies the service instance label (always
+     *   treated as a single label) and the `mServiceType` field specifies the service type (e.g. "_tst._udp"). In this
+     *   case the DNS name for key record is `<mName>.<mServiceTye>`.
+     * - The rest of the fields in @p aKey structure are ignored in  a`otMdnsUnregisterKey()` call.
+     *
+     * If there is no previously registered key with the same name, no action is performed.
+     *
+     * If there is a previously registered key with the same name, the mDNS module will send "goodbye" announcements
+     * for the key record.
+     *
+     * @param[in] aKey          The key to unregister.
+     *
+     * @retval kErrorNone            Successfully unregistered key
+     * @retval kErrorInvalidState    mDNS module is not enabled.
+     *
+     */
+    Error UnregisterKey(const Key &aKey);
+
+    /**
+     * Starts a service browser.
+     *
+     * Initiates a continuous search for the specified `mServiceType` in @p aBrowser. For sub-type services, use
+     * `mSubTypeLabel` to define the sub-type, for base services, set `mSubTypeLabel` to NULL.
+     *
+     * Discovered services are reported through the `mCallback` function in @p aBrowser. Services that have been
+     * removed are reported with a TTL value of zero. The callback may be invoked immediately with cached information
+     * (if available) and potentially before this method returns. When cached results are used, the reported TTL value
+     * will reflect the original TTL from the last received response.
+     *
+     * Multiple browsers can be started for the same service, provided they use different callback functions.
+     *
+     * @param[in] aBrowser    The browser to be started.
+     *
+     * @retval kErrorNone           Browser started successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     * @retval kErrorAlready        An identical browser (same service and callback) is already active.
+     *
+     */
+    Error StartBrowser(const Browser &aBrowser);
+
+    /**
+     * Stops a service browser.
+     *
+     * No action is performed if no matching browser with the same service and callback is currently active.
+     *
+     * @param[in] aBrowser    The browser to stop.
+     *
+     * @retval kErrorNone           Browser stopped successfully.
+     * @retval kErrorInvalidSatet  mDNS module is not enabled.
+     *
+     */
+    Error StopBrowser(const Browser &aBrowser);
+
+    /**
+     * Starts an SRV record resolver.
+     *
+     * Initiates a continuous SRV record resolver for the specified service in @p aResolver.
+     *
+     * Discovered information is reported through the `mCallback` function in @p aResolver. When the service is removed
+     * it is reported with a TTL value of zero. In this case, `mHostName` may be NULL and other result fields (such as
+     * `mPort`) should be ignored.
+     *
+     * The callback may be invoked immediately with cached information (if available) and potentially before this
+     * method returns. When cached result is used, the reported TTL value will reflect the original TTL from the last
+     * received response.
+     *
+     * Multiple resolvers can be started for the same service, provided they use different callback functions.
+     *
+     * @param[in] aResolver    The resolver to be started.
+     *
+     * @retval kErrorNone           Resolver started successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     * @retval kErrorAlready        An identical resolver (same service and callback) is already active.
+     *
+     */
+    Error StartSrvResolver(const SrvResolver &aResolver);
+
+    /**
+     * Stops an SRV record resolver.
+     *
+     * No action is performed if no matching resolver with the same service and callback is currently active.
+     *
+     * @param[in] aResolver    The resolver to stop.
+     *
+     * @retval kErrorNone           Resolver stopped successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     *
+     */
+    Error StopSrvResolver(const SrvResolver &aResolver);
+
+    /**
+     * Starts a TXT record resolver.
+     *
+     * Initiates a continuous TXT record resolver for the specified service in @p aResolver.
+     *
+     * Discovered information is reported through the `mCallback` function in @p aResolver. When the TXT record is
+     * removed it is reported with a TTL value of zero. In this case, `mTxtData` may be NULL, and other result fields
+     * (such as `mTxtDataLength`) should be ignored.
+     *
+     * The callback may be invoked immediately with cached information (if available) and potentially before this
+     * method returns. When cached result is used, the reported TTL value will reflect the original TTL from the last
+     * received response.
+     *
+     * Multiple resolvers can be started for the same service, provided they use different callback functions.
+     *
+     * @param[in] aResolver    The resolver to be started.
+     *
+     * @retval kErrorNone           Resolver started successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     * @retval kErrorAlready        An identical resolver (same service and callback) is already active.
+     *
+     */
+    Error StartTxtResolver(const TxtResolver &aResolver);
+
+    /**
+     * Stops a TXT record resolver.
+     *
+     * No action is performed if no matching resolver with the same service and callback is currently active.
+     *
+     * @param[in] aResolver    The resolver to stop.
+     *
+     * @retval kErrorNone           Resolver stopped successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     *
+     */
+    Error StopTxtResolver(const TxtResolver &aResolver);
+
+    /**
+     * Starts an IPv6 address resolver.
+     *
+     * Initiates a continuous IPv6 address resolver for the specified host name in @p aResolver.
+     *
+     * Discovered addresses are reported through the `mCallback` function in @p aResolver. The callback is invoked
+     * whenever addresses are added or removed, providing an updated list. If all addresses are removed, the callback
+     * is invoked with an empty list (`mAddresses` will be NULL, and `mAddressesLength` will be zero).
+     *
+     * The callback may be invoked immediately with cached information (if available) and potentially before this
+     * method returns. When cached result is used, the reported TTL values will reflect the original TTL from the last
+     * received response.
+     *
+     * Multiple resolvers can be started for the same host name, provided they use different callback functions.
+     *
+     * @param[in] aResolver    The resolver to be started.
+     *
+     * @retval kErrorNone           Resolver started successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     * @retval kErrorAlready        An identical resolver (same host and callback) is already active.
+     *
+     */
+    Error StartIp6AddressResolver(const AddressResolver &aResolver);
+
+    /**
+     * Stops an IPv6 address resolver.
+     *
+     * No action is performed if no matching resolver with the same host name and callback is currently active.
+     *
+     * @param[in] aResolver    The resolver to stop.
+     *
+     * @retval kErrorNone           Resolver stopped successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     *
+     */
+    Error StopIp6AddressResolver(const AddressResolver &aResolver);
+
+    /**
+     * Starts an IPv4 address resolver.
+     *
+     * Initiates a continuous IPv4 address resolver for the specified host name in @p aResolver.
+     *
+     * Discovered addresses are reported through the `mCallback` function in @p aResolver. The IPv4 addresses are
+     * represented using the IPv4-mapped IPv6 address format in `mAddresses` array.  The callback is invoked  whenever
+     * addresses are added or removed, providing an updated list. If all addresses are removed, the callback is invoked
+     * with an empty list (`mAddresses` will be NULL, and `mAddressesLength` will be zero).
+     *
+     * The callback may be invoked immediately with cached information (if available) and potentially before this
+     * method returns. When cached result is used, the reported TTL values will reflect the original TTL from the last
+     * received response.
+     *
+     * Multiple resolvers can be started for the same host name, provided they use different callback functions.
+     *
+     * @param[in] aResolver    The resolver to be started.
+     *
+     * @retval kErrorNone           Resolver started successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     * @retval kErrorAlready        An identical resolver (same host and callback) is already active.
+     *
+     */
+    Error StartIp4AddressResolver(const AddressResolver &aResolver);
+
+    /**
+     * Stops an IPv4 address resolver.
+     *
+     * No action is performed if no matching resolver with the same host name and callback is currently active.
+     *
+     * @param[in] aResolver    The resolver to stop.
+     *
+     * @retval kErrorNone           Resolver stopped successfully.
+     * @retval kErrorInvalidState   mDNS module is not enabled.
+     *
+     */
+    Error StopIp4AddressResolver(const AddressResolver &aResolver);
+
+    /**
+     * Sets the max size threshold for mDNS messages.
+     *
+     * This method is mainly intended for testing. The max size threshold is used to break larger messages.
+     *
+     * @param[in] aMaxSize  The max message size threshold.
+     *
+     */
+    void SetMaxMessageSize(uint16_t aMaxSize) { mMaxMessageSize = aMaxSize; }
+
+    /**
+     * Allocates a new iterator.
+     *
+     * @returns   A pointer to the newly allocated iterator or `nullptr` if it fails to allocate.
+     *
+     */
+    Iterator *AllocateIterator(void);
+
+    /**
+     * Frees a previously allocated iterator.
+     *
+     * @param[in] aIterator  The iterator to free.
+     *
+     */
+    void FreeIterator(Iterator &aIterator);
+
+    /**
+     * Iterates over registered host entries.
+     *
+     * On success, @p aHost is populated with information about the next host. Pointers within the `Host` structure
+     * (like `mName`) remain valid until the next call to any OpenThread stack's public or platform API/callback.
+     *
+     * @param[in]  aIterator   The iterator to use.
+     * @param[out] aHost       A `Host` to return the information about the next host entry.
+     * @param[out] aState      An `EntryState` to return the entry state.
+     *
+     * @retval kErrorNone         @p aHost, @p aState, & @p aIterator are updated successfully.
+     * @retval kErrorNotFound     Reached the end of the list.
+     * @retval kErrorInvalidArg   @p aIterator is not valid.
+     *
+     */
+    Error GetNextHost(Iterator &aIterator, Host &aHost, EntryState &aState) const;
+
+    /**
+     * Iterates over registered service entries.
+     *
+     * On success, @p aService is populated with information about the next service. Pointers within the `Service`
+     * structure (like `mServiceType`) remain valid until the next call to any OpenThread stack's public or platform
+     * API/callback.
+     *
+     * @param[out] aService    A `Service` to return the information about the next service entry.
+     * @param[out] aState      An `EntryState` to return the entry state.
+     *
+     * @retval kErrorNone         @p aService, @p aState, & @p aIterator are updated successfully.
+     * @retval kErrorNotFound     Reached the end of the list.
+     * @retval kErrorInvalidArg   @p aIterator is not valid.
+     *
+     */
+    Error GetNextService(Iterator &aIterator, Service &aService, EntryState &aState) const;
+
+    /**
+     * Iterates over registered key entries.
+     *
+     * On success, @p aKey is populated with information about the next key. Pointers within the `Key` structure
+     * (like `mName`) remain valid until the next call to any OpenThread stack's public or platform API/callback.
+     *
+     * @param[out] aKey        A `Key` to return the information about the next key entry.
+     * @param[out] aState      An `EntryState` to return the entry state.
+     *
+     * @retval kErrorNone         @p aKey, @p aState, & @p aIterator are updated successfully.
+     * @retval kErrorNotFound     Reached the end of the list.
+     * @retval kErrorInvalidArg   @p aIterator is not valid.
+     *
+     */
+    Error GetNextKey(Iterator &aIterator, Key &aKey, EntryState &aState) const;
+
+private:
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    static constexpr uint16_t kUdpPort = 5353;
+
+    static constexpr bool kDefaultQuAllowed = OPENTHREAD_CONFIG_MULTICAST_DNS_DEFAULT_QUESTION_UNICAST_ALLOWED;
+
+    static constexpr uint32_t kMaxMessageSize = 1200;
+
+    static constexpr uint8_t  kNumberOfProbes = 3;
+    static constexpr uint32_t kMinProbeDelay  = 20;  // In msec
+    static constexpr uint32_t kMaxProbeDelay  = 250; // In msec
+    static constexpr uint32_t kProbeWaitTime  = 250; // In msec
+
+    static constexpr uint8_t  kNumberOfAnnounces = 3;
+    static constexpr uint32_t kAnnounceInterval  = 1000; // In msec - time between first two announces
+
+    static constexpr uint8_t  kNumberOfInitalQueries = 3;
+    static constexpr uint32_t kInitialQueryInterval  = 1000; // In msec - time between first two queries
+
+    static constexpr uint32_t kMinInitialQueryDelay     = 20;  // msec
+    static constexpr uint32_t kMaxInitialQueryDelay     = 120; // msec
+    static constexpr uint32_t kRandomDelayReuseInterval = 2;   // msec
+
+    static constexpr uint32_t kUnspecifiedTtl = 0;
+    static constexpr uint32_t kDefaultTtl     = 120;
+    static constexpr uint32_t kDefaultKeyTtl  = kDefaultTtl;
+    static constexpr uint32_t kNsecTtl        = 4500;
+    static constexpr uint32_t kServicesPtrTtl = 4500;
+
+    static constexpr uint16_t kClassQuestionUnicastFlag = (1U << 15);
+    static constexpr uint16_t kClassCacheFlushFlag      = (1U << 15);
+    static constexpr uint16_t kClassMask                = (0x7fff);
+
+    static constexpr uint16_t kUnspecifiedOffset = 0;
+
+    static constexpr uint8_t kNumSections = 4;
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    enum Section : uint8_t
+    {
+        kQuestionSection,
+        kAnswerSection,
+        kAuthoritySection,
+        kAdditionalDataSection,
+    };
+
+    enum AppendOutcome : uint8_t
+    {
+        kAppendedFullNameAsCompressed,
+        kAppendedLabels,
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Forward declarations
+
+    class EntryTimerContext;
+    class TxMessage;
+    class RxMessage;
+    class ServiceEntry;
+    class ServiceType;
+    class EntryIterator;
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    struct EmptyChecker
+    {
+        // Used in `Matches()` to find empty entries (with no record) to remove and free.
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    struct ExpireChecker
+    {
+        // Used in `Matches()` to find expired entries in a list.
+
+        explicit ExpireChecker(TimeMilli aNow) { mNow = aNow; }
+
+        TimeMilli mNow;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class Callback : public Clearable<Callback>
+    {
+    public:
+        Callback(void) { Clear(); }
+        Callback(RequestId aRequestId, RegisterCallback aCallback);
+
+        bool IsEmpty(void) const { return (mCallback == nullptr); }
+        void InvokeAndClear(Instance &aInstance, Error aError);
+
+    private:
+        RequestId        mRequestId;
+        RegisterCallback mCallback;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class RecordCounts : public Clearable<RecordCounts>
+    {
+    public:
+        RecordCounts(void) { Clear(); }
+
+        uint16_t GetFor(Section aSection) const { return mCounts[aSection]; }
+        void     Increment(Section aSection) { mCounts[aSection]++; }
+        void     ReadFrom(const Header &aHeader);
+        void     WriteTo(Header &aHeader) const;
+        bool     IsEmpty(void) const;
+
+    private:
+        uint16_t mCounts[kNumSections];
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    struct AnswerInfo
+    {
+        uint16_t  mQuestionRrType;
+        TimeMilli mAnswerTime;
+        bool      mIsProbe;
+        bool      mUnicastResponse;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class AddressArray : public Heap::Array<Ip6::Address>
+    {
+    public:
+        bool Matches(const Ip6::Address *aAddresses, uint16_t aNumAddresses) const;
+        void SetFrom(const Ip6::Address *aAddresses, uint16_t aNumAddresses);
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class FireTime
+    {
+    public:
+        FireTime(void) { ClearFireTime(); }
+        void      ClearFireTime(void) { mHasFireTime = false; }
+        bool      HasFireTime(void) const { return mHasFireTime; }
+        TimeMilli GetFireTime(void) const { return mFireTime; }
+        void      SetFireTime(TimeMilli aFireTime);
+
+    protected:
+        void ScheduleFireTimeOn(TimerMilli &aTimer);
+
+    private:
+        TimeMilli mFireTime;
+        bool      mHasFireTime;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class RecordInfo : public Clearable<RecordInfo>, private NonCopyable
+    {
+    public:
+        // Keeps track of record state and timings.
+
+        RecordInfo(void) { Clear(); }
+
+        bool     IsPresent(void) const { return mIsPresent; }
+        uint32_t GetTtl(void) const { return mTtl; }
+
+        template <typename UintType> void UpdateProperty(UintType &aProperty, UintType aValue);
+        void UpdateProperty(AddressArray &aAddrProperty, const Ip6::Address *aAddrs, uint16_t aNumAddrs);
+        void UpdateProperty(Heap::String &aStringProperty, const char *aString);
+        void UpdateProperty(Heap::Data &aDataProperty, const uint8_t *aData, uint16_t aLength);
+        void UpdateTtl(uint32_t aTtl);
+
+        void     StartAnnouncing(void);
+        bool     ShouldAppendTo(TxMessage &aResponse, TimeMilli aNow) const;
+        bool     CanAnswer(void) const;
+        void     ScheduleAnswer(const AnswerInfo &aInfo);
+        void     UpdateStateAfterAnswer(const TxMessage &aResponse);
+        void     UpdateFireTimeOn(FireTime &aFireTime);
+        uint32_t GetDurationSinceLastMulticast(TimeMilli aTime) const;
+        Error    GetLastMulticastTime(TimeMilli &aLastMulticastTime) const;
+
+        // `AppendState` methods: Used to track whether the record
+        // is appended in a message, or needs to be appended in
+        // Additional Data section.
+
+        void MarkAsNotAppended(void) { mAppendState = kNotAppended; }
+        void MarkAsAppended(TxMessage &aTxMessage, Section aSection);
+        void MarkToAppendInAdditionalData(void);
+        bool IsAppended(void) const;
+        bool CanAppend(void) const;
+        bool ShouldAppendInAdditionalDataSection(void) const { return (mAppendState == kToAppendInAdditionalData); }
+
+    private:
+        enum AppendState : uint8_t
+        {
+            kNotAppended,
+            kToAppendInAdditionalData,
+            kAppendedInMulticastMsg,
+            kAppendedInUnicastMsg,
+        };
+
+        static constexpr uint32_t kMinIntervalBetweenMulticast = 1000; // msec
+        static constexpr uint32_t kLastMulticastTimeAge        = 10 * Time::kOneHourInMsec;
+
+        static_assert(kNotAppended == 0, "kNotAppended MUST be zero, so `Clear()` works correctly");
+
+        bool        mIsPresent : 1;
+        bool        mMulticastAnswerPending : 1;
+        bool        mUnicastAnswerPending : 1;
+        bool        mIsLastMulticastValid : 1;
+        uint8_t     mAnnounceCounter;
+        AppendState mAppendState;
+        Section     mAppendSection;
+        uint32_t    mTtl;
+        TimeMilli   mAnnounceTime;
+        TimeMilli   mAnswerTime;
+        TimeMilli   mLastMulticastTime;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class Entry : public InstanceLocatorInit, public FireTime, private NonCopyable
+    {
+        // Base class for `HostEntry` and `ServiceEntry`.
+
+        friend class ServiceType;
+
+    public:
+        enum State : uint8_t
+        {
+            kProbing    = OT_MDNS_ENTRY_STATE_PROBING,
+            kRegistered = OT_MDNS_ENTRY_STATE_REGISTERED,
+            kConflict   = OT_MDNS_ENTRY_STATE_CONFLICT,
+            kRemoving   = OT_MDNS_ENTRY_STATE_REMOVING,
+        };
+
+        State GetState(void) const { return mState; }
+        bool  HasKeyRecord(void) const { return mKeyRecord.IsPresent(); }
+        void  Register(const Key &aKey, const Callback &aCallback);
+        void  Unregister(const Key &aKey);
+        void  InvokeCallbacks(void);
+        void  ClearAppendState(void);
+        Error CopyKeyInfoTo(Key &aKey, EntryState &aState) const;
+
+    protected:
+        static constexpr uint32_t kMinIntervalProbeResponse = 250; // msec
+        static constexpr uint8_t  kTypeArraySize            = 8;   // We can have SRV, TXT and KEY today.
+
+        struct TypeArray : public Array<uint16_t, kTypeArraySize> // Array of record types for NSEC record
+        {
+            void Add(uint16_t aType) { SuccessOrAssert(PushBack(aType)); }
+        };
+
+        struct RecordAndType
+        {
+            RecordInfo &mRecord;
+            uint16_t    mType;
+        };
+
+        typedef void (*NameAppender)(Entry &aEntry, TxMessage &aTxMessage, Section aSection);
+
+        Entry(void);
+        void Init(Instance &aInstance);
+        void SetCallback(const Callback &aCallback);
+        void ClearCallback(void) { mCallback.Clear(); }
+        void StartProbing(void);
+        void SetStateToConflict(void);
+        void SetStateToRemoving(void);
+        void UpdateRecordsState(const TxMessage &aResponse);
+        void AppendQuestionTo(TxMessage &aTxMessage) const;
+        void AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection, NameAppender aNameAppender);
+        void AppendNsecRecordTo(TxMessage       &aTxMessage,
+                                Section          aSection,
+                                const TypeArray &aTypes,
+                                NameAppender     aNameAppender);
+        bool ShouldAnswerNsec(TimeMilli aNow) const;
+        void DetermineNextFireTime(void);
+        void ScheduleTimer(void);
+        void AnswerProbe(const AnswerInfo &aInfo, RecordAndType *aRecords, uint16_t aRecordsLength);
+        void AnswerNonProbe(const AnswerInfo &aInfo, RecordAndType *aRecords, uint16_t aRecordsLength);
+        void ScheduleNsecAnswer(const AnswerInfo &aInfo);
+
+        template <typename EntryType> void HandleTimer(EntryTimerContext &aContext);
+
+        RecordInfo mKeyRecord;
+
+    private:
+        void SetState(State aState);
+        void ClearKey(void);
+        void ScheduleCallbackTask(void);
+        void CheckMessageSizeLimitToPrepareAgain(TxMessage &aTxMessage, bool &aPrepareAgain);
+
+        State      mState;
+        uint8_t    mProbeCount;
+        bool       mMulticastNsecPending : 1;
+        bool       mUnicastNsecPending : 1;
+        bool       mAppendedNsec : 1;
+        TimeMilli  mNsecAnswerTime;
+        Heap::Data mKeyData;
+        Callback   mCallback;
+        Callback   mKeyCallback;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class HostEntry : public Entry, public LinkedListEntry<HostEntry>, public Heap::Allocatable<HostEntry>
+    {
+        friend class LinkedListEntry<HostEntry>;
+        friend class Entry;
+        friend class ServiceEntry;
+
+    public:
+        HostEntry(void);
+        Error Init(Instance &aInstance, const Host &aHost) { return Init(aInstance, aHost.mHostName); }
+        Error Init(Instance &aInstance, const Key &aKey) { return Init(aInstance, aKey.mName); }
+        bool  IsEmpty(void) const;
+        bool  Matches(const Name &aName) const;
+        bool  Matches(const Host &aHost) const;
+        bool  Matches(const Key &aKey) const;
+        bool  Matches(const Heap::String &aName) const;
+        bool  Matches(State aState) const { return GetState() == aState; }
+        bool  Matches(const HostEntry &aEntry) const { return (this == &aEntry); }
+        void  Register(const Host &aHost, const Callback &aCallback);
+        void  Register(const Key &aKey, const Callback &aCallback);
+        void  Unregister(const Host &aHost);
+        void  Unregister(const Key &aKey);
+        void  AnswerQuestion(const AnswerInfo &aInfo);
+        void  HandleTimer(EntryTimerContext &aContext);
+        void  ClearAppendState(void);
+        void  PrepareResponse(TxMessage &aResponse, TimeMilli aNow);
+        void  HandleConflict(void);
+        Error CopyInfoTo(Host &aHost, EntryState &aState) const;
+        Error CopyInfoTo(Key &aKey, EntryState &aState) const;
+
+    private:
+        Error Init(Instance &aInstance, const char *aName);
+        void  ClearHost(void);
+        void  ScheduleToRemoveIfEmpty(void);
+        void  PrepareProbe(TxMessage &aProbe);
+        void  StartAnnouncing(void);
+        void  PrepareResponseRecords(TxMessage &aResponse, TimeMilli aNow);
+        void  UpdateRecordsState(const TxMessage &aResponse);
+        void  DetermineNextFireTime(void);
+        void  AppendAddressRecordsTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendNsecRecordTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendNameTo(TxMessage &aTxMessage, Section aSection);
+
+        static void AppendEntryName(Entry &aEntry, TxMessage &aTxMessage, Section aSection);
+
+        HostEntry   *mNext;
+        Heap::String mName;
+        RecordInfo   mAddrRecord;
+        AddressArray mAddresses;
+        uint16_t     mNameOffset;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class ServiceEntry : public Entry, public LinkedListEntry<ServiceEntry>, public Heap::Allocatable<ServiceEntry>
+    {
+        friend class LinkedListEntry<ServiceEntry>;
+        friend class Entry;
+        friend class ServiceType;
+
+    public:
+        ServiceEntry(void);
+        Error Init(Instance &aInstance, const Service &aService);
+        Error Init(Instance &aInstance, const Key &aKey);
+        bool  IsEmpty(void) const;
+        bool  Matches(const Name &aFullName) const;
+        bool  Matches(const Service &aService) const;
+        bool  Matches(const Key &aKey) const;
+        bool  Matches(State aState) const { return GetState() == aState; }
+        bool  Matches(const ServiceEntry &aEntry) const { return (this == &aEntry); }
+        bool  MatchesServiceType(const Name &aServiceType) const;
+        bool  CanAnswerSubType(const char *aSubLabel) const;
+        void  Register(const Service &aService, const Callback &aCallback);
+        void  Register(const Key &aKey, const Callback &aCallback);
+        void  Unregister(const Service &aService);
+        void  Unregister(const Key &aKey);
+        void  AnswerServiceNameQuestion(const AnswerInfo &aInfo);
+        void  AnswerServiceTypeQuestion(const AnswerInfo &aInfo, const char *aSubLabel);
+        bool  ShouldSuppressKnownAnswer(uint32_t aTtl, const char *aSubLabel) const;
+        void  HandleTimer(EntryTimerContext &aContext);
+        void  ClearAppendState(void);
+        void  PrepareResponse(TxMessage &aResponse, TimeMilli aNow);
+        void  HandleConflict(void);
+        Error CopyInfoTo(Service &aService, EntryState &aState, EntryIterator &aIterator) const;
+        Error CopyInfoTo(Key &aKey, EntryState &aState) const;
+
+    private:
+        class SubType : public LinkedListEntry<SubType>, public Heap::Allocatable<SubType>, private ot::NonCopyable
+        {
+        public:
+            Error Init(const char *aLabel);
+            bool  Matches(const char *aLabel) const { return NameMatch(mLabel, aLabel); }
+            bool  Matches(const EmptyChecker &aChecker) const;
+            bool  IsContainedIn(const Service &aService) const;
+
+            SubType     *mNext;
+            Heap::String mLabel;
+            RecordInfo   mPtrRecord;
+            uint16_t     mSubServiceNameOffset;
+        };
+
+        Error Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType);
+        void  ClearService(void);
+        void  ScheduleToRemoveIfEmpty(void);
+        void  PrepareProbe(TxMessage &aProbe);
+        void  StartAnnouncing(void);
+        void  PrepareResponseRecords(TxMessage &aResponse, TimeMilli aNow);
+        void  UpdateRecordsState(const TxMessage &aResponse);
+        void  DetermineNextFireTime(void);
+        void  DiscoverOffsetsAndHost(HostEntry *&aHost);
+        void  UpdateServiceTypes(void);
+        void  AppendSrvRecordTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendTxtRecordTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendPtrRecordTo(TxMessage &aTxMessage, Section aSection, SubType *aSubType = nullptr);
+        void  AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendNsecRecordTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendServiceNameTo(TxMessage &TxMessage, Section aSection);
+        void  AppendServiceTypeTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendSubServiceTypeTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendSubServiceNameTo(TxMessage &aTxMessage, Section aSection, SubType &aSubType);
+        void  AppendHostNameTo(TxMessage &aTxMessage, Section aSection);
+
+        static void AppendEntryName(Entry &aEntry, TxMessage &aTxMessage, Section aSection);
+
+        static const uint8_t kEmptyTxtData[];
+
+        ServiceEntry       *mNext;
+        Heap::String        mServiceInstance;
+        Heap::String        mServiceType;
+        RecordInfo          mPtrRecord;
+        RecordInfo          mSrvRecord;
+        RecordInfo          mTxtRecord;
+        OwningList<SubType> mSubTypes;
+        Heap::String        mHostName;
+        Heap::Data          mTxtData;
+        uint16_t            mPriority;
+        uint16_t            mWeight;
+        uint16_t            mPort;
+        uint16_t            mServiceNameOffset;
+        uint16_t            mServiceTypeOffset;
+        uint16_t            mSubServiceTypeOffset;
+        uint16_t            mHostNameOffset;
+        bool                mIsAddedInServiceTypes;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class ServiceType : public InstanceLocatorInit,
+                        public FireTime,
+                        public LinkedListEntry<ServiceType>,
+                        public Heap::Allocatable<ServiceType>,
+                        private NonCopyable
+    {
+        // Track a service type to answer to `_services._dns-sd._udp.local`
+        // queries.
+
+        friend class LinkedListEntry<ServiceType>;
+
+    public:
+        Error    Init(Instance &aInstance, const char *aServiceType);
+        bool     Matches(const Name &aServiceTypeName) const;
+        bool     Matches(const Heap::String &aServiceType) const;
+        bool     Matches(const ServiceType &aServiceType) const { return (this == &aServiceType); }
+        void     IncrementNumEntries(void) { mNumEntries++; }
+        void     DecrementNumEntries(void) { mNumEntries--; }
+        uint16_t GetNumEntries(void) const { return mNumEntries; }
+        void     ClearAppendState(void);
+        void     AnswerQuestion(const AnswerInfo &aInfo);
+        bool     ShouldSuppressKnownAnswer(uint32_t aTtl) const;
+        void     HandleTimer(EntryTimerContext &aContext);
+        void     PrepareResponse(TxMessage &aResponse, TimeMilli aNow);
+
+    private:
+        void PrepareResponseRecords(TxMessage &aResponse, TimeMilli aNow);
+        void AppendPtrRecordTo(TxMessage &aResponse, uint16_t aServiceTypeOffset);
+
+        ServiceType *mNext;
+        Heap::String mServiceType;
+        RecordInfo   mServicesPtr;
+        uint16_t     mNumEntries; // Number of service entries providing this service type.
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class TxMessage : public InstanceLocator
+    {
+    public:
+        enum Type : uint8_t
+        {
+            kMulticastProbe,
+            kMulticastQuery,
+            kMulticastResponse,
+            kUnicastResponse,
+        };
+
+        TxMessage(Instance &aInstance, Type aType);
+        TxMessage(Instance &aInstance, Type aType, const AddressInfo &aUnicastDest);
+        Type          GetType(void) const { return mType; }
+        Message      &SelectMessageFor(Section aSection);
+        AppendOutcome AppendLabel(Section aSection, const char *aLabel, uint16_t &aCompressOffset);
+        AppendOutcome AppendMultipleLabels(Section aSection, const char *aLabels, uint16_t &aCompressOffset);
+        void          AppendServiceType(Section aSection, const char *aServiceType, uint16_t &aCompressOffset);
+        void          AppendDomainName(Section aSection);
+        void          AppendServicesDnssdName(Section aSection);
+        void          IncrementRecordCount(Section aSection) { mRecordCounts.Increment(aSection); }
+        void          CheckSizeLimitToPrepareAgain(bool &aPrepareAgain);
+        void          SaveCurrentState(void);
+        void          RestoreToSavedState(void);
+        void          Send(void);
+
+    private:
+        static constexpr bool kIsSingleLabel = true;
+
+        void          Init(Type aType);
+        void          Reinit(void);
+        bool          IsOverSizeLimit(void) const;
+        AppendOutcome AppendLabels(Section     aSection,
+                                   const char *aLabels,
+                                   bool        aIsSingleLabel,
+                                   uint16_t   &aCompressOffset);
+        bool          ShouldClearAppendStateOnReinit(const Entry &aEntry) const;
+
+        static void SaveOffset(uint16_t &aCompressOffset, const Message &aMessage, Section aSection);
+
+        RecordCounts      mRecordCounts;
+        OwnedPtr<Message> mMsgPtr;
+        OwnedPtr<Message> mExtraMsgPtr;
+        RecordCounts      mSavedRecordCounts;
+        uint16_t          mSavedMsgLength;
+        uint16_t          mSavedExtraMsgLength;
+        uint16_t          mDomainOffset;        // Offset for domain name `.local.` for name compression.
+        uint16_t          mUdpOffset;           // Offset to `_udp.local.`
+        uint16_t          mTcpOffset;           // Offset to `_tcp.local.`
+        uint16_t          mServicesDnssdOffset; // Offset to `_services._dns-sd`
+        AddressInfo       mUnicastDest;
+        Type              mType;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class TimerContext : public InstanceLocator
+    {
+    public:
+        TimerContext(Instance &aInstance);
+
+        TimeMilli GetNow(void) const { return mNow; }
+        TimeMilli GetNextTime(void) const { return mNextTime; }
+        void      UpdateNextTime(TimeMilli aTime);
+
+    private:
+        TimeMilli mNow;
+        TimeMilli mNextTime;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class EntryTimerContext : public TimerContext // Used by `HandleEntryTimer`.
+    {
+    public:
+        EntryTimerContext(Instance &aInstance);
+        TxMessage &GetProbeMessage(void) { return mProbeMessage; }
+        TxMessage &GetResponseMessage(void) { return mResponseMessage; }
+
+    private:
+        TxMessage mProbeMessage;
+        TxMessage mResponseMessage;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class RxMessage : public InstanceLocatorInit,
+                      public Heap::Allocatable<RxMessage>,
+                      public LinkedListEntry<RxMessage>,
+                      private NonCopyable
+    {
+        friend class LinkedListEntry<RxMessage>;
+
+    public:
+        enum ProcessOutcome : uint8_t
+        {
+            kProcessed,
+            kSaveAsMultiPacket,
+        };
+
+        Error               Init(Instance          &aInstance,
+                                 OwnedPtr<Message> &aMessagePtr,
+                                 bool               aIsUnicast,
+                                 const AddressInfo &aSenderAddress);
+        bool                IsQuery(void) const { return mIsQuery; }
+        bool                IsTruncated(void) const { return mTruncated; }
+        bool                IsSelfOriginating(void) const { return mIsSelfOriginating; }
+        const RecordCounts &GetRecordCounts(void) const { return mRecordCounts; }
+        const AddressInfo  &GetSenderAddress(void) const { return mSenderAddress; }
+        void                ClearProcessState(void);
+        ProcessOutcome      ProcessQuery(bool aShouldProcessTruncated);
+        void                ProcessResponse(void);
+
+    private:
+        typedef void (RxMessage::*RecordProcessor)(const Name           &aName,
+                                                   const ResourceRecord &aRecord,
+                                                   uint16_t              aRecordOffset);
+
+        struct Question : public Clearable<Question>
+        {
+            Question(void) { Clear(); }
+            void ClearProcessState(void);
+
+            Entry   *mEntry;                     // Entry which can provide answer (if any).
+            uint16_t mNameOffset;                // Offset to start of question name.
+            uint16_t mRrType;                    // The question record type.
+            bool     mIsRrClassInternet : 1;     // Is the record class Internet or Any.
+            bool     mIsProbe : 1;               // Is a probe (contains a matching record in Authority section).
+            bool     mUnicastResponse : 1;       // Is QU flag set (requesting a unicast response).
+            bool     mCanAnswer : 1;             // Can provide answer for this question
+            bool     mIsUnique : 1;              // Is unique record (vs a shared record).
+            bool     mIsForService : 1;          // Is for a `ServiceEntry` (vs a `HostEntry`).
+            bool     mIsServiceType : 1;         // Is for service type or sub-type of a `ServiceEntry`.
+            bool     mIsForAllServicesDnssd : 1; // Is for "_services._dns-sd._udp" (all service types).
+        };
+
+        static constexpr uint32_t kMinResponseDelay = 20;  // msec
+        static constexpr uint32_t kMaxResponseDelay = 120; // msec
+
+        void ProcessQuestion(Question &aQuestion);
+        void AnswerQuestion(const Question &aQuestion, TimeMilli aAnswerTime);
+        void AnswerServiceTypeQuestion(const Question &aQuestion, const AnswerInfo &aInfo, ServiceEntry &aFirstEntry);
+        bool ShouldSuppressKnownAnswer(const Name         &aServiceType,
+                                       const char         *aSubLabel,
+                                       const ServiceEntry &aServiceEntry) const;
+        bool ParseQuestionNameAsSubType(const Question    &aQuestion,
+                                        Name::LabelBuffer &aSubLabel,
+                                        Name              &aServiceType) const;
+        void AnswerAllServicesQuestion(const Question &aQuestion, const AnswerInfo &aInfo);
+        bool ShouldSuppressKnownAnswer(const Question &aQuestion, const ServiceType &aServiceType) const;
+        void SendUnicastResponse(const AddressInfo &aUnicastDest);
+        void IterateOnAllRecordsInResponse(RecordProcessor aRecordProcessor);
+        void ProcessRecordForConflict(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset);
+        void ProcessPtrRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset);
+        void ProcessSrvRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset);
+        void ProcessTxtRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset);
+        void ProcessAaaaRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset);
+        void ProcessARecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset);
+
+        RxMessage            *mNext;
+        OwnedPtr<Message>     mMessagePtr;
+        Heap::Array<Question> mQuestions;
+        AddressInfo           mSenderAddress;
+        RecordCounts          mRecordCounts;
+        uint16_t              mStartOffset[kNumSections];
+        bool                  mIsQuery : 1;
+        bool                  mIsUnicast : 1;
+        bool                  mTruncated : 1;
+        bool                  mIsSelfOriginating : 1;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    void HandleMultiPacketTimer(void) { mMultiPacketRxMessages.HandleTimer(); }
+
+    class MultiPacketRxMessages : public InstanceLocator
+    {
+    public:
+        explicit MultiPacketRxMessages(Instance &aInstance);
+
+        void AddToExisting(OwnedPtr<RxMessage> &aRxMessagePtr);
+        void AddNew(OwnedPtr<RxMessage> &aRxMessagePtr);
+        void HandleTimer(void);
+        void Clear(void);
+
+    private:
+        static constexpr uint32_t kMinProcessDelay = 400; // msec
+        static constexpr uint32_t kMaxProcessDelay = 500; // msec
+        static constexpr uint16_t kMaxNumMessages  = 10;
+
+        struct RxMsgEntry : public InstanceLocator,
+                            public LinkedListEntry<RxMsgEntry>,
+                            public Heap::Allocatable<RxMsgEntry>,
+                            private NonCopyable
+        {
+            explicit RxMsgEntry(Instance &aInstance);
+
+            bool Matches(const AddressInfo &aAddress) const;
+            bool Matches(const ExpireChecker &aExpireChecker) const;
+            void Add(OwnedPtr<RxMessage> &aRxMessagePtr);
+
+            OwningList<RxMessage> mRxMessages;
+            TimeMilli             mProcessTime;
+            RxMsgEntry           *mNext;
+        };
+
+        using MultiPacketTimer = TimerMilliIn<Core, &Core::HandleMultiPacketTimer>;
+
+        OwningList<RxMsgEntry> mRxMsgEntries;
+        MultiPacketTimer       mTimer;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    void HandleTxMessageHistoryTimer(void) { mTxMessageHistory.HandleTimer(); }
+
+    class TxMessageHistory : public InstanceLocator
+    {
+        // Keep track of messages sent by mDNS module to tell if
+        // a received message is self originating.
+
+    public:
+        explicit TxMessageHistory(Instance &aInstance);
+        void Clear(void);
+        void Add(const Message &aMessage);
+        bool Contains(const Message &aMessage) const;
+        void HandleTimer(void);
+
+    private:
+        static constexpr uint32_t kExpireInterval = TimeMilli::SecToMsec(10); // in msec
+
+        typedef Crypto::Sha256::Hash Hash;
+
+        struct HashEntry : public LinkedListEntry<HashEntry>, public Heap::Allocatable<HashEntry>
+        {
+            bool Matches(const Hash &aHash) const { return aHash == mHash; }
+            bool Matches(const ExpireChecker &aExpireChecker) const { return mExpireTime <= aExpireChecker.mNow; }
+
+            HashEntry *mNext;
+            Hash       mHash;
+            TimeMilli  mExpireTime;
+        };
+
+        static void CalculateHash(const Message &aMessage, Hash &aHash);
+
+        using TxMsgHistoryTimer = TimerMilliIn<Core, &Core::HandleTxMessageHistoryTimer>;
+
+        OwningList<HashEntry> mHashEntries;
+        TxMsgHistoryTimer     mTimer;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class CacheEntry;
+    class TxtCache;
+
+    class ResultCallback : public LinkedListEntry<ResultCallback>, public Heap::Allocatable<ResultCallback>
+    {
+        friend class Heap::Allocatable<ResultCallback>;
+        friend class LinkedListEntry<ResultCallback>;
+        friend class CacheEntry;
+
+    public:
+        ResultCallback(const ResultCallback &aResultCallback) = default;
+
+        template <typename CallbackType>
+        explicit ResultCallback(CallbackType aCallback)
+            : mNext(nullptr)
+            , mSharedCallback(aCallback)
+        {
+        }
+
+        bool Matches(BrowseCallback aCallback) const { return mSharedCallback.mBrowse == aCallback; }
+        bool Matches(SrvCallback aCallback) const { return mSharedCallback.mSrv == aCallback; }
+        bool Matches(TxtCallback aCallback) const { return mSharedCallback.mTxt == aCallback; }
+        bool Matches(AddressCallback aCallback) const { return mSharedCallback.mAddress == aCallback; }
+        bool Matches(EmptyChecker) const { return (mSharedCallback.mSrv == nullptr); }
+
+        void Invoke(Instance &aInstance, const BrowseResult &aResult) const;
+        void Invoke(Instance &aInstance, const SrvResult &aResult) const;
+        void Invoke(Instance &aInstance, const TxtResult &aResult) const;
+        void Invoke(Instance &aInstance, const AddressResult &aResult) const;
+
+        void ClearCallback(void) { mSharedCallback.Clear(); }
+
+    private:
+        union SharedCallback
+        {
+            explicit SharedCallback(BrowseCallback aCallback) { mBrowse = aCallback; }
+            explicit SharedCallback(SrvCallback aCallback) { mSrv = aCallback; }
+            explicit SharedCallback(TxtCallback aCallback) { mTxt = aCallback; }
+            explicit SharedCallback(AddressCallback aCallback) { mAddress = aCallback; }
+
+            void Clear(void) { mBrowse = nullptr; }
+
+            BrowseCallback  mBrowse;
+            SrvCallback     mSrv;
+            TxtCallback     mTxt;
+            AddressCallback mAddress;
+        };
+
+        ResultCallback *mNext;
+        SharedCallback  mSharedCallback;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class CacheTimerContext : public TimerContext
+    {
+    public:
+        CacheTimerContext(Instance &aInstance);
+        TxMessage &GetQueryMessage(void) { return mQueryMessage; }
+
+    private:
+        TxMessage mQueryMessage;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class CacheRecordInfo
+    {
+    public:
+        CacheRecordInfo(void);
+
+        bool     IsPresent(void) const { return (mTtl > 0); }
+        uint32_t GetTtl(void) const { return mTtl; }
+        bool     RefreshTtl(uint32_t aTtl);
+        bool     ShouldExpire(TimeMilli aNow) const;
+        void     UpdateStateAfterQuery(TimeMilli aNow);
+        void     UpdateQueryAndFireTimeOn(CacheEntry &aCacheEntry);
+        bool     LessThanHalfTtlRemains(TimeMilli aNow) const;
+        uint32_t GetRemainingTtl(TimeMilli aNow) const;
+
+    private:
+        static constexpr uint32_t kMaxTtl            = (24 * 3600); // One day
+        static constexpr uint8_t  kNumberOfQueries   = 4;
+        static constexpr uint32_t kQueryTtlVariation = 1000 * 2 / 100; // 2%
+
+        uint32_t  GetClampedTtl(void) const;
+        TimeMilli GetExpireTime(void) const;
+        TimeMilli GetQueryTime(uint8_t aAttemptIndex) const;
+
+        uint32_t  mTtl;
+        TimeMilli mLastRxTime;
+        uint8_t   mQueryCount;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class CacheEntry : public FireTime, public InstanceLocatorInit, private NonCopyable
+    {
+        // Base class for cache entries: `BrowseCache`, `mSrvCache`,
+        // `mTxtCache`, etc. Implements common behaviors: initial
+        // queries, query/timer scheduling, callback tracking, entry
+        // aging, and timer handling. Tracks entry type in `mType` and
+        // invokes sub-class method for type-specific behaviors
+        // (e.g., query message construction).
+
+    public:
+        void HandleTimer(CacheTimerContext &aContext);
+        void ClearEmptyCallbacks(void);
+        void ScheduleQuery(TimeMilli aQueryTime);
+
+    protected:
+        enum Type : uint8_t
+        {
+            kBrowseCache,
+            kSrvCache,
+            kTxtCache,
+            kIp6AddrCache,
+            kIp4AddrCache,
+        };
+
+        void  Init(Instance &aInstance, Type aType);
+        bool  IsActive(void) const { return mIsActive; }
+        bool  ShouldDelete(TimeMilli aNow) const;
+        void  StartInitialQueries(void);
+        void  StopInitialQueries(void) { mInitalQueries = kNumberOfInitalQueries; }
+        Error Add(const ResultCallback &aCallback);
+        void  Remove(const ResultCallback &aCallback);
+        void  DetermineNextFireTime(void);
+        void  ScheduleTimer(void);
+
+        template <typename ResultType> void InvokeCallbacks(const ResultType &aResult);
+
+    private:
+        static constexpr uint32_t kMinIntervalBetweenQueries = 1000; // In msec
+        static constexpr uint32_t kNonActiveDeleteTimeout    = 7 * Time::kOneMinuteInMsec;
+
+        typedef OwningList<ResultCallback> CallbackList;
+
+        void SetIsActive(bool aIsActive);
+        bool ShouldQuery(TimeMilli aNow);
+        void PrepareQuery(CacheTimerContext &aContext);
+        void ProcessExpiredRecords(TimeMilli aNow);
+        void DetermineNextInitialQueryTime(void);
+
+        ResultCallback *FindCallbackMatching(const ResultCallback &aCallback);
+
+        template <typename CacheType> CacheType       &As(void) { return *static_cast<CacheType *>(this); }
+        template <typename CacheType> const CacheType &As(void) const { return *static_cast<const CacheType *>(this); }
+
+        Type         mType;                   // Cache entry type.
+        uint8_t      mInitalQueries;          // Number initial queries sent already.
+        bool         mQueryPending : 1;       // Whether a query tx request is pending.
+        bool         mLastQueryTimeValid : 1; // Whether `mLastQueryTime` is valid.
+        bool         mIsActive : 1;           // Whether there is any active resolver/browser for this entry.
+        TimeMilli    mNextQueryTime;          // The next query tx time when `mQueryPending`.
+        TimeMilli    mLastQueryTime;          // The last query tx time or the upcoming tx time of first initial query.
+        TimeMilli    mDeleteTime;             // The time to delete the entry when not `mIsActive`.
+        CallbackList mCallbacks;              // Resolver/Browser callbacks.
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class BrowseCache : public CacheEntry, public LinkedListEntry<BrowseCache>, public Heap::Allocatable<BrowseCache>
+    {
+        friend class LinkedListEntry<BrowseCache>;
+        friend class Heap::Allocatable<BrowseCache>;
+        friend class CacheEntry;
+
+    public:
+        void  ClearCompressOffsets(void);
+        bool  Matches(const Name &aFullName) const;
+        bool  Matches(const char *aServiceType, const char *aSubTypeLabel) const;
+        bool  Matches(const Browser &aBrowser) const;
+        bool  Matches(const ExpireChecker &aExpireChecker) const;
+        Error Add(const Browser &aBrowser);
+        void  Remove(const Browser &aBrowser);
+        void  ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset);
+
+    private:
+        struct PtrEntry : public LinkedListEntry<PtrEntry>, public Heap::Allocatable<PtrEntry>
+        {
+            Error Init(const char *aServiceInstance);
+            bool  Matches(const char *aServiceInstance) const { return NameMatch(mServiceInstance, aServiceInstance); }
+            bool  Matches(const ExpireChecker &aExpireChecker) const;
+            void  ConvertTo(BrowseResult &aResult, const BrowseCache &aBrowseCache) const;
+
+            PtrEntry       *mNext;
+            Heap::String    mServiceInstance;
+            CacheRecordInfo mRecord;
+        };
+
+        // Called by base class `CacheEntry`
+        void PreparePtrQuestion(TxMessage &aQuery, TimeMilli aNow);
+        void UpdateRecordStateAfterQuery(TimeMilli aNow);
+        void DetermineRecordFireTime(void);
+        void ProcessExpiredRecords(TimeMilli aNow);
+        void ReportResultsTo(ResultCallback &aCallback) const;
+
+        Error Init(Instance &aInstance, const char *aServiceType, const char *aSubTypeLabel);
+        Error Init(Instance &aInstance, const Browser &aBrowser);
+        void  AppendServiceTypeOrSubTypeTo(TxMessage &aTxMessage, Section aSection);
+        void  AppendKnownAnswer(TxMessage &aTxMessage, const PtrEntry &aPtrEntry, TimeMilli aNow);
+        void  DiscoverCompressOffsets(void);
+
+        BrowseCache         *mNext;
+        Heap::String         mServiceType;
+        Heap::String         mSubTypeLabel;
+        OwningList<PtrEntry> mPtrEntries;
+        uint16_t             mServiceTypeOffset;
+        uint16_t             mSubServiceTypeOffset;
+        uint16_t             mSubServiceNameOffset;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    struct ServiceName
+    {
+        ServiceName(const char *aServiceInstance, const char *aServiceType)
+            : mServiceInstance(aServiceInstance)
+            , mServiceType(aServiceType)
+        {
+        }
+
+        const char *mServiceInstance;
+        const char *mServiceType;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class ServiceCache : public CacheEntry
+    {
+        // Base class for `SrvCache` and `TxtCache`, tracking common info
+        // shared between the two, e.g. service instance/type strings,
+        // record info, and append state and compression offsets.
+
+        friend class CacheEntry;
+
+    public:
+        void ClearCompressOffsets(void);
+
+    protected:
+        ServiceCache(void) = default;
+
+        Error Init(Instance &aInstance, Type aType, const char *aServiceInstance, const char *aServiceType);
+        bool  Matches(const Name &aFullName) const;
+        bool  Matches(const char *aServiceInstance, const char *aServiceType) const;
+        void  PrepareQueryQuestion(TxMessage &aQuery, uint16_t aRrType);
+        void  AppendServiceNameTo(TxMessage &aTxMessage, Section aSection);
+        void  UpdateRecordStateAfterQuery(TimeMilli aNow);
+        void  DetermineRecordFireTime(void);
+        bool  ShouldStartInitialQueries(void) const;
+
+        CacheRecordInfo mRecord;
+        Heap::String    mServiceInstance;
+        Heap::String    mServiceType;
+        uint16_t        mServiceNameOffset;
+        uint16_t        mServiceTypeOffset;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class SrvCache : public ServiceCache, public LinkedListEntry<SrvCache>, public Heap::Allocatable<SrvCache>
+    {
+        friend class LinkedListEntry<SrvCache>;
+        friend class Heap::Allocatable<SrvCache>;
+        friend class CacheEntry;
+        friend class TxtCache;
+        friend class BrowseCache;
+
+    public:
+        bool  Matches(const Name &aFullName) const;
+        bool  Matches(const SrvResolver &aResolver) const;
+        bool  Matches(const ServiceName &aServiceName) const;
+        bool  Matches(const ExpireChecker &aExpireChecker) const;
+        Error Add(const SrvResolver &aResolver);
+        void  Remove(const SrvResolver &aResolver);
+        void  ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset);
+
+    private:
+        Error Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType);
+        Error Init(Instance &aInstance, const ServiceName &aServiceName);
+        Error Init(Instance &aInstance, const SrvResolver &aResolver);
+        void  PrepareSrvQuestion(TxMessage &aQuery);
+        void  DiscoverCompressOffsets(void);
+        void  ProcessExpiredRecords(TimeMilli aNow);
+        void  ReportResultTo(ResultCallback &aCallback) const;
+        void  ConvertTo(SrvResult &aResult) const;
+
+        SrvCache    *mNext;
+        Heap::String mHostName;
+        uint16_t     mPort;
+        uint16_t     mPriority;
+        uint16_t     mWeight;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class TxtCache : public ServiceCache, public LinkedListEntry<TxtCache>, public Heap::Allocatable<TxtCache>
+    {
+        friend class LinkedListEntry<TxtCache>;
+        friend class Heap::Allocatable<TxtCache>;
+        friend class CacheEntry;
+        friend class BrowseCache;
+
+    public:
+        bool  Matches(const Name &aFullName) const;
+        bool  Matches(const TxtResolver &aResolver) const;
+        bool  Matches(const ServiceName &aServiceName) const;
+        bool  Matches(const ExpireChecker &aExpireChecker) const;
+        Error Add(const TxtResolver &aResolver);
+        void  Remove(const TxtResolver &aResolver);
+        void  ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset);
+
+    private:
+        Error Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType);
+        Error Init(Instance &aInstance, const ServiceName &aServiceName);
+        Error Init(Instance &aInstance, const TxtResolver &aResolver);
+        void  PrepareTxtQuestion(TxMessage &aQuery);
+        void  DiscoverCompressOffsets(void);
+        void  ProcessExpiredRecords(TimeMilli aNow);
+        void  ReportResultTo(ResultCallback &aCallback) const;
+        void  ConvertTo(TxtResult &aResult) const;
+
+        TxtCache  *mNext;
+        Heap::Data mTxtData;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class AddrCache : public CacheEntry
+    {
+        // Base class for `Ip6AddrCache` and `Ip4AddrCache`, tracking common info
+        // shared between the two.
+
+        friend class CacheEntry;
+
+    public:
+        bool  Matches(const Name &aFullName) const;
+        bool  Matches(const char *aName) const;
+        bool  Matches(const AddressResolver &aResolver) const;
+        bool  Matches(const ExpireChecker &aExpireChecker) const;
+        Error Add(const AddressResolver &aResolver);
+        void  Remove(const AddressResolver &aResolver);
+        void  CommitNewResponseEntries(void);
+
+    protected:
+        struct AddrEntry : public LinkedListEntry<AddrEntry>, public Heap::Allocatable<AddrEntry>
+        {
+            explicit AddrEntry(const Ip6::Address &aAddress);
+            bool     Matches(const Ip6::Address &aAddress) const { return (mAddress == aAddress); }
+            bool     Matches(const ExpireChecker &aExpireChecker) const;
+            bool     Matches(EmptyChecker aChecker) const;
+            uint32_t GetTtl(void) const { return mRecord.GetTtl(); }
+
+            AddrEntry      *mNext;
+            Ip6::Address    mAddress;
+            CacheRecordInfo mRecord;
+        };
+
+        // Called by base class `CacheEntry`
+        void PrepareQueryQuestion(TxMessage &aQuery, uint16_t aRrType);
+        void UpdateRecordStateAfterQuery(TimeMilli aNow);
+        void DetermineRecordFireTime(void);
+        void ProcessExpiredRecords(TimeMilli aNow);
+        void ReportResultsTo(ResultCallback &aCallback) const;
+        bool ShouldStartInitialQueries(void) const;
+
+        Error Init(Instance &aInstance, Type aType, const char *aHostName);
+        Error Init(Instance &aInstance, Type aType, const AddressResolver &aResolver);
+        void  AppendNameTo(TxMessage &aTxMessage, Section aSection);
+        void  ConstructResult(AddressResult &aResult, Heap::Array<AddressAndTtl> &aAddrArray) const;
+        void  AddNewResponseAddress(const Ip6::Address &aAddress, uint32_t aTtl, bool aCacheFlush);
+
+        AddrCache            *mNext;
+        Heap::String          mName;
+        OwningList<AddrEntry> mCommittedEntries;
+        OwningList<AddrEntry> mNewEntries;
+        bool                  mShouldFlush;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class Ip6AddrCache : public AddrCache, public LinkedListEntry<Ip6AddrCache>, public Heap::Allocatable<Ip6AddrCache>
+    {
+        friend class CacheEntry;
+        friend class LinkedListEntry<Ip6AddrCache>;
+        friend class Heap::Allocatable<Ip6AddrCache>;
+
+    public:
+        void ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset);
+
+    private:
+        Error Init(Instance &aInstance, const char *aHostName);
+        Error Init(Instance &aInstance, const AddressResolver &aResolver);
+        void  PrepareAaaaQuestion(TxMessage &aQuery);
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class Ip4AddrCache : public AddrCache, public LinkedListEntry<Ip4AddrCache>, public Heap::Allocatable<Ip4AddrCache>
+    {
+        friend class CacheEntry;
+        friend class LinkedListEntry<Ip4AddrCache>;
+        friend class Heap::Allocatable<Ip4AddrCache>;
+
+    public:
+        void ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset);
+
+    private:
+        Error Init(Instance &aInstance, const char *aHostName);
+        Error Init(Instance &aInstance, const AddressResolver &aResolver);
+        void  PrepareAQuestion(TxMessage &aQuery);
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    class EntryIterator : public Iterator, public InstanceLocator, public Heap::Allocatable<EntryIterator>
+    {
+        friend class Heap::Allocatable<EntryIterator>;
+        friend class ServiceEntry;
+
+    public:
+        Error GetNextHost(Host &aHost, EntryState &aState);
+        Error GetNextService(Service &aService, EntryState &aState);
+        Error GetNextKey(Key &aKey, EntryState &aState);
+
+    private:
+        static constexpr uint16_t kArrayCapacityIncrement = 32;
+
+        enum Type : uint8_t
+        {
+            kUnspecified,
+            kHost,
+            kService,
+            kHostKey,
+            kServiceKey,
+        };
+
+        explicit EntryIterator(Instance &aInstance);
+
+        Type mType;
+
+        union
+        {
+            const HostEntry    *mHostEntry;
+            const ServiceEntry *mServiceEntry;
+        };
+
+        Heap::Array<const char *, kArrayCapacityIncrement> mSubTypeArray;
+    };
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    template <typename EntryType> OwningList<EntryType> &GetEntryList(void);
+    template <typename EntryType, typename ItemInfo>
+    Error Register(const ItemInfo &aItemInfo, RequestId aRequestId, RegisterCallback aCallback);
+    template <typename EntryType, typename ItemInfo> Error Unregister(const ItemInfo &aItemInfo);
+
+    template <typename CacheType> OwningList<CacheType> &GetCacheList(void);
+    template <typename CacheType, typename BrowserResolverType>
+    Error Start(const BrowserResolverType &aBrowserOrResolver);
+    template <typename CacheType, typename BrowserResolverType>
+    Error Stop(const BrowserResolverType &aBrowserOrResolver);
+
+    void      InvokeConflictCallback(const char *aName, const char *aServiceType);
+    void      HandleMessage(Message &aMessage, bool aIsUnicast, const AddressInfo &aSenderAddress);
+    void      AddPassiveSrvTxtCache(const char *aServiceInstance, const char *aServiceType);
+    void      AddPassiveIp6AddrCache(const char *aHostName);
+    TimeMilli RandomizeFirstProbeTxTime(void);
+    TimeMilli RandomizeInitialQueryTxTime(void);
+    void      RemoveEmptyEntries(void);
+    void      HandleEntryTimer(void);
+    void      HandleEntryTask(void);
+    void      HandleCacheTimer(void);
+    void      HandleCacheTask(void);
+
+    static bool     IsKeyForService(const Key &aKey) { return aKey.mServiceType != nullptr; }
+    static uint32_t DetermineTtl(uint32_t aTtl, uint32_t aDefaultTtl);
+    static bool     NameMatch(const Heap::String &aHeapString, const char *aName);
+    static bool     NameMatch(const Heap::String &aFirst, const Heap::String &aSecond);
+    static void     UpdateCacheFlushFlagIn(ResourceRecord &aResourceRecord, Section aSection);
+    static void     UpdateRecordLengthInMessage(ResourceRecord &aRecord, Message &aMessage, uint16_t aOffset);
+    static void     UpdateCompressOffset(uint16_t &aOffset, uint16_t aNewOffse);
+    static bool     QuestionMatches(uint16_t aQuestionRrType, uint16_t aRrType);
+    static bool     RrClassIsInternetOrAny(uint16_t aRrClass);
+
+    using EntryTimer = TimerMilliIn<Core, &Core::HandleEntryTimer>;
+    using CacheTimer = TimerMilliIn<Core, &Core::HandleCacheTimer>;
+    using EntryTask  = TaskletIn<Core, &Core::HandleEntryTask>;
+    using CacheTask  = TaskletIn<Core, &Core::HandleCacheTask>;
+
+    static const char kLocalDomain[];         // "local."
+    static const char kUdpServiceLabel[];     // "_udp"
+    static const char kTcpServiceLabel[];     // "_tcp"
+    static const char kSubServiceLabel[];     // "_sub"
+    static const char kServicesDnssdLabels[]; // "_services._dns-sd._udp"
+
+    bool                     mIsEnabled;
+    bool                     mIsQuestionUnicastAllowed;
+    uint16_t                 mMaxMessageSize;
+    uint32_t                 mInfraIfIndex;
+    OwningList<HostEntry>    mHostEntries;
+    OwningList<ServiceEntry> mServiceEntries;
+    OwningList<ServiceType>  mServiceTypes;
+    MultiPacketRxMessages    mMultiPacketRxMessages;
+    TimeMilli                mNextProbeTxTime;
+    EntryTimer               mEntryTimer;
+    EntryTask                mEntryTask;
+    TxMessageHistory         mTxMessageHistory;
+    ConflictCallback         mConflictCallback;
+
+    OwningList<BrowseCache>  mBrowseCacheList;
+    OwningList<SrvCache>     mSrvCacheList;
+    OwningList<TxtCache>     mTxtCacheList;
+    OwningList<Ip6AddrCache> mIp6AddrCacheList;
+    OwningList<Ip4AddrCache> mIp4AddrCacheList;
+    TimeMilli                mNextQueryTxTime;
+    CacheTimer               mCacheTimer;
+    CacheTask                mCacheTask;
+};
+
+// Specializations of `Core::GetEntryList()` for `HostEntry` and `ServiceEntry`:
+
+template <> inline OwningList<Core::HostEntry> &Core::GetEntryList<Core::HostEntry>(void) { return mHostEntries; }
+
+template <> inline OwningList<Core::ServiceEntry> &Core::GetEntryList<Core::ServiceEntry>(void)
+{
+    return mServiceEntries;
+}
+
+// Specializations of `Core::GetCacheList()`:
+
+template <> inline OwningList<Core::BrowseCache> &Core::GetCacheList<Core::BrowseCache>(void)
+{
+    return mBrowseCacheList;
+}
+
+template <> inline OwningList<Core::SrvCache> &Core::GetCacheList<Core::SrvCache>(void) { return mSrvCacheList; }
+
+template <> inline OwningList<Core::TxtCache> &Core::GetCacheList<Core::TxtCache>(void) { return mTxtCacheList; }
+
+template <> inline OwningList<Core::Ip6AddrCache> &Core::GetCacheList<Core::Ip6AddrCache>(void)
+{
+    return mIp6AddrCacheList;
+}
+
+template <> inline OwningList<Core::Ip4AddrCache> &Core::GetCacheList<Core::Ip4AddrCache>(void)
+{
+    return mIp4AddrCacheList;
+}
+
+} // namespace Multicast
+} // namespace Dns
+
+DefineCoreType(otPlatMdnsAddressInfo, Dns::Multicast::Core::AddressInfo);
+
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+#endif // MULTICAST_DNS_HPP_
diff --git a/src/core/net/nd6.cpp b/src/core/net/nd6.cpp
index e66fb04..d73aea1 100644
--- a/src/core/net/nd6.cpp
+++ b/src/core/net/nd6.cpp
@@ -36,6 +36,7 @@
 
 #include "common/as_core_type.hpp"
 #include "common/code_utils.hpp"
+#include "instance/instance.hpp"
 
 namespace ot {
 namespace Ip6 {
@@ -181,9 +182,9 @@
 }
 
 //----------------------------------------------------------------------------------------------------------------------
-// RouterAdverMessage::Header
+// RouterAdver::Header
 
-void RouterAdvertMessage::Header::SetToDefault(void)
+void RouterAdvert::Header::SetToDefault(void)
 {
     OT_UNUSED_VARIABLE(mCode);
     OT_UNUSED_VARIABLE(mCurHopLimit);
@@ -194,21 +195,21 @@
     mType = Icmp::Header::kTypeRouterAdvert;
 }
 
-RoutePreference RouterAdvertMessage::Header::GetDefaultRouterPreference(void) const
+RoutePreference RouterAdvert::Header::GetDefaultRouterPreference(void) const
 {
     return NetworkData::RoutePreferenceFromValue((mFlags & kPreferenceMask) >> kPreferenceOffset);
 }
 
-void RouterAdvertMessage::Header::SetDefaultRouterPreference(RoutePreference aPreference)
+void RouterAdvert::Header::SetDefaultRouterPreference(RoutePreference aPreference)
 {
     mFlags &= ~kPreferenceMask;
     mFlags |= (NetworkData::RoutePreferenceToValue(aPreference) << kPreferenceOffset) & kPreferenceMask;
 }
 
 //----------------------------------------------------------------------------------------------------------------------
-// RouterAdverMessage
+// RouterAdver::TxMessage
 
-Option *RouterAdvertMessage::AppendOption(uint16_t aOptionSize)
+Option *RouterAdvert::TxMessage::AppendOption(uint16_t aOptionSize)
 {
     // This method appends an option with a given size to the RA
     // message by reserving space in the data buffer if there is
@@ -217,21 +218,39 @@
     // initialized and populated by the caller.
 
     Option  *option    = nullptr;
-    uint32_t newLength = mData.GetLength();
+    uint16_t oldLength = mArray.GetLength();
 
-    newLength += aOptionSize;
-    VerifyOrExit(newLength <= mMaxLength);
-
-    option = reinterpret_cast<Option *>(AsNonConst(GetDataEnd()));
-    mData.SetLength(static_cast<uint16_t>(newLength));
+    SuccessOrExit(AppendBytes(nullptr, aOptionSize));
+    option = reinterpret_cast<Option *>(&mArray[oldLength]);
 
 exit:
     return option;
 }
 
-Error RouterAdvertMessage::AppendPrefixInfoOption(const Prefix &aPrefix,
-                                                  uint32_t      aValidLifetime,
-                                                  uint32_t      aPreferredLifetime)
+Error RouterAdvert::TxMessage::AppendBytes(const uint8_t *aBytes, uint16_t aLength)
+{
+    Error error = kErrorNone;
+
+    for (; aLength > 0; aLength--)
+    {
+        uint8_t byte;
+
+        byte = (aBytes == nullptr) ? 0 : *aBytes++;
+        SuccessOrExit(error = mArray.PushBack(byte));
+    }
+
+exit:
+    return error;
+}
+
+Error RouterAdvert::TxMessage::AppendHeader(const Header &aHeader)
+{
+    return AppendBytes(reinterpret_cast<const uint8_t *>(&aHeader), sizeof(Header));
+}
+
+Error RouterAdvert::TxMessage::AppendPrefixInfoOption(const Prefix &aPrefix,
+                                                      uint32_t      aValidLifetime,
+                                                      uint32_t      aPreferredLifetime)
 {
     Error             error = kErrorNone;
     PrefixInfoOption *pio;
@@ -250,9 +269,9 @@
     return error;
 }
 
-Error RouterAdvertMessage::AppendRouteInfoOption(const Prefix   &aPrefix,
-                                                 uint32_t        aRouteLifetime,
-                                                 RoutePreference aPreference)
+Error RouterAdvert::TxMessage::AppendRouteInfoOption(const Prefix   &aPrefix,
+                                                     uint32_t        aRouteLifetime,
+                                                     RoutePreference aPreference)
 {
     Error            error = kErrorNone;
     RouteInfoOption *rio;
@@ -269,7 +288,7 @@
     return error;
 }
 
-Error RouterAdvertMessage::AppendFlagsExtensionOption(bool aStubRouterFlag)
+Error RouterAdvert::TxMessage::AppendFlagsExtensionOption(bool aStubRouterFlag)
 {
     Error             error = kErrorNone;
     RaFlagsExtOption *flagsOption;
diff --git a/src/core/net/nd6.hpp b/src/core/net/nd6.hpp
index f4a6d5a..4178f18 100644
--- a/src/core/net/nd6.hpp
+++ b/src/core/net/nd6.hpp
@@ -47,6 +47,7 @@
 #include "common/const_cast.hpp"
 #include "common/encoding.hpp"
 #include "common/equatable.hpp"
+#include "common/heap_array.hpp"
 #include "net/icmp6.hpp"
 #include "net/ip6.hpp"
 #include "thread/network_data_types.hpp"
@@ -67,7 +68,7 @@
 OT_TOOL_PACKED_BEGIN
 class Option
 {
-    friend class RouterAdvertMessage;
+    friend class RouterAdvert;
 
 public:
     enum Type : uint8_t
@@ -525,14 +526,14 @@
 static_assert(sizeof(RaFlagsExtOption) == 8, "invalid RaFlagsExtOption structure");
 
 /**
- * Represents a Router Advertisement message.
+ * Defines Router Advertisement components.
  *
  */
-class RouterAdvertMessage
+class RouterAdvert
 {
 public:
     /**
-     * Implements the RA message header.
+     * Represent an RA message header.
      *
      * See section 2.2 of RFC 4191 [https://datatracker.ietf.org/doc/html/rfc4191]
      *
@@ -673,135 +674,170 @@
     typedef Data<kWithUint16Length> Icmp6Packet; ///< A data buffer containing an ICMPv6 packet.
 
     /**
-     * Initializes the RA message from a received packet data buffer.
-     *
-     * @param[in] aPacket   A received packet data.
+     * Represents a received RA message.
      *
      */
-    explicit RouterAdvertMessage(const Icmp6Packet &aPacket)
-        : mData(aPacket)
-        , mMaxLength(0)
+    class RxMessage
     {
-    }
+    public:
+        /**
+         * Initializes the RA message from a received packet data buffer.
+         *
+         * @param[in] aPacket   A received packet data.
+         *
+         */
+        explicit RxMessage(const Icmp6Packet &aPacket)
+            : mData(aPacket)
+        {
+        }
+
+        /**
+         * Gets the RA message as an `Icmp6Packet`.
+         *
+         * @returns The RA message as an `Icmp6Packet`.
+         *
+         */
+        const Icmp6Packet &GetAsPacket(void) const { return mData; }
+
+        /**
+         * Indicates whether or not the received RA message is valid.
+         *
+         * @retval TRUE   If the RA message is valid.
+         * @retval FALSE  If the RA message is not valid.
+         *
+         */
+        bool IsValid(void) const
+        {
+            return (mData.GetBytes() != nullptr) && (mData.GetLength() >= sizeof(Header)) &&
+                   (GetHeader().GetType() == Icmp::Header::kTypeRouterAdvert);
+        }
+
+        /**
+         * Gets the RA message's header.
+         *
+         * @returns The RA message's header.
+         *
+         */
+        const Header &GetHeader(void) const { return *reinterpret_cast<const Header *>(mData.GetBytes()); }
+
+        /**
+         * Indicates whether or not the received RA message contains any options.
+         *
+         * @retval TRUE   If the RA message contains at least one option.
+         * @retval FALSE  If the RA message contains no options.
+         *
+         */
+        bool ContainsAnyOptions(void) const { return (mData.GetLength() > sizeof(Header)); }
+
+        // The following methods are intended to support range-based `for`
+        // loop iteration over `Option`s in the RA message.
+
+        Option::Iterator begin(void) const { return Option::Iterator(GetOptionStart(), GetDataEnd()); }
+        Option::Iterator end(void) const { return Option::Iterator(); }
+
+    private:
+        const uint8_t *GetOptionStart(void) const { return (mData.GetBytes() + sizeof(Header)); }
+        const uint8_t *GetDataEnd(void) const { return mData.GetBytes() + mData.GetLength(); }
+
+        Data<kWithUint16Length> mData;
+    };
 
     /**
-     * This template constructor initializes the RA message with a given header using a given buffer to store the RA
-     * message.
-     *
-     * @tparam kBufferSize   The size of the buffer used to store the RA message.
-     *
-     * @param[in] aHeader    The RA message header.
-     * @param[in] aBuffer    The data buffer to store the RA message in.
+     * Represents an RA message to be sent.
      *
      */
-    template <uint16_t kBufferSize>
-    RouterAdvertMessage(const Header &aHeader, uint8_t (&aBuffer)[kBufferSize])
-        : mMaxLength(kBufferSize)
+    class TxMessage
     {
-        static_assert(kBufferSize >= sizeof(Header), "Buffer for RA msg is too small");
+    public:
+        /**
+         * Gets the prepared RA message as an `Icmp6Packet`.
+         *
+         * @param[out] aPacket   A reference to an `Icmp6Packet`.
+         *
+         */
+        void GetAsPacket(Icmp6Packet &aPacket) const { aPacket.Init(mArray.AsCArray(), mArray.GetLength()); }
 
-        memcpy(aBuffer, &aHeader, sizeof(Header));
-        mData.Init(aBuffer, sizeof(Header));
-    }
+        /**
+         * Appends the RA header.
+         *
+         * @param[in] aHeader  The RA header.
+         *
+         * @retval kErrorNone    Header is written successfully.
+         * @retval kErrorNoBufs  Insufficient available buffers to grow the message.
+         *
+         */
+        Error AppendHeader(const Header &aHeader);
 
-    /**
-     * Gets the RA message as an `Icmp6Packet`.
-     *
-     * @returns The RA message as an `Icmp6Packet`.
-     *
-     */
-    const Icmp6Packet &GetAsPacket(void) const { return mData; }
+        /**
+         * Appends a Prefix Info Option to the RA message.
+         *
+         * The appended Prefix Info Option will have both on-link (L) and autonomous address-configuration (A) flags
+         * set.
+         *
+         * @param[in] aPrefix             The prefix.
+         * @param[in] aValidLifetime      The valid lifetime in seconds.
+         * @param[in] aPreferredLifetime  The preferred lifetime in seconds.
+         *
+         * @retval kErrorNone    Option is appended successfully.
+         * @retval kErrorNoBufs  Insufficient available buffers to grow the message.
+         *
+         */
+        Error AppendPrefixInfoOption(const Prefix &aPrefix, uint32_t aValidLifetime, uint32_t aPreferredLifetime);
 
-    /**
-     * Indicates whether or not the RA message is valid.
-     *
-     * @retval TRUE   If the RA message is valid.
-     * @retval FALSE  If the RA message is not valid.
-     *
-     */
-    bool IsValid(void) const
-    {
-        return (mData.GetBytes() != nullptr) && (mData.GetLength() >= sizeof(Header)) &&
-               (GetHeader().GetType() == Icmp::Header::kTypeRouterAdvert);
-    }
+        /**
+         * Appends a Route Info Option to the RA message.
+         *
+         * @param[in] aPrefix             The prefix.
+         * @param[in] aRouteLifetime      The route lifetime in seconds.
+         * @param[in] aPreference         The route preference.
+         *
+         * @retval kErrorNone    Option is appended successfully.
+         * @retval kErrorNoBufs  Insufficient available buffers to grow the message.
+         *
+         */
+        Error AppendRouteInfoOption(const Prefix &aPrefix, uint32_t aRouteLifetime, RoutePreference aPreference);
 
-    /**
-     * Gets the RA message's header.
-     *
-     * @returns The RA message's header.
-     *
-     */
-    const Header &GetHeader(void) const { return *reinterpret_cast<const Header *>(mData.GetBytes()); }
+        /**
+         * Appends a Flags Extension Option to the RA message.
+         *
+         * @param[in] aStubRouterFlag    The stub router flag.
+         *
+         * @retval kErrorNone    Option is appended successfully.
+         * @retval kErrorNoBufs  Insufficient available buffers to grow the message.
+         *
+         */
+        Error AppendFlagsExtensionOption(bool aStubRouterFlag);
 
-    /**
-     * Gets the RA message's header.
-     *
-     * @returns The RA message's header.
-     *
-     */
-    Header &GetHeader(void) { return *reinterpret_cast<Header *>(AsNonConst(mData.GetBytes())); }
+        /**
+         * Appends bytes from a given buffer to the RA message.
+         *
+         * @param[in] aBytes     A pointer to the buffer containing the bytes to append.
+         * @param[in] aLength    The buffer length.
+         *
+         * @retval kErrorNone    Bytes are appended successfully.
+         * @retval kErrorNoBufs  Insufficient available buffers to grow the message.
+         *
+         */
+        Error AppendBytes(const uint8_t *aBytes, uint16_t aLength);
 
-    /**
-     * Appends a Prefix Info Option to the RA message.
-     *
-     * The appended Prefix Info Option will have both on-link (L) and autonomous address-configuration (A) flags set.
-     *
-     * @param[in] aPrefix             The prefix.
-     * @param[in] aValidLifetime      The valid lifetime in seconds.
-     * @param[in] aPreferredLifetime  The preferred lifetime in seconds.
-     *
-     * @retval kErrorNone    Option is appended successfully.
-     * @retval kErrorNoBufs  No more space in the buffer to append the option.
-     *
-     */
-    Error AppendPrefixInfoOption(const Prefix &aPrefix, uint32_t aValidLifetime, uint32_t aPreferredLifetime);
+        /**
+         * Indicates whether or not the received RA message contains any options.
+         *
+         * @retval TRUE   If the RA message contains at least one option.
+         * @retval FALSE  If the RA message contains no options.
+         *
+         */
+        bool ContainsAnyOptions(void) const { return (mArray.GetLength() > sizeof(Header)); }
 
-    /**
-     * Appends a Route Info Option to the RA message.
-     *
-     * @param[in] aPrefix             The prefix.
-     * @param[in] aRouteLifetime      The route lifetime in seconds.
-     * @param[in] aPreference         The route preference.
-     *
-     * @retval kErrorNone    Option is appended successfully.
-     * @retval kErrorNoBufs  No more space in the buffer to append the option.
-     *
-     */
-    Error AppendRouteInfoOption(const Prefix &aPrefix, uint32_t aRouteLifetime, RoutePreference aPreference);
+    private:
+        static constexpr uint16_t kCapacityIncrement = 256;
 
-    /**
-     * Appends a Flags Extension Option to the RA message.
-     *
-     * @param[in] aStubRouterFlag    The stub router flag.
-     *
-     * @retval kErrorNone    Option is appended successfully.
-     * @retval kErrorNoBufs  No more space in the buffer to append the option.
-     *
-     */
-    Error AppendFlagsExtensionOption(bool aStubRouterFlag);
+        Option *AppendOption(uint16_t aOptionSize);
 
-    /**
-     * Indicates whether or not the RA message contains any options.
-     *
-     * @retval TRUE   If the RA message contains at least one option.
-     * @retval FALSE  If the RA message contains no options.
-     *
-     */
-    bool ContainsAnyOptions(void) const { return (mData.GetLength() > sizeof(Header)); }
+        Heap::Array<uint8_t, kCapacityIncrement> mArray;
+    };
 
-    // The following methods are intended to support range-based `for`
-    // loop iteration over `Option`s in the RA message.
-
-    Option::Iterator begin(void) const { return Option::Iterator(GetOptionStart(), GetDataEnd()); }
-    Option::Iterator end(void) const { return Option::Iterator(); }
-
-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);
-
-    Data<kWithUint16Length> mData;
-    uint16_t                mMaxLength;
+    RouterAdvert(void) = delete;
 };
 
 /**
diff --git a/src/core/net/sntp_client.cpp b/src/core/net/sntp_client.cpp
index 5897a5f..7718b02 100644
--- a/src/core/net/sntp_client.cpp
+++ b/src/core/net/sntp_client.cpp
@@ -193,7 +193,7 @@
     if (error != kErrorNone)
     {
         FreeMessage(messageCopy);
-        LogWarn("Failed to send SNTP request: %s", ErrorToString(error));
+        LogWarnOnError(error, "send SNTP request");
     }
 }
 
diff --git a/src/core/net/socket.hpp b/src/core/net/socket.hpp
index fb4e928..e1ca9ac 100644
--- a/src/core/net/socket.hpp
+++ b/src/core/net/socket.hpp
@@ -181,30 +181,6 @@
     void SetMulticastLoop(bool aMulticastLoop) { mMulticastLoop = aMulticastLoop; }
 
     /**
-     * Returns a pointer to the link-specific information object.
-     *
-     * @returns A pointer to the link-specific information object.
-     *
-     */
-    const void *GetLinkInfo(void) const { return mLinkInfo; }
-
-    /**
-     * Sets the pointer to the link-specific information object.
-     *
-     * @param[in]  aLinkInfo  A pointer to the link-specific information object.
-     *
-     */
-    void SetLinkInfo(const void *aLinkInfo) { mLinkInfo = aLinkInfo; }
-
-    /**
-     * Returns a pointer to the link-specific information as a `ThreadLinkInfo`.
-     *
-     * @returns A pointer to to the link-specific information object as `ThreadLinkInfo`.
-     *
-     */
-    const ThreadLinkInfo *GetThreadLinkInfo(void) const { return reinterpret_cast<const ThreadLinkInfo *>(mLinkInfo); }
-
-    /**
      * Gets the ECN status.
      *
      * @returns The ECN status, as represented in the IP header.
diff --git a/src/core/net/srp_advertising_proxy.hpp b/src/core/net/srp_advertising_proxy.hpp
index 19fa82d..19ab4b4 100644
--- a/src/core/net/srp_advertising_proxy.hpp
+++ b/src/core/net/srp_advertising_proxy.hpp
@@ -38,8 +38,8 @@
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE
 
-#if !OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE
-#error "OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE requires OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE"
+#if !OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE && !OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+#error "OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE requires PLATFORM_DNSSD_ENABLE or MULTICAST_DNS_ENABLE"
 #endif
 
 #if !OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
diff --git a/src/core/net/srp_server.cpp b/src/core/net/srp_server.cpp
index 3d6550d..635cbed 100644
--- a/src/core/net/srp_server.cpp
+++ b/src/core/net/srp_server.cpp
@@ -91,7 +91,7 @@
     , mOutstandingUpdatesTimer(aInstance)
     , mCompletedUpdateTask(aInstance)
     , mServiceUpdateId(Random::NonCrypto::GetUint32())
-    , mPort(kUdpPortMin)
+    , mPort(kUninitializedPort)
     , mState(kStateDisabled)
     , mAddressMode(kDefaultAddressMode)
     , mAnycastSequenceNumber(0)
@@ -595,7 +595,7 @@
     }
 }
 
-void Server::SelectPort(void)
+void Server::InitPort(void)
 {
     mPort = kUdpPortMin;
 
@@ -605,24 +605,35 @@
 
         if (Get<Settings>().Read(info) == kErrorNone)
         {
-            mPort = info.GetPort() + 1;
-            if (mPort < kUdpPortMin || mPort > kUdpPortMax)
-            {
-                mPort = kUdpPortMin;
-            }
+            mPort = info.GetPort();
         }
     }
 #endif
+}
+
+void Server::SelectPort(void)
+{
+    if (mPort == kUninitializedPort)
+    {
+        InitPort();
+    }
+    ++mPort;
+    if (mPort < kUdpPortMin || mPort > kUdpPortMax)
+    {
+        mPort = kUdpPortMin;
+    }
 
     LogInfo("Selected port %u", mPort);
 }
 
 void Server::Start(void)
 {
+    Error error = kErrorNone;
+
     VerifyOrExit(mState == kStateStopped);
 
     mState = kStateRunning;
-    PrepareSocket();
+    SuccessOrExit(error = PrepareSocket());
     LogInfo("Start listening on port %u", mPort);
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE
@@ -630,10 +641,15 @@
 #endif
 
 exit:
-    return;
+    // Re-enable server to select a new port.
+    if (error != kErrorNone)
+    {
+        Disable();
+        Enable();
+    }
 }
 
-void Server::PrepareSocket(void)
+Error Server::PrepareSocket(void)
 {
     Error error = kErrorNone;
 
@@ -658,9 +674,12 @@
 exit:
     if (error != kErrorNone)
     {
-        LogCrit("Failed to prepare socket: %s", ErrorToString(error));
+        LogWarnOnError(error, "prepare socket");
+        IgnoreError(mSocket.Close());
         Stop();
     }
+
+    return error;
 }
 
 Ip6::Udp::Socket &Server::GetSocket(void)
@@ -689,7 +708,7 @@
 
     if (mState == kStateRunning)
     {
-        PrepareSocket();
+        IgnoreError(PrepareSocket());
     }
 }
 
@@ -811,6 +830,13 @@
     // Parse lease time and validate signature.
     SuccessOrExit(error = ProcessAdditionalSection(host, aMessage, aMetadata));
 
+#if OPENTHREAD_FTD
+    if (aMetadata.IsDirectRxFromClient())
+    {
+        UpdateAddrResolverCacheTable(*aMetadata.mMessageInfo, *host);
+    }
+#endif
+
     HandleUpdate(*host, aMetadata);
 
 exit:
@@ -846,11 +872,7 @@
     aMetadata.mOffset = offset;
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to process DNS Zone section: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "process DNS Zone section");
     return error;
 }
 
@@ -876,11 +898,7 @@
     VerifyOrExit(!HasNameConflictsWith(aHost), error = kErrorDuplicated);
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to process DNS Update section: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "Process DNS Update section");
     return error;
 }
 
@@ -969,11 +987,7 @@
     // the host is being removed or registered.
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to process Host Description instructions: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "process Host Description instructions");
     return error;
 }
 
@@ -1092,11 +1106,7 @@
     }
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to process Service Discovery instructions: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "process Service Discovery instructions");
     return error;
 }
 
@@ -1195,11 +1205,7 @@
     aMetadata.mOffset = offset;
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to process Service Description instructions: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "process Service Description instructions");
     return error;
 }
 
@@ -1288,11 +1294,7 @@
     aMetadata.mOffset = offset;
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to process DNS Additional section: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "process DNS Additional section");
     return error;
 }
 
@@ -1339,11 +1341,7 @@
     error = aKey.Verify(hash, signature);
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to verify message signature: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "verify message signature");
     FreeMessage(signerNameMessage);
     return error;
 }
@@ -1503,11 +1501,8 @@
     UpdateResponseCounters(aResponseCode);
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to send response: %s", ErrorToString(error));
-        FreeMessage(response);
-    }
+    LogWarnOnError(error, "send response");
+    FreeMessageOnError(response, error);
 }
 
 void Server::SendResponse(const Dns::UpdateHeader &aHeader,
@@ -1562,11 +1557,8 @@
     UpdateResponseCounters(Dns::UpdateHeader::kResponseSuccess);
 
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to send response: %s", ErrorToString(error));
-        FreeMessage(response);
-    }
+    LogWarnOnError(error, "send response");
+    FreeMessageOnError(response, error);
 }
 
 void Server::HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
@@ -1578,10 +1570,8 @@
 {
     Error error = ProcessMessage(aMessage, aMessageInfo);
 
-    if (error != kErrorNone)
-    {
-        LogInfo("Failed to handle DNS message: %s", ErrorToString(error));
-    }
+    LogWarnOnError(error, "handle DNS message");
+    OT_UNUSED_VARIABLE(error);
 }
 
 Error Server::ProcessMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
@@ -1789,6 +1779,41 @@
     }
 }
 
+#if OPENTHREAD_FTD
+void Server::UpdateAddrResolverCacheTable(const Ip6::MessageInfo &aMessageInfo, const Host &aHost)
+{
+    // If message is from a client on mesh, we add all registered
+    // addresses as snooped entries in the address resolver cache
+    // table. We associate the registered addresses with the same
+    // RLOC16 (if any) as the received message's peer IPv6 address.
+
+    uint16_t rloc16;
+
+    VerifyOrExit(aHost.GetLease() != 0);
+    VerifyOrExit(aHost.GetTtl() > 0);
+
+    // If the `LookUp()` call succeeds, the cache entry will be marked
+    // as "cached and in-use". We can mark it as "in-use" early since
+    // the entry will be needed and used soon when sending the SRP
+    // response. This also prevents a snooped cache entry (added for
+    // `GetPeerAddr()` due to rx of the SRP update message) from
+    // being overwritten by `UpdateSnoopedCacheEntry()` calls when
+    // there are limited snoop entries available.
+
+    rloc16 = Get<AddressResolver>().LookUp(aMessageInfo.GetPeerAddr());
+
+    VerifyOrExit(rloc16 != Mac::kShortAddrInvalid);
+
+    for (const Ip6::Address &address : aHost.mAddresses)
+    {
+        Get<AddressResolver>().UpdateSnoopedCacheEntry(address, rloc16, Get<Mle::Mle>().GetRloc16());
+    }
+
+exit:
+    return;
+}
+#endif
+
 //---------------------------------------------------------------------------------------------------------------------
 // Server::Service
 
diff --git a/src/core/net/srp_server.hpp b/src/core/net/srp_server.hpp
index 6b5556d..ebe67d8 100644
--- a/src/core/net/srp_server.hpp
+++ b/src/core/net/srp_server.hpp
@@ -912,6 +912,7 @@
     static constexpr AddressMode kDefaultAddressMode =
         static_cast<AddressMode>(OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDRESS_MODE);
 
+    static constexpr uint16_t kUninitializedPort      = 0;
     static constexpr uint16_t kAnycastAddressModePort = 53;
 
     // Metadata for a received SRP Update message.
@@ -971,8 +972,9 @@
     void              Disable(void);
     void              Start(void);
     void              Stop(void);
+    void              InitPort(void);
     void              SelectPort(void);
-    void              PrepareSocket(void);
+    Error             PrepareSocket(void);
     Ip6::Udp::Socket &GetSocket(void);
     LinkedList<Host> &GetHosts(void) { return mHosts; }
 
@@ -1043,6 +1045,7 @@
     static const char    *AddressModeToString(AddressMode aMode);
 
     void UpdateResponseCounters(Dns::Header::Response aResponseCode);
+    void UpdateAddrResolverCacheTable(const Ip6::MessageInfo &aMessageInfo, const Host &aHost);
 
     using LeaseTimer           = TimerMilliIn<Server, &Server::HandleLeaseTimer>;
     using UpdateTimer          = TimerMilliIn<Server, &Server::HandleOutstandingUpdatesTimer>;
diff --git a/src/core/net/udp6.cpp b/src/core/net/udp6.cpp
index 71d0ea8..bb24e50 100644
--- a/src/core/net/udp6.cpp
+++ b/src/core/net/udp6.cpp
@@ -552,6 +552,12 @@
 #if OPENTHREAD_FTD
             && aPort != Get<MeshCoP::JoinerRouter>().GetJoinerUdpPort()
 #endif
+#if OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE
+            && aPort != Dhcp6::kDhcpServerPort
+#endif
+#if OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE
+            && aPort != Dhcp6::kDhcpClientPort
+#endif
     );
 }
 
diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h
index 498eb62..7465915 100644
--- a/src/core/openthread-core-config.h
+++ b/src/core/openthread-core-config.h
@@ -41,7 +41,9 @@
 #define OT_THREAD_VERSION_1_1 2
 #define OT_THREAD_VERSION_1_2 3
 #define OT_THREAD_VERSION_1_3 4
+// Support projects on legacy "1.3.1" version, which is now "1.4"
 #define OT_THREAD_VERSION_1_3_1 5
+#define OT_THREAD_VERSION_1_4 5
 
 #define OPENTHREAD_CORE_CONFIG_H_IN
 
@@ -96,6 +98,7 @@
 #include "config/link_raw.h"
 #include "config/logging.h"
 #include "config/mac.h"
+#include "config/mdns.h"
 #include "config/mesh_diag.h"
 #include "config/mesh_forwarder.h"
 #include "config/misc.h"
diff --git a/src/core/radio/ble_secure.cpp b/src/core/radio/ble_secure.cpp
index 5e52244..8f4a0a9 100644
--- a/src/core/radio/ble_secure.cpp
+++ b/src/core/radio/ble_secure.cpp
@@ -89,7 +89,14 @@
 Error BleSecure::TcatStart(const MeshCoP::TcatAgent::VendorInfo &aVendorInfo,
                            MeshCoP::TcatAgent::JoinCallback      aJoinHandler)
 {
-    return mTcatAgent.Start(aVendorInfo, mReceiveCallback.GetHandler(), aJoinHandler, mReceiveCallback.GetContext());
+    Error error;
+
+    VerifyOrExit(mBleState != kStopped, error = kErrorInvalidState);
+
+    error = mTcatAgent.Start(aVendorInfo, mReceiveCallback.GetHandler(), aJoinHandler, mReceiveCallback.GetContext());
+
+exit:
+    return error;
 }
 
 void BleSecure::Stop(void)
@@ -124,8 +131,14 @@
 Error BleSecure::Connect(void)
 {
     Ip6::SockAddr sockaddr;
+    Error         error;
 
-    return mTls.Connect(sockaddr);
+    VerifyOrExit(mBleState == kConnected, error = kErrorInvalidState);
+
+    error = mTls.Connect(sockaddr);
+
+exit:
+    return error;
 }
 
 void BleSecure::Disconnect(void)
@@ -137,8 +150,11 @@
 
     if (mBleState == kConnected)
     {
+        mBleState = kAdvertising;
         IgnoreReturnValue(otPlatBleGapDisconnect(&GetInstance()));
     }
+
+    mConnectCallback.InvokeIfSet(&GetInstance(), false, false);
 }
 
 void BleSecure::SetPsk(const MeshCoP::JoinerPskd &aPskd)
@@ -278,12 +294,7 @@
     mBleState = kAdvertising;
     mMtuSize  = kInitialMtuSize;
 
-    if (IsConnected())
-    {
-        Disconnect(); // Stop TLS connection
-    }
-
-    mConnectCallback.InvokeIfSet(&GetInstance(), false, false);
+    Disconnect(); // Stop TLS connection
 }
 
 Error BleSecure::HandleBleMtuUpdate(uint16_t aMtu)
diff --git a/src/core/radio/radio.hpp b/src/core/radio/radio.hpp
index 1805161..c8bb938 100644
--- a/src/core/radio/radio.hpp
+++ b/src/core/radio/radio.hpp
@@ -37,11 +37,12 @@
 #include "openthread-core-config.h"
 
 #include <openthread/radio_stats.h>
+#include <openthread/platform/crypto.h>
 #include <openthread/platform/radio.h>
 
-#include <openthread/platform/crypto.h>
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
+#include "common/numeric_limits.hpp"
 #include "common/time.hpp"
 #include "mac/mac_frame.hpp"
 
@@ -1082,9 +1083,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 NumericLimits<uint8_t>::kMax; }
 
-inline uint8_t Radio::GetCslUncertainty(void) { return UINT8_MAX; }
+inline uint8_t Radio::GetCslUncertainty(void) { return NumericLimits<uint8_t>::kMax; }
 #endif
 
 inline Mac::TxFrame &Radio::GetTransmitBuffer(void)
diff --git a/src/core/radio/radio_platform.cpp b/src/core/radio/radio_platform.cpp
index 6dcb85d..0f60012 100644
--- a/src/core/radio/radio_platform.cpp
+++ b/src/core/radio/radio_platform.cpp
@@ -261,14 +261,14 @@
 {
     OT_UNUSED_VARIABLE(aInstance);
 
-    return UINT8_MAX;
+    return NumericLimits<uint8_t>::kMax;
 }
 
 OT_TOOL_WEAK uint8_t otPlatRadioGetCslUncertainty(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
 
-    return UINT8_MAX;
+    return NumericLimits<uint8_t>::kMax;
 }
 
 OT_TOOL_WEAK otError otPlatRadioGetFemLnaGain(otInstance *aInstance, int8_t *aGain)
diff --git a/src/core/thread/address_resolver.hpp b/src/core/thread/address_resolver.hpp
index 880d436..8b5d0a4 100644
--- a/src/core/thread/address_resolver.hpp
+++ b/src/core/thread/address_resolver.hpp
@@ -199,7 +199,10 @@
     /**
      * Looks up the RLOC16 for a given EID in the address cache.
      *
-     * @param[in]   aEid                A reference to the EID.
+     * When a cache entry is successfully looked up using this method, it will be marked as "cached and in-use".
+     * Specifically, a snooped entry (`kStateSnooped`) will be marked as cached (`kStateCached`).
+     *
+     * @param[in]   aEid   A reference to the EID to lookup.
      *
      * @returns The RLOC16 mapping to @p aEid or `Mac::kShortAddrInvalid` if it is not found in the address cache.
      *
diff --git a/src/core/thread/child_table.cpp b/src/core/thread/child_table.cpp
index cd9cb65..7fb557d 100644
--- a/src/core/thread/child_table.cpp
+++ b/src/core/thread/child_table.cpp
@@ -245,6 +245,7 @@
         child->SetTimeout(childInfo.GetTimeout());
         child->SetDeviceMode(Mle::DeviceMode(childInfo.GetMode()));
         child->SetState(Neighbor::kStateRestored);
+        child->GenerateChallenge();
         child->SetLastHeard(TimerMilli::GetNow());
         child->SetVersion(childInfo.GetVersion());
         Get<IndirectSender>().SetChildUseShortAddress(*child, true);
diff --git a/src/core/thread/csl_tx_scheduler.hpp b/src/core/thread/csl_tx_scheduler.hpp
index 77eebcd..7b3d129 100644
--- a/src/core/thread/csl_tx_scheduler.hpp
+++ b/src/core/thread/csl_tx_scheduler.hpp
@@ -113,7 +113,7 @@
          * containing the CSL IE was transmitted until the next channel sample,
          * see IEEE 802.15.4-2015, section 6.12.2.
          *
-         * The Thread standard further defines the CSL phase (see Thread 1.3.1,
+         * The Thread standard further defines the CSL phase (see Thread 1.4,
          * section 3.2.6.3.4, also conforming to IEEE 802.15.4-2020, section
          * 6.12.2.1):
          *  * The "first symbol" from the definition SHALL be interpreted as the
diff --git a/src/core/thread/discover_scanner.cpp b/src/core/thread/discover_scanner.cpp
index 3a59651..9cd9978 100644
--- a/src/core/thread/discover_scanner.cpp
+++ b/src/core/thread/discover_scanner.cpp
@@ -308,8 +308,7 @@
 
 void DiscoverScanner::HandleDiscoveryResponse(Mle::RxInfo &aRxInfo) const
 {
-    Error                         error    = kErrorNone;
-    const ThreadLinkInfo         *linkInfo = aRxInfo.mMessageInfo.GetThreadLinkInfo();
+    Error                         error = kErrorNone;
     MeshCoP::Tlv                  meshcopTlv;
     MeshCoP::DiscoveryResponseTlv discoveryResponse;
     MeshCoP::NetworkNameTlv       networkName;
@@ -327,10 +326,10 @@
 
     ClearAllBytes(result);
     result.mDiscover = true;
-    result.mPanId    = linkInfo->mPanId;
-    result.mChannel  = linkInfo->mChannel;
-    result.mRssi     = linkInfo->mRss;
-    result.mLqi      = linkInfo->mLqi;
+    result.mPanId    = aRxInfo.mMessage.GetPanId();
+    result.mChannel  = aRxInfo.mMessage.GetChannel();
+    result.mRssi     = aRxInfo.mMessage.GetAverageRss();
+    result.mLqi      = aRxInfo.mMessage.GetAverageLqi();
 
     aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(AsCoreType(&result.mExtAddress));
 
diff --git a/src/core/thread/dua_manager.cpp b/src/core/thread/dua_manager.cpp
index 7d78879..9f98b07 100644
--- a/src/core/thread/dua_manager.cpp
+++ b/src/core/thread/dua_manager.cpp
@@ -158,7 +158,7 @@
     }
     else
     {
-        LogWarn("Generate DUA: %s", ErrorToString(error));
+        LogWarnOnError(error, "generate DUA");
     }
 
     return error;
@@ -548,7 +548,7 @@
         UpdateCheckDelay(kNoBufDelay);
     }
 
-    LogInfo("PerformNextRegistration: %s", ErrorToString(error));
+    LogWarnOnError(error, "perform next registration");
     FreeMessageOnError(message, error);
 }
 
diff --git a/src/core/thread/energy_scan_server.cpp b/src/core/thread/energy_scan_server.cpp
index 2585805..c79c7d8 100644
--- a/src/core/thread/energy_scan_server.cpp
+++ b/src/core/thread/energy_scan_server.cpp
@@ -198,7 +198,7 @@
 
 exit:
     FreeMessageOnError(mReportMessage, error);
-    MeshCoP::LogError("send scan results", error);
+    LogWarnOnError(error, "send scan results");
     mReportMessage = nullptr;
 }
 
diff --git a/src/core/thread/key_manager.cpp b/src/core/thread/key_manager.cpp
index 3d9337d..d38fdb8 100644
--- a/src/core/thread/key_manager.cpp
+++ b/src/core/thread/key_manager.cpp
@@ -60,6 +60,9 @@
                                                'r', 'I', 'n', 'f', 'r', 'a', 'K', 'e', 'y'};
 #endif
 
+//---------------------------------------------------------------------------------------------------------------------
+// SecurityPolicy
+
 void SecurityPolicy::SetToDefault(void)
 {
     mRotationTime = kDefaultKeyRotationTime;
@@ -163,6 +166,9 @@
     return;
 }
 
+//---------------------------------------------------------------------------------------------------------------------
+// KeyManager
+
 KeyManager::KeyManager(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mKeySequence(0)
@@ -171,7 +177,7 @@
     , mStoredMleFrameCounter(0)
     , mHoursSinceKeyRotation(0)
     , mKeySwitchGuardTime(kDefaultKeySwitchGuardTime)
-    , mKeySwitchGuardEnabled(false)
+    , mKeySwitchGuardTimer(0)
     , mKeyRotationTimer(aInstance)
     , mKekFrameCounter(0)
     , mIsPskcSet(false)
@@ -198,8 +204,8 @@
 
 void KeyManager::Start(void)
 {
-    mKeySwitchGuardEnabled = false;
-    StartKeyRotationTimer();
+    mKeySwitchGuardTimer = 0;
+    ResetKeyRotationTimer();
 }
 
 void KeyManager::Stop(void) { mKeyRotationTimer.Stop(); }
@@ -362,20 +368,13 @@
 #endif
 }
 
-void KeyManager::SetCurrentKeySequence(uint32_t aKeySequence)
+void KeyManager::SetCurrentKeySequence(uint32_t aKeySequence, KeySequenceUpdateMode aUpdateMode)
 {
     VerifyOrExit(aKeySequence != mKeySequence, Get<Notifier>().SignalIfFirst(kEventThreadKeySeqCounterChanged));
 
-    if ((aKeySequence == (mKeySequence + 1)) && mKeyRotationTimer.IsRunning())
+    if (aUpdateMode == kApplyKeySwitchGuard)
     {
-        if (mKeySwitchGuardEnabled)
-        {
-            // Check if the guard timer has expired if key rotation is requested.
-            VerifyOrExit(mHoursSinceKeyRotation >= mKeySwitchGuardTime);
-            StartKeyRotationTimer();
-        }
-
-        mKeySwitchGuardEnabled = true;
+        VerifyOrExit(mKeySwitchGuardTimer == 0);
     }
 
     mKeySequence = aKeySequence;
@@ -384,6 +383,9 @@
     SetAllMacFrameCounters(0, /* aSetIfLarger */ false);
     mMleFrameCounter = 0;
 
+    ResetKeyRotationTimer();
+    mKeySwitchGuardTimer = mKeySwitchGuardTime;
+
     Get<Notifier>().Signal(kEventThreadKeySeqCounterChanged);
 
 exit:
@@ -476,40 +478,57 @@
 
 void KeyManager::SetSecurityPolicy(const SecurityPolicy &aSecurityPolicy)
 {
-    if (aSecurityPolicy.mRotationTime < SecurityPolicy::kMinKeyRotationTime)
+    SecurityPolicy newPolicy = aSecurityPolicy;
+
+    if (newPolicy.mRotationTime < SecurityPolicy::kMinKeyRotationTime)
     {
-        LogNote("Key Rotation Time too small: %d", aSecurityPolicy.mRotationTime);
-        ExitNow();
+        newPolicy.mRotationTime = SecurityPolicy::kMinKeyRotationTime;
+        LogNote("Key Rotation Time in SecurityPolicy is set to min allowed value of %u", newPolicy.mRotationTime);
     }
 
-    IgnoreError(Get<Notifier>().Update(mSecurityPolicy, aSecurityPolicy, kEventSecurityPolicyChanged));
+    if (newPolicy.mRotationTime != mSecurityPolicy.mRotationTime)
+    {
+        uint32_t newGuardTime = newPolicy.mRotationTime;
 
-exit:
-    return;
+        // Calculations are done using a `uint32_t` variable to prevent
+        // potential overflow.
+
+        newGuardTime *= kKeySwitchGuardTimePercentage;
+        newGuardTime /= 100;
+
+        mKeySwitchGuardTime = static_cast<uint16_t>(newGuardTime);
+    }
+
+    IgnoreError(Get<Notifier>().Update(mSecurityPolicy, newPolicy, kEventSecurityPolicyChanged));
+
+    CheckForKeyRotation();
 }
 
-void KeyManager::StartKeyRotationTimer(void)
+void KeyManager::ResetKeyRotationTimer(void)
 {
     mHoursSinceKeyRotation = 0;
-    mKeyRotationTimer.Start(kOneHourIntervalInMsec);
+    mKeyRotationTimer.Start(Time::kOneHourInMsec);
 }
 
 void KeyManager::HandleKeyRotationTimer(void)
 {
+    mKeyRotationTimer.Start(Time::kOneHourInMsec);
+
     mHoursSinceKeyRotation++;
 
-    // Order of operations below is important. We should restart the timer (from
-    // last fire time for one hour interval) before potentially calling
-    // `SetCurrentKeySequence()`. `SetCurrentKeySequence()` uses the fact that
-    // timer is running to decide to check for the guard time and to reset the
-    // rotation timer (and the `mHoursSinceKeyRotation`) if it updates the key
-    // sequence.
+    if (mKeySwitchGuardTimer > 0)
+    {
+        mKeySwitchGuardTimer--;
+    }
 
-    mKeyRotationTimer.StartAt(mKeyRotationTimer.GetFireTime(), kOneHourIntervalInMsec);
+    CheckForKeyRotation();
+}
 
+void KeyManager::CheckForKeyRotation(void)
+{
     if (mHoursSinceKeyRotation >= mSecurityPolicy.mRotationTime)
     {
-        SetCurrentKeySequence(mKeySequence + 1);
+        SetCurrentKeySequence(mKeySequence + 1, kForceUpdate);
     }
 }
 
diff --git a/src/core/thread/key_manager.hpp b/src/core/thread/key_manager.hpp
index 18f11f2..099854c 100644
--- a/src/core/thread/key_manager.hpp
+++ b/src/core/thread/key_manager.hpp
@@ -77,8 +77,17 @@
      */
     static constexpr uint8_t kVersionThresholdOffsetVersion = 3;
 
-    static constexpr uint16_t kMinKeyRotationTime     = 1;   ///< The minimum Key Rotation Time in hours.
-    static constexpr uint16_t kDefaultKeyRotationTime = 672; ///< Default Key Rotation Time (in unit of hours).
+    /**
+     * Default Key Rotation Time (in unit of hours).
+     *
+     */
+    static constexpr uint16_t kDefaultKeyRotationTime = 672;
+
+    /**
+     * Minimum Key Rotation Time (in unit of hours).
+     *
+     */
+    static constexpr uint16_t kMinKeyRotationTime = 2;
 
     /**
      * Initializes the object with default Key Rotation Time
@@ -212,6 +221,18 @@
 {
 public:
     /**
+     * Determines whether to apply or ignore key switch guard when updating the key sequence.
+     *
+     * Used as input by `SetCurrentKeySequence()`.
+     *
+     */
+    enum KeySequenceUpdateMode : uint8_t
+    {
+        kApplyKeySwitchGuard, ///< Apply key switch guard check before setting the new key sequence.
+        kForceUpdate,         ///< Ignore key switch guard check and forcibly update the key sequence to new value.
+    };
+
+    /**
      * Initializes the object.
      *
      * @param[in]  aInstance     A reference to the OpenThread instance.
@@ -321,10 +342,14 @@
     /**
      * Sets the current key sequence value.
      *
-     * @param[in]  aKeySequence  The key sequence value.
+     * If @p aMode is `kApplyKeySwitchGuard`, the current key switch guard timer is checked and only if it is zero, key
+     * sequence will be updated.
+     *
+     * @param[in]  aKeySequence    The key sequence value.
+     * @param[in]  aUpdateMode     Whether or not to apply the key switch guard.
      *
      */
-    void SetCurrentKeySequence(uint32_t aKeySequence);
+    void SetCurrentKeySequence(uint32_t aKeySequence, KeySequenceUpdateMode aUpdateMode);
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
     /**
@@ -500,17 +525,19 @@
      * @returns The KeySwitchGuardTime value in hours.
      *
      */
-    uint32_t GetKeySwitchGuardTime(void) const { return mKeySwitchGuardTime; }
+    uint16_t GetKeySwitchGuardTime(void) const { return mKeySwitchGuardTime; }
 
     /**
      * Sets the KeySwitchGuardTime.
      *
      * The KeySwitchGuardTime is the time interval during which key rotation procedure is prevented.
      *
-     * @param[in]  aKeySwitchGuardTime  The KeySwitchGuardTime value in hours.
+     * Intended for testing only. Changing the guard time will render device non-compliant with the Thread spec.
+     *
+     * @param[in]  aGuardTime  The KeySwitchGuardTime value in hours.
      *
      */
-    void SetKeySwitchGuardTime(uint32_t aKeySwitchGuardTime) { mKeySwitchGuardTime = aKeySwitchGuardTime; }
+    void SetKeySwitchGuardTime(uint16_t aGuardTime) { mKeySwitchGuardTime = aGuardTime; }
 
     /**
      * Returns the Security Policy.
@@ -565,9 +592,13 @@
 #endif
 
 private:
-    static constexpr uint32_t kDefaultKeySwitchGuardTime = 624;
-    static constexpr uint32_t kOneHourIntervalInMsec     = 3600u * 1000u;
-    static constexpr bool     kExportableMacKeys         = OPENTHREAD_CONFIG_PLATFORM_MAC_KEYS_EXPORTABLE_ENABLE;
+    static constexpr uint16_t kDefaultKeySwitchGuardTime    = 624; // ~ 93% of 672 (default key rotation time)
+    static constexpr uint32_t kKeySwitchGuardTimePercentage = 93;  // Percentage of key rotation time.
+    static constexpr bool     kExportableMacKeys            = OPENTHREAD_CONFIG_PLATFORM_MAC_KEYS_EXPORTABLE_ENABLE;
+
+    static_assert(kDefaultKeySwitchGuardTime ==
+                      SecurityPolicy::kDefaultKeyRotationTime * kKeySwitchGuardTimePercentage / 100,
+                  "Default key switch guard time value is not correct");
 
     OT_TOOL_PACKED_BEGIN
     struct Keys
@@ -591,8 +622,9 @@
     void ComputeTrelKey(uint32_t aKeySequence, Mac::Key &aKey) const;
 #endif
 
-    void StartKeyRotationTimer(void);
+    void ResetKeyRotationTimer(void);
     void HandleKeyRotationTimer(void);
+    void CheckForKeyRotation(void);
 
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
     void StoreNetworkKey(const NetworkKey &aNetworkKey, bool aOverWriteExisting);
@@ -630,9 +662,9 @@
     uint32_t               mStoredMacFrameCounter;
     uint32_t               mStoredMleFrameCounter;
 
-    uint32_t      mHoursSinceKeyRotation;
-    uint32_t      mKeySwitchGuardTime;
-    bool          mKeySwitchGuardEnabled;
+    uint16_t      mHoursSinceKeyRotation;
+    uint16_t      mKeySwitchGuardTime;
+    uint16_t      mKeySwitchGuardTimer;
     RotationTimer mKeyRotationTimer;
 
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
diff --git a/src/core/thread/link_quality.cpp b/src/core/thread/link_quality.cpp
index bf6b35b..6747540 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/locator_getters.hpp"
 #include "common/num_utils.hpp"
+#include "common/numeric_limits.hpp"
 #include "instance/instance.hpp"
 
 namespace ot {
@@ -116,16 +117,20 @@
 
 void LqiAverager::Add(uint8_t aLqi)
 {
-    uint8_t count;
+    uint8_t  count;
+    uint16_t newAverage;
 
-    if (mCount < UINT8_MAX)
+    if (mCount < NumericLimits<uint8_t>::kMax)
     {
         mCount++;
     }
 
     count = Min(static_cast<uint8_t>(1 << kCoeffBitShift), mCount);
 
-    mAverage = static_cast<uint8_t>(((mAverage * (count - 1)) + aLqi) / count);
+    newAverage = mAverage;
+    newAverage = (newAverage * (count - 1) + aLqi) / count;
+
+    mAverage = static_cast<uint8_t>(newAverage);
 }
 
 void LinkQualityInfo::Clear(void)
diff --git a/src/core/thread/mesh_forwarder.cpp b/src/core/thread/mesh_forwarder.cpp
index 99531a1..bffbf5b 100644
--- a/src/core/thread/mesh_forwarder.cpp
+++ b/src/core/thread/mesh_forwarder.cpp
@@ -1476,7 +1476,7 @@
 
         message->SetDatagramTag(fragmentHeader.GetDatagramTag());
         message->SetTimestampToNow();
-        message->SetLinkInfo(aLinkInfo);
+        message->UpdateLinkInfoFrom(aLinkInfo);
 
         VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);
 
@@ -1529,9 +1529,7 @@
         message->WriteData(message->GetOffset(), aFrameData);
         message->MoveOffset(aFrameData.GetLength());
         message->AddRss(aLinkInfo.GetRss());
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
         message->AddLqi(aLinkInfo.GetLqi());
-#endif
         message->SetTimestampToNow();
     }
 
@@ -1542,7 +1540,7 @@
         if (message->GetOffset() >= message->GetLength())
         {
             mReassemblyList.Dequeue(*message);
-            IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
+            IgnoreError(HandleDatagram(*message, aMacAddrs.mSource));
         }
     }
     else
@@ -1642,7 +1640,7 @@
 
     SuccessOrExit(error = FrameToMessage(aFrameData, 0, aMacAddrs, message));
 
-    message->SetLinkInfo(aLinkInfo);
+    message->UpdateLinkInfoFrom(aLinkInfo);
 
     VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);
 
@@ -1654,7 +1652,7 @@
 
     if (error == kErrorNone)
     {
-        IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
+        IgnoreError(HandleDatagram(*message, aMacAddrs.mSource));
     }
     else
     {
@@ -1663,7 +1661,7 @@
     }
 }
 
-Error MeshForwarder::HandleDatagram(Message &aMessage, const ThreadLinkInfo &aLinkInfo, const Mac::Address &aMacSource)
+Error MeshForwarder::HandleDatagram(Message &aMessage, const Mac::Address &aMacSource)
 {
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
     Get<Utils::HistoryTracker>().RecordRxMessage(aMessage, aMacSource);
@@ -1679,7 +1677,7 @@
     aMessage.SetLoopbackToHostAllowed(true);
     aMessage.SetOrigin(Message::kOriginThreadNetif);
 
-    return Get<Ip6::Ip6>().HandleDatagram(OwnedPtr<Message>(&aMessage), &aLinkInfo);
+    return Get<Ip6::Ip6>().HandleDatagram(OwnedPtr<Message>(&aMessage));
 }
 
 Error MeshForwarder::GetFramePriority(const FrameData      &aFrameData,
diff --git a/src/core/thread/mesh_forwarder.hpp b/src/core/thread/mesh_forwarder.hpp
index 8c3a790..1ccd0d0 100644
--- a/src/core/thread/mesh_forwarder.hpp
+++ b/src/core/thread/mesh_forwarder.hpp
@@ -550,7 +550,7 @@
                                  uint16_t                aFragmentLength,
                                  uint16_t                aSrcRloc16,
                                  Message::Priority       aPriority);
-    Error HandleDatagram(Message &aMessage, const ThreadLinkInfo &aLinkInfo, const Mac::Address &aMacSource);
+    Error HandleDatagram(Message &aMessage, const Mac::Address &aMacSource);
     void  ClearReassemblyList(void);
     void  EvictMessage(Message &aMessage);
     void  HandleDiscoverComplete(void);
diff --git a/src/core/thread/mesh_forwarder_ftd.cpp b/src/core/thread/mesh_forwarder_ftd.cpp
index e91ceb1..d4911b9 100644
--- a/src/core/thread/mesh_forwarder_ftd.cpp
+++ b/src/core/thread/mesh_forwarder_ftd.cpp
@@ -627,10 +627,16 @@
 
     error = ip6Headers.DecompressFrom(aFrameData, aMeshAddrs, GetInstance());
 
-    if (error == kErrorNotFound)
+    switch (error)
     {
+    case kErrorNone:
+        break;
+    case kErrorNotFound:
         // Frame may not contain an IPv6 header.
-        ExitNow(error = kErrorNone);
+        error = kErrorNone;
+        OT_FALL_THROUGH;
+    default:
+        ExitNow();
     }
 
     error = Get<Mle::MleRouter>().CheckReachability(aMeshAddrs.mDestination.GetShort(), ip6Headers.GetIp6Header());
@@ -706,13 +712,7 @@
         SuccessOrExit(error = meshHeader.AppendTo(*messagePtr));
         SuccessOrExit(error = messagePtr->AppendData(aFrameData));
 
-        messagePtr->SetLinkInfo(aLinkInfo);
-
-#if OPENTHREAD_CONFIG_MULTI_RADIO
-        // We set the received radio type on the message in order for it
-        // to be logged correctly from LogMessage().
-        messagePtr->SetRadioType(static_cast<Mac::RadioType>(aLinkInfo.mRadioType));
-#endif
+        messagePtr->UpdateLinkInfoFrom(aLinkInfo);
 
         LogMessage(kMessageReceive, *messagePtr, kErrorNone, &aMacSource);
 
diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp
index 7eeb25d..85e0039 100644
--- a/src/core/thread/mle.cpp
+++ b/src/core/thread/mle.cpp
@@ -378,7 +378,7 @@
 
     SuccessOrExit(Get<Settings>().Read(networkInfo));
 
-    Get<KeyManager>().SetCurrentKeySequence(networkInfo.GetKeySequence());
+    Get<KeyManager>().SetCurrentKeySequence(networkInfo.GetKeySequence(), KeyManager::kForceUpdate);
     Get<KeyManager>().SetMleFrameCounter(networkInfo.GetMleFrameCounter());
     Get<KeyManager>().SetAllMacFrameCounters(networkInfo.GetMacFrameCounter(), /* aSetIfLarger */ false);
 
@@ -454,7 +454,8 @@
     mWasLeader = networkInfo.GetRole() == kRoleLeader;
 #endif
 
-    // Successfully restored the network information from non-volatile settings after boot.
+    // Successfully restored the network information from
+    // non-volatile settings after boot.
     mHasRestored = true;
 
 exit:
@@ -533,7 +534,7 @@
 
     VerifyOrExit(!IsDetached() || mAttachState != kAttachStateStart);
 
-    // not in reattach stage after reset
+    // Not in reattach stage after reset
     if (mReattachState == kReattachStop)
     {
         Get<MeshCoP::PendingDatasetManager>().HandleDetach();
@@ -1093,7 +1094,7 @@
 {
     aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(aNeighbor.GetExtAddress());
     aNeighbor.GetLinkInfo().Clear();
-    aNeighbor.GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
+    aNeighbor.GetLinkInfo().AddRss(aRxInfo.mMessage.GetAverageRss());
     aNeighbor.ResetLinkFailures();
     aNeighbor.SetLastHeard(TimerMilli::GetNow());
 }
@@ -1120,7 +1121,6 @@
     {
         if (!Get<ThreadNetif>().HasUnicastAddress(mMeshLocal64.GetAddress()))
         {
-            // Mesh Local EID was removed, choose a new one and add it back
             mMeshLocal64.GetAddress().GetIid().GenerateRandom();
 
             Get<ThreadNetif>().AddUnicastAddress(mMeshLocal64);
@@ -1135,9 +1135,12 @@
 
     if (aEvents.ContainsAny(kEventIp6MulticastSubscribed | kEventIp6MulticastUnsubscribed))
     {
-        // When multicast subscription changes, SED always notifies its parent as it depends on its
-        // parent for indirect transmission. Since Thread 1.2, MED MAY also notify its parent of 1.2
-        // or higher version as it could depend on its parent to perform Multicast Listener Report.
+        // When multicast subscription changes, SED always notifies
+        // its parent as it depends on its parent for indirect
+        // transmission. Since Thread 1.2, MED MAY also notify its
+        // parent of 1.2 or higher version as it could depend on its
+        // parent to perform Multicast Listener Report.
+
         if (IsChild() && !IsFullThreadDevice() &&
             (!IsRxOnWhenIdle()
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
@@ -1577,7 +1580,7 @@
         }
         else if (!IsRxOnWhenIdle())
         {
-            // return to sleepy operation
+            // Return to sleepy operation
             Get<DataPollSender>().SetAttachMode(false);
             Get<MeshForwarder>().SetRxOnWhenIdle(false);
         }
@@ -1653,7 +1656,6 @@
 exit:
     if (error != kErrorNone)
     {
-        // do not use `FreeMessageOnError()` to avoid null check on nonnull pointer
         aMessage.Free();
     }
 }
@@ -1782,9 +1784,13 @@
         {
             // Invalidate stale parent state.
             //
-            // Parent state is not normally invalidated after becoming a Router/Leader (see #1875).  When trying to
-            // attach to a better partition, invalidating old parent state (especially when in kStateRestored) ensures
-            // that FindNeighbor() returns mParentCandidate when processing the Child ID Response.
+            // Parent state is not normally invalidated after becoming
+            // a Router/Leader (see #1875).  When trying to attach to
+            // a better partition, invalidating old parent state
+            // (especially when in `kStateRestored`) ensures that
+            // `FindNeighbor()` returns `mParentCandidate` when
+            // processing the Child ID Response.
+
             mParent.SetState(Neighbor::kStateInvalid);
         }
     }
@@ -1802,7 +1808,7 @@
     {
         SuccessOrExit(error = message->AppendAddressRegistrationTlv(mAddressRegistrationMode));
 
-        // no need to request the last Route64 TLV for MTD
+        // No need to request the last Route64 TLV for MTD
         tlvsLen -= 1;
     }
 
@@ -2011,9 +2017,8 @@
     case kChildUpdateRequestPending:
         if (Get<Notifier>().IsPending())
         {
-            // Here intentionally delay another kChildUpdateRequestPendingDelay
-            // cycle to ensure we only send a Child Update Request after we
-            // know there are no more pending changes.
+            // Add another delay to ensures the Child Update Request is sent
+            // only after all pending changes are incorporated.
             ScheduleMessageTransmissionTimer();
             ExitNow();
         }
@@ -2052,8 +2057,13 @@
         ExitNow();
     }
 
-    mChildUpdateRequestState = kChildUpdateRequestActive;
-    ScheduleMessageTransmissionTimer();
+    if (aMode != kAppendZeroTimeout)
+    {
+        // Enable MLE retransmissions on all Child Update Request
+        // messages, except when actively detaching.
+        mChildUpdateRequestState = kChildUpdateRequestActive;
+        ScheduleMessageTransmissionTimer();
+    }
 
     VerifyOrExit((message = NewMleMessage(kCommandChildUpdateRequest)) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendModeTlv(mDeviceMode));
@@ -2387,7 +2397,7 @@
     if (aMode == Crypto::AesCcm::kDecrypt)
     {
         // Skip decrypting the message under fuzz build mode
-        IgnoreError(aMessage.SetLength(aMessage.GetLength() - kMleSecurityTagSize));
+        aMessage.RemoveFooter(kMleSecurityTagSize);
         ExitNow();
     }
 #endif
@@ -2402,7 +2412,7 @@
     else
     {
         VerifyOrExit(aMessage.Compare(aMessage.GetLength() - kMleSecurityTagSize, tag), error = kErrorSecurity);
-        IgnoreError(aMessage.SetLength(aMessage.GetLength() - kMleSecurityTagSize));
+        aMessage.RemoveFooter(kMleSecurityTagSize);
     }
 
 exit:
@@ -2431,7 +2441,7 @@
 
     LogDebg("Receive MLE message");
 
-    VerifyOrExit(aMessageInfo.GetLinkInfo() != nullptr);
+    VerifyOrExit(aMessage.GetOrigin() == Message::kOriginThreadNetif);
     VerifyOrExit(aMessageInfo.GetHopLimit() == kMleHopLimit, error = kErrorParse);
 
     SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), securitySuite));
@@ -2642,13 +2652,13 @@
     case kCommandChildIdRequest:
         Get<MleRouter>().HandleChildIdRequest(rxInfo);
         break;
+#endif // OPENTHREAD_FTD
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     case kCommandTimeSync:
-        Get<MleRouter>().HandleTimeSync(rxInfo);
+        HandleTimeSync(rxInfo);
         break;
 #endif
-#endif // OPENTHREAD_FTD
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     case kCommandLinkMetricsManagementRequest:
@@ -2699,7 +2709,7 @@
     // We skip logging failures for broadcast MLE messages since it
     // can be common to receive such messages from adjacent Thread
     // networks.
-    if (!aMessageInfo.GetSockAddr().IsMulticast() || !aMessageInfo.GetThreadLinkInfo()->IsDstPanIdBroadcast())
+    if (!aMessageInfo.GetSockAddr().IsMulticast() || !aMessage.IsDstPanIdBroadcast())
     {
         LogProcessError(kTypeGenericUdp, error);
     }
@@ -2726,7 +2736,7 @@
     switch (aRxInfo.mClass)
     {
     case RxInfo::kAuthoritativeMessage:
-        Get<KeyManager>().SetCurrentKeySequence(aRxInfo.mKeySequence);
+        Get<KeyManager>().SetCurrentKeySequence(aRxInfo.mKeySequence, KeyManager::kForceUpdate);
         break;
 
     case RxInfo::kPeerMessage:
@@ -2734,7 +2744,7 @@
         {
             if (aRxInfo.mKeySequence - Get<KeyManager>().GetCurrentKeySequence() == 1)
             {
-                Get<KeyManager>().SetCurrentKeySequence(aRxInfo.mKeySequence);
+                Get<KeyManager>().SetCurrentKeySequence(aRxInfo.mKeySequence, KeyManager::kApplyKeySwitchGuard);
             }
             else
             {
@@ -2875,11 +2885,8 @@
 
     if (mDataRequestState == kDataRequestNone && !IsRxOnWhenIdle())
     {
-        // Here simply stops fast data poll request by Mle Data Request.
-        // Note that in some cases fast data poll may continue after below stop operation until
-        // running out the specified number. E.g. other component also trigger fast poll, and
-        // is waiting for response; or the corner case where multiple Mle Data Request attempts
-        // happened due to the retransmission mechanism.
+        // Stop fast data poll request by MLE since we received
+        // the response.
         Get<DataPollSender>().StopFastPolls();
     }
 
@@ -2913,7 +2920,6 @@
     uint16_t                  pendingDatasetLength = 0;
     bool                      dataRequest          = false;
 
-    // Leader Data
     SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
 
     if ((leaderData.GetPartitionId() != mLeaderData.GetPartitionId()) ||
@@ -2934,7 +2940,6 @@
         VerifyOrExit(IsNetworkDataNewer(leaderData));
     }
 
-    // Active Timestamp
     switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, activeTimestamp))
     {
     case kErrorNone:
@@ -2942,8 +2947,9 @@
 
         timestamp = Get<MeshCoP::ActiveDatasetManager>().GetTimestamp();
 
-        // if received timestamp does not match the local value and message does not contain the dataset,
-        // send MLE Data Request
+        // Send an MLE Data Request if the received timestamp
+        // mismatches the local value and the message does not
+        // include the dataset.
         if (!IsLeader() && (MeshCoP::Timestamp::Compare(&activeTimestamp, timestamp) != 0) &&
             (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kActiveDataset, activeDatasetOffset, activeDatasetLength) !=
              kErrorNone))
@@ -2960,7 +2966,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Pending Timestamp
     switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, pendingTimestamp))
     {
     case kErrorNone:
@@ -2968,8 +2973,6 @@
 
         timestamp = Get<MeshCoP::PendingDatasetManager>().GetTimestamp();
 
-        // 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::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kPendingDataset, pendingDatasetOffset,
                                      pendingDatasetLength) != kErrorNone))
@@ -3007,7 +3010,6 @@
     else
 #endif
     {
-        // Active Dataset
         if (hasActiveTimestamp)
         {
             if (activeDatasetOffset > 0)
@@ -3017,7 +3019,6 @@
             }
         }
 
-        // Pending Dataset
         if (hasPendingTimestamp)
         {
             if (pendingDatasetOffset > 0)
@@ -3132,7 +3133,7 @@
 void Mle::HandleParentResponse(RxInfo &aRxInfo)
 {
     Error            error = kErrorNone;
-    int8_t           rss   = aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss();
+    int8_t           rss   = aRxInfo.mMessage.GetAverageRss();
     RxChallenge      response;
     uint16_t         version;
     uint16_t         sourceAddress;
@@ -3149,16 +3150,13 @@
     TimeParameterTlv timeParameterTlv;
 #endif
 
-    // Source Address
     SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
 
     Log(kMessageReceive, kTypeParentResponse, aRxInfo.mMessageInfo.GetPeerAddr(), sourceAddress);
 
-    // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
     VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
-    // Response
     SuccessOrExit(error = aRxInfo.mMessage.ReadResponseTlv(response));
     VerifyOrExit(response == mParentRequestChallenge, error = kErrorParse);
 
@@ -3169,20 +3167,16 @@
         mReceivedResponseFromParent = true;
     }
 
-    // Leader Data
     SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
 
-    // Link Margin
     SuccessOrExit(error = Tlv::Find<LinkMarginTlv>(aRxInfo.mMessage, linkMarginFromTlv));
     linkMargin  = Min(Get<Mac::Mac>().ComputeLinkMargin(rss), linkMarginFromTlv);
     linkQuality = LinkQualityForLinkMargin(linkMargin);
 
-    // Connectivity
     SuccessOrExit(error = Tlv::FindTlv(aRxInfo.mMessage, connectivityTlv));
     VerifyOrExit(connectivityTlv.IsValid(), error = kErrorParse);
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    // CSL Accuracy
     switch (aRxInfo.mMessage.ReadCslClockAccuracyTlv(cslAccuracy))
     {
     case kErrorNone:
@@ -3255,7 +3249,7 @@
 
     if (mParentCandidate.IsStateParentResponse() && (mParentCandidate.GetExtAddress() != extAddress))
     {
-        // if already have a candidate parent, only seek a better parent
+        // If already have a candidate parent, only seek a better parent
 
         int compare = 0;
 
@@ -3266,23 +3260,21 @@
                                                    mParentCandidate.mIsSingleton, mParentCandidate.mLeaderData);
         }
 
-        // only consider partitions that are the same or better
+        // Only consider partitions that are the same or better
         VerifyOrExit(compare >= 0);
 #endif
 
-        // only consider better parents if the partitions are the same
+        // Only consider better parents if the partitions are the same
         if (compare == 0)
         {
             VerifyOrExit(IsBetterParent(sourceAddress, linkQuality, linkMargin, connectivityTlv, version, cslAccuracy));
         }
     }
 
-    // Link/MLE Frame Counters
     SuccessOrExit(error = aRxInfo.mMessage.ReadFrameCounterTlvs(linkFrameCounter, mleFrameCounter));
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 
-    // Time Parameter
     if (Tlv::FindTlv(aRxInfo.mMessage, timeParameterTlv) == kErrorNone)
     {
         VerifyOrExit(timeParameterTlv.IsValid());
@@ -3294,14 +3286,13 @@
 #if OPENTHREAD_CONFIG_TIME_SYNC_REQUIRED
     else
     {
-        // If the time sync feature is required, don't choose the parent which doesn't support it.
+        // If the time sync feature is required, don't choose the
+        // parent which doesn't support it.
         ExitNow();
     }
-
-#endif // OPENTHREAD_CONFIG_TIME_SYNC_REQUIRED
+#endif
 #endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 
-    // Challenge
     SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(mParentCandidate.mRxChallenge));
 
     InitNeighbor(mParentCandidate, aRxInfo);
@@ -3346,7 +3337,6 @@
     uint16_t           offset;
     uint16_t           length;
 
-    // Source Address
     SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
 
     Log(kMessageReceive, kTypeChildIdResponse, aRxInfo.mMessageInfo.GetPeerAddr(), sourceAddress);
@@ -3355,22 +3345,17 @@
 
     VerifyOrExit(mAttachState == kAttachStateChildIdRequest);
 
-    // ShortAddress
     SuccessOrExit(error = Tlv::Find<Address16Tlv>(aRxInfo.mMessage, shortAddress));
     VerifyOrExit(RouterIdMatch(sourceAddress, shortAddress), error = kErrorRejected);
 
-    // Leader Data
     SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
 
-    // Network Data
     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::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kActiveDataset, offset, length) == kErrorNone)
         {
             SuccessOrExit(error =
@@ -3385,17 +3370,15 @@
         ExitNow(error = kErrorParse);
     }
 
-    // clear Pending Dataset if device succeed to reattach using stored Pending Dataset
+    // Clear Pending Dataset if device succeed to reattach using stored Pending Dataset
     if (mReattachState == kReattachPending)
     {
         Get<MeshCoP::PendingDatasetManager>().Clear();
     }
 
-    // Pending Timestamp
     switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
-        // Pending Dataset
         if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kPendingDataset, offset, length) == kErrorNone)
         {
             IgnoreError(Get<MeshCoP::PendingDatasetManager>().Save(timestamp, aRxInfo.mMessage, offset, length));
@@ -3411,7 +3394,6 @@
     }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-    // Sync to Thread network time
     if (aRxInfo.mMessage.GetTimeSyncSeq() != OT_TIME_SYNC_INVALID_SEQ)
     {
         Get<TimeSync>().HandleTimeSyncMessage(aRxInfo.mMessage);
@@ -3467,12 +3449,10 @@
     TlvList     requestedTlvList;
     TlvList     tlvList;
 
-    // Source Address
     SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
 
     Log(kMessageReceive, kTypeChildUpdateRequestAsChild, aRxInfo.mMessageInfo.GetPeerAddr(), sourceAddress);
 
-    // Challenge
     switch (aRxInfo.mMessage.ReadChallengeTlv(challenge))
     {
     case kErrorNone:
@@ -3508,7 +3488,6 @@
             ExitNow();
         }
 
-        // Leader Data, Network Data, Active Timestamp, Pending Timestamp
         SuccessOrExit(error = HandleLeaderData(aRxInfo));
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
@@ -3517,7 +3496,8 @@
 
             if (aRxInfo.mMessage.ReadCslClockAccuracyTlv(cslAccuracy) == kErrorNone)
             {
-                // MUST include CSL timeout TLV when request includes CSL accuracy
+                // MUST include CSL timeout TLV when request includes
+                // CSL accuracy
                 tlvList.Add(Tlv::kCslTimeout);
             }
         }
@@ -3525,11 +3505,10 @@
     }
     else
     {
-        // this device is not a child of the Child Update Request source
+        // This device is not a child of the Child Update Request source
         tlvList.Add(Tlv::kStatus);
     }
 
-    // TLV Request
     switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
     {
     case kErrorNone:
@@ -3551,7 +3530,8 @@
     }
 #endif
 
-    // Send the response to the requester, regardless if it's this device's parent or not
+    // Send the response to the requester, regardless if it's this
+    // device's parent or not.
     SuccessOrExit(error = SendChildUpdateResponse(tlvList, challenge, aRxInfo.mMessageInfo.GetPeerAddr()));
 
 exit:
@@ -3596,14 +3576,12 @@
         OT_ASSERT(false);
     }
 
-    // Status
     if (Tlv::Find<StatusTlv>(aRxInfo.mMessage, status) == kErrorNone)
     {
         IgnoreError(BecomeDetached());
         ExitNow();
     }
 
-    // Mode
     SuccessOrExit(error = Tlv::Find<ModeTlv>(aRxInfo.mMessage, mode));
     VerifyOrExit(DeviceMode(mode) == mDeviceMode, error = kErrorDrop);
 
@@ -3631,7 +3609,6 @@
         OT_FALL_THROUGH;
 
     case kRoleChild:
-        // Source Address
         SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
 
         if (RouterIdFromRloc16(sourceAddress) != RouterIdFromRloc16(GetRloc16()))
@@ -3640,10 +3617,8 @@
             ExitNow();
         }
 
-        // Leader Data, Network Data, Active Timestamp, Pending Timestamp
         SuccessOrExit(error = HandleLeaderData(aRxInfo));
 
-        // Timeout optional
         switch (Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout))
         {
         case kErrorNone:
@@ -3666,7 +3641,6 @@
         {
             Mac::CslAccuracy cslAccuracy;
 
-            // CSL Accuracy
             switch (aRxInfo.mMessage.ReadCslClockAccuracyTlv(cslAccuracy))
             {
             case kErrorNone:
@@ -3815,8 +3789,23 @@
 exit:
     LogProcessError(kTypeLinkMetricsManagementRequest, error);
 }
+#endif
 
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+void Mle::HandleTimeSync(RxInfo &aRxInfo)
+{
+    Log(kMessageReceive, kTypeTimeSync, aRxInfo.mMessageInfo.GetPeerAddr());
+
+    VerifyOrExit(aRxInfo.IsNeighborStateValid());
+
+    aRxInfo.mClass = RxInfo::kPeerMessage;
+
+    Get<TimeSync>().HandleTimeSyncMessage(aRxInfo.mMessage);
+
+exit:
+    return;
+}
+#endif
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
 void Mle::HandleLinkMetricsManagementResponse(RxInfo &aRxInfo)
@@ -3835,7 +3824,7 @@
 exit:
     LogProcessError(kTypeLinkMetricsManagementResponse, error);
 }
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+#endif
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 void Mle::HandleLinkProbe(RxInfo &aRxInfo)
@@ -3856,7 +3845,7 @@
 exit:
     LogProcessError(kTypeLinkProbe, error);
 }
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#endif
 
 void Mle::ProcessAnnounce(void)
 {
@@ -3916,22 +3905,6 @@
 
 bool Mle::IsMeshLocalAddress(const Ip6::Address &aAddress) const { return (aAddress.GetPrefix() == mMeshLocalPrefix); }
 
-Error Mle::CheckReachability(uint16_t aMeshDest, const Ip6::Header &aIp6Header)
-{
-    Error error;
-
-    if ((aMeshDest != GetRloc16()) || Get<ThreadNetif>().HasUnicastAddress(aIp6Header.GetDestination()))
-    {
-        error = kErrorNone;
-    }
-    else
-    {
-        error = kErrorNoRoute;
-    }
-
-    return error;
-}
-
 #if OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH
 void Mle::InformPreviousParent(void)
 {
@@ -3951,13 +3924,8 @@
     LogNote("Sending message to inform previous parent 0x%04x", mPreviousParentRloc);
 
 exit:
-
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to inform previous parent: %s", ErrorToString(error));
-
-        FreeMessage(message);
-    }
+    LogWarnOnError(error, "inform previous parent");
+    FreeMessageOnError(message, error);
 }
 #endif // OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH
 
@@ -4158,14 +4126,14 @@
         "Link Reject",             // (25) kTypeLinkReject
         "Link Request",            // (26) kTypeLinkRequest
         "Parent Request",          // (27) kTypeParentRequest
-#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-        "Time Sync", // (28) kTypeTimeSync
-#endif
 #endif
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-        "Link Metrics Management Request",  // (29) kTypeLinkMetricsManagementRequest
-        "Link Metrics Management Response", // (30) kTypeLinkMetricsManagementResponse
-        "Link Probe",                       // (31) kTypeLinkProbe
+        "Link Metrics Management Request",  // (28) kTypeLinkMetricsManagementRequest
+        "Link Metrics Management Response", // (29) kTypeLinkMetricsManagementResponse
+        "Link Probe",                       // (30) kTypeLinkProbe
+#endif
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+        "Time Sync", // (31) kTypeTimeSync
 #endif
     };
 
@@ -4198,25 +4166,30 @@
     static_assert(kTypeLinkReject == 25, "kTypeLinkReject value is incorrect");
     static_assert(kTypeLinkRequest == 26, "kTypeLinkRequest value is incorrect");
     static_assert(kTypeParentRequest == 27, "kTypeParentRequest value is incorrect");
-#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-    static_assert(kTypeTimeSync == 28, "kTypeTimeSync value is incorrect");
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    static_assert(kTypeLinkMetricsManagementRequest == 29, "kTypeLinkMetricsManagementRequest value is incorrect)");
-    static_assert(kTypeLinkMetricsManagementResponse == 30, "kTypeLinkMetricsManagementResponse value is incorrect)");
-    static_assert(kTypeLinkProbe == 31, "kTypeLinkProbe value is incorrect)");
-#endif
-#else // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     static_assert(kTypeLinkMetricsManagementRequest == 28, "kTypeLinkMetricsManagementRequest value is incorrect)");
     static_assert(kTypeLinkMetricsManagementResponse == 29, "kTypeLinkMetricsManagementResponse value is incorrect)");
     static_assert(kTypeLinkProbe == 30, "kTypeLinkProbe value is incorrect)");
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    static_assert(kTypeTimeSync == 31, "kTypeTimeSync value is incorrect");
 #endif
-#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-#else  // OPENTHREAD_FTD
+#else
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    static_assert(kTypeTimeSync == 28, "kTypeTimeSync value is incorrect");
+#endif
+#endif
+#else // OPENTHREAD_FTD
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     static_assert(kTypeLinkMetricsManagementRequest == 16, "kTypeLinkMetricsManagementRequest value is incorrect)");
     static_assert(kTypeLinkMetricsManagementResponse == 17, "kTypeLinkMetricsManagementResponse value is incorrect)");
     static_assert(kTypeLinkProbe == 18, "kTypeLinkProbe value is incorrect)");
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    static_assert(kTypeTimeSync == 19, "kTypeTimeSync value is incorrect");
+#endif
+#else
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    static_assert(kTypeTimeSync == 16, "kTypeTimeSync value is incorrect");
+#endif
 #endif
 #endif // OPENTHREAD_FTD
 
@@ -4450,10 +4423,7 @@
     IgnoreError(aMessage.Read(length - sizeof(*this), *this));
 }
 
-void Mle::DelayedResponseMetadata::RemoveFrom(Message &aMessage) const
-{
-    SuccessOrAssert(aMessage.SetLength(aMessage.GetLength() - sizeof(*this)));
-}
+void Mle::DelayedResponseMetadata::RemoveFrom(Message &aMessage) const { aMessage.RemoveFooter(sizeof(*this)); }
 
 //---------------------------------------------------------------------------------------------------------------------
 // TxMessage
diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp
index 33d0920..873e338 100644
--- a/src/core/thread/mle.hpp
+++ b/src/core/thread/mle.hpp
@@ -973,15 +973,15 @@
         kTypeLinkReject,
         kTypeLinkRequest,
         kTypeParentRequest,
-#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-        kTypeTimeSync,
-#endif
 #endif
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
         kTypeLinkMetricsManagementRequest,
         kTypeLinkMetricsManagementResponse,
         kTypeLinkProbe,
 #endif
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+        kTypeTimeSync,
+#endif
     };
 
     //------------------------------------------------------------------------------------------------------------------
@@ -1240,7 +1240,6 @@
     void       SetAttachState(AttachState aState);
     void       InitNeighbor(Neighbor &aNeighbor, const RxInfo &aRxInfo);
     void       ClearParentCandidate(void) { mParentCandidate.Clear(); }
-    Error      CheckReachability(uint16_t aMeshDest, const Ip6::Header &aIp6Header);
     Error      SendDataRequest(const Ip6::Address &aDestination);
     void       HandleNotifierEvents(Events aEvents);
     void       SendDelayedResponse(TxMessage &aMessage, const DelayedResponseMetadata &aMetadata);
@@ -1314,6 +1313,10 @@
     void         UpdateServiceAlocs(void);
 #endif
 
+#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    void HandleTimeSync(RxInfo &aRxInfo);
+#endif
+
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     void  HandleLinkMetricsManagementRequest(RxInfo &aRxInfo);
     void  HandleLinkProbe(RxInfo &aRxInfo);
diff --git a/src/core/thread/mle_router.cpp b/src/core/thread/mle_router.cpp
index b97a7ec..26c6541 100644
--- a/src/core/thread/mle_router.cpp
+++ b/src/core/thread/mle_router.cpp
@@ -157,7 +157,12 @@
 {
     Error error = kErrorNone;
 
-    VerifyOrExit(IsFullThreadDevice() || !aEligible, error = kErrorNotCapable);
+    if (!IsFullThreadDevice())
+    {
+        VerifyOrExit(!aEligible, error = kErrorNotCapable);
+    }
+
+    VerifyOrExit(aEligible != mRouterEligible);
 
     mRouterEligible = aEligible;
 
@@ -168,6 +173,11 @@
         break;
 
     case kRoleChild:
+        if (mRouterEligible)
+        {
+            mRouterRoleTransition.StartTimeout();
+        }
+
         Get<Mac::Mac>().SetBeaconEnabled(mRouterEligible);
         break;
 
@@ -363,15 +373,22 @@
 
     case kAnyPartition:
     case kBetterParent:
-        // If attach was started due to receiving MLE Announce Messages, all rx-on-when-idle devices would
-        // start attach immediately when receiving such Announce message as in Thread 1.1 specification,
-        // Section 4.8.1,
-        // "If the received value is newer and the channel and/or PAN ID in the Announce message differ
-        //  from those currently in use, the receiving device attempts to attach using the channel and
-        //  PAN ID received from the Announce message."
+
+        // If attach was initiated due to receiving an MLE Announce
+        // message, all rx-on-when-idle devices will immediately
+        // attempt to attach as well. This aligns with the Thread 1.1
+        // specification (Section 4.8.1):
         //
-        // That is, Parent-child relationship is highly unlikely to be kept in the new partition, so here
-        // removes all children, leaving whether to become router according to the new partition status.
+        // "If the received value is newer and the channel and/or PAN
+        //  ID in the Announce message differ from those currently in
+        //  use, the receiving device attempts to attach using the
+        //  channel and PAN ID received from the Announce message."
+        //
+        // Since parent-child relationships are unlikely to persist in
+        // the new partition, we remove all children here. The
+        // decision to become router is determined based on the new
+        // partition's status.
+
         if (IsAnnounceAttach() && HasChildren())
         {
             RemoveChildren();
@@ -442,7 +459,7 @@
         Get<AddressResolver>().Clear();
     }
 
-    // Remove children that do not have matching RLOC16
+    // Remove children that do not have a matching RLOC16
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValidOrRestoring))
     {
         if (RouterIdFromRloc16(child.GetRloc16()) != mRouterId)
@@ -521,17 +538,19 @@
     Ip6::Address destination;
     TxMessage   *message = nullptr;
 
-    // Suppress MLE Advertisements when trying to attach to a better partition.
-    //
-    // Without this suppression, a device may send an MLE Advertisement before receiving the MLE Child ID Response.
-    // The candidate parent then removes the attaching device because the Source Address TLV includes an RLOC16 that
-    // indicates a Router role (i.e. a Child ID equal to zero).
+    // Suppress MLE Advertisements when trying to attach to a better
+    // partition. Without this, a candidate parent might incorrectly
+    // interpret this advertisement (Source Address TLV containing an
+    // RLOC16 indicating device is acting as router) and reject the
+    // attaching device.
+
     VerifyOrExit(!IsAttaching());
 
-    // Suppress MLE Advertisements when transitioning to the router role.
-    //
-    // When trying to attach to a new partition, sending out advertisements as a REED can cause already-attached
-    // children to detach.
+    // Suppress MLE Advertisements when attempting to transition to
+    // router role. Advertisements as a REED while attaching to a new
+    // partition can cause existing children to detach
+    // unnecessarily.
+
     VerifyOrExit(!mAddressSolicitPending);
 
     VerifyOrExit((message = NewMleMessage(kCommandAdvertisement)) != nullptr, error = kErrorNoBufs);
@@ -665,14 +684,11 @@
 
     VerifyOrExit(!IsAttaching(), error = kErrorInvalidState);
 
-    // Challenge
     SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(challenge));
 
-    // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
     VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
-    // Leader Data
     switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
     {
     case kErrorNone:
@@ -684,7 +700,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Source Address
     switch (Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress))
     {
     case kErrorNone:
@@ -711,7 +726,8 @@
         break;
 
     case kErrorNotFound:
-        // lack of source address indicates router coming out of reset
+        // A missing source address indicates that the router was
+        // recently reset.
         VerifyOrExit(aRxInfo.IsNeighborStateValid() && IsActiveRouter(aRxInfo.mNeighbor->GetRloc16()),
                      error = kErrorDrop);
         neighbor = aRxInfo.mNeighbor;
@@ -721,7 +737,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // TLV Request
     switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
     {
     case kErrorNone:
@@ -748,16 +763,16 @@
     aRxInfo.mClass = RxInfo::kPeerMessage;
     ProcessKeySequence(aRxInfo);
 
-    SuccessOrExit(error = SendLinkAccept(aRxInfo.mMessageInfo, neighbor, requestedTlvList, challenge));
+    SuccessOrExit(error = SendLinkAccept(aRxInfo, neighbor, requestedTlvList, challenge));
 
 exit:
     LogProcessError(kTypeLinkRequest, error);
 }
 
-Error MleRouter::SendLinkAccept(const Ip6::MessageInfo &aMessageInfo,
-                                Neighbor               *aNeighbor,
-                                const TlvList          &aRequestedTlvList,
-                                const RxChallenge      &aChallenge)
+Error MleRouter::SendLinkAccept(const RxInfo      &aRxInfo,
+                                Neighbor          *aNeighbor,
+                                const TlvList     &aRequestedTlvList,
+                                const RxChallenge &aChallenge)
 {
     static const uint8_t kRouterTlvs[] = {Tlv::kLinkMargin};
 
@@ -775,9 +790,7 @@
     SuccessOrExit(error = message->AppendLinkFrameCounterTlv());
     SuccessOrExit(error = message->AppendMleFrameCounterTlv());
 
-    // always append a link margin, regardless of whether or not it was requested
-    linkMargin = Get<Mac::Mac>().ComputeLinkMargin(aMessageInfo.GetThreadLinkInfo()->GetRss());
-
+    linkMargin = Get<Mac::Mac>().ComputeLinkMargin(aRxInfo.mMessage.GetAverageRss());
     SuccessOrExit(error = message->AppendLinkMarginTlv(linkMargin));
 
     if (aNeighbor != nullptr && IsActiveRouter(aNeighbor->GetRloc16()))
@@ -823,20 +836,20 @@
     }
 #endif
 
-    if (aMessageInfo.GetSockAddr().IsMulticast())
+    if (aRxInfo.mMessageInfo.GetSockAddr().IsMulticast())
     {
-        SuccessOrExit(error = message->SendAfterDelay(aMessageInfo.GetPeerAddr(),
+        SuccessOrExit(error = message->SendAfterDelay(aRxInfo.mMessageInfo.GetPeerAddr(),
                                                       1 + Random::NonCrypto::GetUint16InRange(0, kMaxLinkAcceptDelay)));
 
         Log(kMessageDelay, (command == kCommandLinkAccept) ? kTypeLinkAccept : kTypeLinkAcceptAndRequest,
-            aMessageInfo.GetPeerAddr());
+            aRxInfo.mMessageInfo.GetPeerAddr());
     }
     else
     {
-        SuccessOrExit(error = message->SendTo(aMessageInfo.GetPeerAddr()));
+        SuccessOrExit(error = message->SendTo(aRxInfo.mMessageInfo.GetPeerAddr()));
 
         Log(kMessageSend, (command == kCommandLinkAccept) ? kTypeLinkAccept : kTypeLinkAcceptAndRequest,
-            aMessageInfo.GetPeerAddr());
+            aRxInfo.mMessageInfo.GetPeerAddr());
     }
 
 exit:
@@ -874,7 +887,6 @@
     LeaderData      leaderData;
     uint8_t         linkMargin;
 
-    // Source Address
     SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
 
     Log(kMessageReceive, aRequest ? kTypeLinkAcceptAndRequest : kTypeLinkAccept, aRxInfo.mMessageInfo.GetPeerAddr(),
@@ -886,10 +898,8 @@
     router        = mRouterTable.FindRouterById(routerId);
     neighborState = (router != nullptr) ? router->GetState() : Neighbor::kStateInvalid;
 
-    // Response
     SuccessOrExit(error = aRxInfo.mMessage.ReadResponseTlv(response));
 
-    // verify response
     switch (neighborState)
     {
     case Neighbor::kStateLinkRequest:
@@ -915,22 +925,20 @@
         RemoveNeighbor(*aRxInfo.mNeighbor);
     }
 
-    // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
     VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
-    // Link and MLE Frame Counters
     SuccessOrExit(error = aRxInfo.mMessage.ReadFrameCounterTlvs(linkFrameCounter, mleFrameCounter));
 
-    // Link Margin
     switch (Tlv::Find<LinkMarginTlv>(aRxInfo.mMessage, linkMargin))
     {
     case kErrorNone:
         break;
     case kErrorNotFound:
-        // Link Margin TLV may be skipped in Router Synchronization process after Reset
+        // The Link Margin TLV may be omitted after a reset. We wait
+        // for MLE Advertisements to establish the routing cost to
+        // the neighbor
         VerifyOrExit(IsDetached(), error = kErrorNotFound);
-        // Wait for an MLE Advertisement to establish a routing cost to the neighbor
         linkMargin = 0;
         break;
     default:
@@ -940,15 +948,12 @@
     switch (mRole)
     {
     case kRoleDetached:
-        // Address16
         SuccessOrExit(error = Tlv::Find<Address16Tlv>(aRxInfo.mMessage, address16));
         VerifyOrExit(GetRloc16() == address16, error = kErrorDrop);
 
-        // Leader Data
         SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
         SetLeaderData(leaderData.GetPartitionId(), leaderData.GetWeighting(), leaderData.GetLeaderRouterId());
 
-        // Route
         mRouterTable.Clear();
         SuccessOrExit(error = aRxInfo.mMessage.ReadRouteTlv(routeTlv));
         SuccessOrExit(error = ProcessRouteTlv(routeTlv, aRxInfo));
@@ -964,7 +969,7 @@
             SetStateRouter(GetRloc16());
         }
 
-        mLinkRequestAttempts    = 0; // completed router sync after reset, no more link request to retransmit
+        mLinkRequestAttempts    = 0;
         mRetrieveNewNetworkData = true;
         IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
 
@@ -981,7 +986,6 @@
     case kRoleLeader:
         VerifyOrExit(router != nullptr);
 
-        // Leader Data
         SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
         VerifyOrExit(leaderData.GetPartitionId() == mLeaderData.GetPartitionId());
 
@@ -992,7 +996,6 @@
             IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
         }
 
-        // Route (optional)
         switch (aRxInfo.mMessage.ReadRouteTlv(routeTlv))
         {
         case kErrorNone:
@@ -1026,7 +1029,6 @@
         OT_ASSERT(false);
     }
 
-    // finish link synchronization
     InitNeighbor(*router, aRxInfo);
     router->SetRloc16(sourceAddress);
     router->GetLinkFrameCounters().SetAll(linkFrameCounter);
@@ -1049,10 +1051,8 @@
         RxChallenge challenge;
         TlvList     requestedTlvList;
 
-        // Challenge
         SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(challenge));
 
-        // TLV Request
         switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
         {
         case kErrorNone:
@@ -1062,7 +1062,7 @@
             ExitNow(error = kErrorParse);
         }
 
-        SuccessOrExit(error = SendLinkAccept(aRxInfo.mMessageInfo, router, requestedTlvList, challenge));
+        SuccessOrExit(error = SendLinkAccept(aRxInfo, router, requestedTlvList, challenge));
     }
 
 exit:
@@ -1178,7 +1178,7 @@
     // - `aLeaderData` is the read value from `LeaderDataTlv`.
 
     Error    error      = kErrorNone;
-    uint8_t  linkMargin = Get<Mac::Mac>().ComputeLinkMargin(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
+    uint8_t  linkMargin = Get<Mac::Mac>().ComputeLinkMargin(aRxInfo.mMessage.GetAverageRss());
     RouteTlv routeTlv;
     Router  *router;
     uint8_t  routerId;
@@ -1218,7 +1218,7 @@
 
         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.
+            // Allow a better partition if it also enables time sync.
             && aRxInfo.mMessage.GetTimeSyncSeq() != OT_TIME_SYNC_INVALID_SEQ
 #endif
         )
@@ -1333,7 +1333,9 @@
         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
+    // 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))
     {
@@ -1350,7 +1352,6 @@
 exit:
     if (aRxInfo.mNeighbor && aRxInfo.mNeighbor->GetRloc16() != aSourceAddress)
     {
-        // Remove stale neighbors
         RemoveNeighbor(*aRxInfo.mNeighbor);
     }
 
@@ -1395,11 +1396,9 @@
 
     aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(extAddr);
 
-    // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
     VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
-    // Scan Mask
     SuccessOrExit(error = Tlv::Find<ScanMaskTlv>(aRxInfo.mMessage, scanMask));
 
     switch (mRole)
@@ -1419,7 +1418,6 @@
         break;
     }
 
-    // Challenge
     SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(challenge));
 
     child = mChildTable.FindChild(extAddr, Child::kInStateAnyExceptInvalid);
@@ -1428,7 +1426,6 @@
     {
         VerifyOrExit((child = mChildTable.GetNewChild()) != nullptr, error = kErrorNoBufs);
 
-        // MAC Address
         InitNeighbor(*child, aRxInfo);
         child->SetState(Neighbor::kStateParentRequest);
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
@@ -1510,6 +1507,9 @@
         mPreviousPartitionIdTimeout--;
     }
 
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Role transitions
+
     roleTransitionTimeoutExpired = mRouterRoleTransition.HandleTimeTick();
 
     switch (mRole)
@@ -1532,7 +1532,6 @@
             }
             else
             {
-                // send announce after decided to stay in REED if needed
                 InformPreviousChannel();
             }
 
@@ -1549,11 +1548,11 @@
         OT_FALL_THROUGH;
 
     case kRoleRouter:
-        LogDebg("network id timeout = %lu", ToUlong(mRouterTable.GetLeaderAge()));
+        LogDebg("Leader age %lu", ToUlong(mRouterTable.GetLeaderAge()));
 
         if ((mRouterTable.GetActiveRouterCount() > 0) && (mRouterTable.GetLeaderAge() >= mNetworkIdTimeout))
         {
-            LogInfo("Router ID Sequence timeout");
+            LogInfo("Leader age timeout");
             Attach(kSamePartition);
         }
 
@@ -1578,7 +1577,9 @@
         OT_ASSERT(false);
     }
 
-    // update children state
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Update `ChildTable`
+
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateAnyExceptInvalid))
     {
         uint32_t timeout = 0;
@@ -1605,7 +1606,7 @@
         if (child.IsCslSynchronized() &&
             TimerMilli::GetNow() - child.GetCslLastHeard() >= Time::SecToMsec(child.GetCslTimeout()))
         {
-            LogInfo("Child CSL synchronization expired");
+            LogInfo("Child 0x%04x CSL synchronization expired", child.GetRloc16());
             child.SetCslSynchronized(false);
             Get<CslTxScheduler>().Update();
         }
@@ -1613,7 +1614,7 @@
 
         if (TimerMilli::GetNow() - child.GetLastHeard() >= timeout)
         {
-            LogInfo("Child timeout expired");
+            LogInfo("Child 0x%04x timeout expired", child.GetRloc16());
             RemoveNeighbor(child);
         }
         else if (IsRouterOrLeader() && child.IsStateRestored())
@@ -1622,7 +1623,9 @@
         }
     }
 
-    // update router state
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Update `RouterTable`
+
     for (Router &router : Get<RouterTable>())
     {
         uint32_t age;
@@ -1635,53 +1638,35 @@
 
         age = TimerMilli::GetNow() - router.GetLastHeard();
 
-        if (router.IsStateValid())
+        if (router.IsStateValid() && (age >= kMaxNeighborAge))
         {
-#if OPENTHREAD_CONFIG_MLE_SEND_LINK_REQUEST_ON_ADV_TIMEOUT == 0
-
-            if (age >= kMaxNeighborAge)
+#if OPENTHREAD_CONFIG_MLE_SEND_LINK_REQUEST_ON_ADV_TIMEOUT
+            if (age < kMaxNeighborAge + kMaxTxCount * kUnicastRetxDelay)
             {
-                LogInfo("Router timeout expired");
-                RemoveNeighbor(router);
-                continue;
+                LogInfo("No Adv from router 0x%04x - sending Link Request", router.GetRloc16());
+                IgnoreError(SendLinkRequest(&router));
             }
-
-#else
-
-            if (age >= kMaxNeighborAge)
-            {
-                if (age < kMaxNeighborAge + kMaxTxCount * kUnicastRetxDelay)
-                {
-                    LogInfo("Router timeout expired");
-                    IgnoreError(SendLinkRequest(&router));
-                }
-                else
-                {
-                    RemoveNeighbor(router);
-                    continue;
-                }
-            }
-
+            else
 #endif
-        }
-        else if (router.IsStateLinkRequest())
-        {
-            if (age >= kLinkRequestTimeout)
             {
-                LogInfo("Link Request timeout expired");
+                LogInfo("Router 0x%04x timeout expired", router.GetRloc16());
                 RemoveNeighbor(router);
                 continue;
             }
         }
 
-        if (IsLeader())
+        if (router.IsStateLinkRequest() && (age >= kLinkRequestTimeout))
         {
-            if (mRouterTable.FindNextHopOf(router) == nullptr && mRouterTable.GetLinkCost(router) >= kMaxRouteCost &&
-                age >= kMaxLeaderToRouterTimeout)
-            {
-                LogInfo("Router ID timeout expired (no route)");
-                IgnoreError(mRouterTable.Release(router.GetRouterId()));
-            }
+            LogInfo("Router 0x%04x - Link Request timeout expired", router.GetRloc16());
+            RemoveNeighbor(router);
+            continue;
+        }
+
+        if (IsLeader() && (mRouterTable.FindNextHopOf(router) == nullptr) &&
+            (mRouterTable.GetLinkCost(router) >= kMaxRouteCost) && (age >= kMaxLeaderToRouterTimeout))
+        {
+            LogInfo("Router 0x%04x ID timeout expired (no route)", router.GetRloc16());
+            IgnoreError(mRouterTable.Release(router.GetRouterId()));
         }
     }
 
@@ -1727,7 +1712,6 @@
         SuccessOrExit(error = message->AppendCslClockAccuracyTlv());
     }
 #endif
-
     aChild->GenerateChallenge();
     SuccessOrExit(error = message->AppendChallengeTlv(aChild->GetChallenge()));
     SuccessOrExit(error = message->AppendLinkMarginTlv(aChild->GetLinkInfo().GetLinkMargin()));
@@ -1808,7 +1792,6 @@
 #endif
 
 #if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
-    // Retrieve registered multicast addresses of the Child
     if (aChild.HasAnyMlrRegisteredAddress())
     {
         OT_ASSERT(aChild.IsStateValid());
@@ -1872,7 +1855,8 @@
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
         if (mMaxChildIpAddresses > 0 && storedCount >= mMaxChildIpAddresses)
         {
-            // Skip remaining address registration entries but keep logging skipped addresses.
+            // Skip remaining address registration entries but keep logging
+            // skipped addresses.
             error = kErrorNoBufs;
         }
         else
@@ -1918,7 +1902,7 @@
             }
             else
             {
-                // if not able to store DUA, then assume child does not have one
+                // It cannot store DUA, then assume child does not have one.
                 hasNewDua = false;
             }
         }
@@ -2000,43 +1984,33 @@
 
     VerifyOrExit(IsRouterEligible(), error = kErrorInvalidState);
 
-    // only process message when operating as a child, router, or leader
     VerifyOrExit(IsAttached(), error = kErrorInvalidState);
 
-    // Find Child
     aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(extAddr);
 
     child = mChildTable.FindChild(extAddr, Child::kInStateAnyExceptInvalid);
     VerifyOrExit(child != nullptr, error = kErrorAlready);
 
-    // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
     VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
-    // Response
     SuccessOrExit(error = aRxInfo.mMessage.ReadResponseTlv(response));
     VerifyOrExit(response == child->GetChallenge(), error = kErrorSecurity);
 
-    // Remove existing MLE messages
     Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleGeneral);
     Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleChildIdRequest);
     Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleChildUpdateRequest);
     Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleDataResponse);
 
-    // Link-Layer and MLE Frame Counters
     SuccessOrExit(error = aRxInfo.mMessage.ReadFrameCounterTlvs(linkFrameCounter, mleFrameCounter));
 
-    // Mode
     SuccessOrExit(error = Tlv::Find<ModeTlv>(aRxInfo.mMessage, modeBitmask));
     mode.Set(modeBitmask);
 
-    // Timeout
     SuccessOrExit(error = Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout));
 
-    // Requested TLVs
     SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(tlvList));
 
-    // Supervision interval
     switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
     {
     case kErrorNone:
@@ -2049,7 +2023,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Active Timestamp
     switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
@@ -2068,7 +2041,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Pending Timestamp
     switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
@@ -2094,12 +2066,10 @@
         SuccessOrExit(error = ProcessAddressRegistrationTlv(aRxInfo, *child));
     }
 
-    // Remove from router table
     router = mRouterTable.FindRouter(extAddr);
 
     if (router != nullptr)
     {
-        // The `router` here can be invalid
         RemoveNeighbor(*router);
     }
 
@@ -2119,7 +2089,7 @@
     child->SetKeySequence(aRxInfo.mKeySequence);
     child->SetDeviceMode(mode);
     child->SetVersion(version);
-    child->GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
+    child->GetLinkInfo().AddRss(aRxInfo.mMessage.GetAverageRss());
     child->SetTimeout(timeout);
     child->SetSupervisionInterval(supervisionInterval);
 #if OPENTHREAD_CONFIG_MULTI_RADIO
@@ -2180,11 +2150,9 @@
 
     Log(kMessageReceive, kTypeChildUpdateRequestOfChild, aRxInfo.mMessageInfo.GetPeerAddr());
 
-    // Mode
     SuccessOrExit(error = Tlv::Find<ModeTlv>(aRxInfo.mMessage, modeBitmask));
     mode.Set(modeBitmask);
 
-    // Challenge
     switch (aRxInfo.mMessage.ReadChallengeTlv(challenge))
     {
     case kErrorNone:
@@ -2237,7 +2205,6 @@
         tlvList.Add(Tlv::kLinkFrameCounter);
     }
 
-    // IPv6 Address TLV
     switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
     {
     case kErrorNone:
@@ -2249,7 +2216,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Leader Data
     switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
     {
     case kErrorNone:
@@ -2261,7 +2227,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Timeout
     switch (Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout))
     {
     case kErrorNone:
@@ -2281,7 +2246,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Supervision interval
     switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
     {
     case kErrorNone:
@@ -2299,7 +2263,6 @@
 
     child->SetSupervisionInterval(supervisionInterval);
 
-    // TLV Request
     switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
     {
     case kErrorNone:
@@ -2409,7 +2372,6 @@
 
     child = static_cast<Child *>(aRxInfo.mNeighbor);
 
-    // Response
     switch (aRxInfo.mMessage.ReadResponseTlv(response))
     {
     case kErrorNone:
@@ -2425,7 +2387,6 @@
 
     Log(kMessageReceive, kTypeChildUpdateResponseOfChild, aRxInfo.mMessageInfo.GetPeerAddr(), child->GetRloc16());
 
-    // Source Address
     switch (Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress))
     {
     case kErrorNone:
@@ -2444,7 +2405,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Status
     switch (Tlv::Find<StatusTlv>(aRxInfo.mMessage, status))
     {
     case kErrorNone:
@@ -2456,8 +2416,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Link-Layer Frame Counter
-
     switch (Tlv::Find<LinkFrameCounterTlv>(aRxInfo.mMessage, linkFrameCounter))
     {
     case kErrorNone:
@@ -2470,7 +2428,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // MLE Frame Counter
     switch (Tlv::Find<MleFrameCounterTlv>(aRxInfo.mMessage, mleFrameCounter))
     {
     case kErrorNone:
@@ -2482,7 +2439,6 @@
         ExitNow(error = kErrorNone);
     }
 
-    // Timeout
     switch (Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout))
     {
     case kErrorNone:
@@ -2509,7 +2465,6 @@
         }
     }
 
-    // IPv6 Address
     switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
     {
     case kErrorNone:
@@ -2519,7 +2474,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Leader Data
     switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
     {
     case kErrorNone:
@@ -2534,7 +2488,7 @@
     SetChildStateToValid(*child);
     child->SetLastHeard(TimerMilli::GetNow());
     child->SetKeySequence(aRxInfo.mKeySequence);
-    child->GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
+    child->GetLinkInfo().AddRss(aRxInfo.mMessage.GetAverageRss());
 
     aRxInfo.mClass = response.IsEmpty() ? RxInfo::kPeerMessage : RxInfo::kAuthoritativeMessage;
 
@@ -2552,10 +2506,8 @@
 
     VerifyOrExit(aRxInfo.IsNeighborStateValid(), error = kErrorSecurity);
 
-    // TLV Request
     SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(tlvList));
 
-    // Active Timestamp
     switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
@@ -2574,7 +2526,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Pending Timestamp
     switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
@@ -2686,7 +2637,6 @@
 
     discoveryRequestTlv.SetLength(0);
 
-    // only Routers and REEDs respond
     VerifyOrExit(IsRouterEligible(), error = kErrorInvalidState);
 
     SuccessOrExit(error = Tlv::FindTlvValueStartEndOffsets(aRxInfo.mMessage, Tlv::kDiscovery, offset, end));
@@ -2768,13 +2718,11 @@
     message->SetRadioType(aDiscoverRequestMessage.GetRadioType());
 #endif
 
-    // Discovery TLV
     tlv.SetType(Tlv::kDiscovery);
     SuccessOrExit(error = message->Append(tlv));
 
     startOffset = message->GetLength();
 
-    // Discovery Response TLV
     discoveryResponseTlv.Init();
     discoveryResponseTlv.SetVersion(kThreadVersion);
 
@@ -2799,11 +2747,9 @@
 
     SuccessOrExit(error = discoveryResponseTlv.AppendTo(*message));
 
-    // Extended PAN ID TLV
     SuccessOrExit(
         error = Tlv::Append<MeshCoP::ExtendedPanIdTlv>(*message, Get<MeshCoP::ExtendedPanIdManager>().GetExtPanId()));
 
-    // Network Name TLV
     networkNameTlv.Init();
     networkNameTlv.SetNetworkName(Get<MeshCoP::NetworkNameManager>().GetNetworkName().GetAsData());
     SuccessOrExit(error = networkNameTlv.AppendTo(*message));
@@ -2844,7 +2790,7 @@
     {
         uint16_t rloc16;
 
-        // pick next Child ID that is not being used
+        // Pick next Child ID that is not being used
         do
         {
             mNextChildId++;
@@ -2858,7 +2804,6 @@
 
         } while (mChildTable.FindChild(rloc16, Child::kInStateAnyExceptInvalid) != nullptr);
 
-        // allocate Child ID
         aChild.SetRloc16(rloc16);
     }
 
@@ -2938,14 +2883,16 @@
         {
             if (msg.GetChildMask(childIndex) && msg.GetSubType() == Message::kSubTypeMleChildUpdateRequest)
             {
-                // No need to send the resync "Child Update Request" to the sleepy child
-                // if there is one already queued.
+                // No need to send the resync "Child Update Request"
+                // to the sleepy child if there is one already
+                // queued.
                 if (aChild.IsStateRestoring())
                 {
                     ExitNow();
                 }
 
-                // Remove queued outdated "Child Update Request" when there is newer Network Data is to send.
+                // Remove queued outdated "Child Update Request" when
+                // there is newer Network Data is to send.
                 Get<MeshForwarder>().RemoveMessages(aChild, Message::kSubTypeMleChildUpdateRequest);
                 break;
             }
@@ -2962,7 +2909,24 @@
     if (!aChild.IsStateValid())
     {
         SuccessOrExit(error = message->AppendTlvRequestTlv(kTlvs));
-        aChild.GenerateChallenge();
+
+        if (!aChild.IsStateRestored())
+        {
+            // A random challenge is generated and saved when `aChild`
+            // is first initialized in `kStateRestored`. We will use
+            // the saved challenge here. This prevents overwriting
+            // the saved challenge when the child is also detached
+            // and happens to send a "Parent Request" in the window
+            // where the parent transitions to the router/leader role
+            // and before the parent sends the "Child Update Request".
+            // This ensures that the same random challenge is
+            // included in both "Parent Response" and "Child Update
+            // Response," guaranteeing proper acceptance of the
+            // child's "Child ID request".
+
+            aChild.GenerateChallenge();
+        }
+
         SuccessOrExit(error = message->AppendChallengeTlv(aChild.GetChallenge()));
     }
 
@@ -3139,10 +3103,8 @@
 
     if (aDelay)
     {
-        // Remove MLE Data Responses from Send Message Queue.
         Get<MeshForwarder>().RemoveDataResponseMessages();
 
-        // Remove multicast MLE Data Response from Delayed Message Queue.
         RemoveDelayedDataResponseMessage();
 
         SuccessOrExit(error = message->SendAfterDelay(aDestination, aDelay));
@@ -3224,7 +3186,6 @@
 
         if (aNeighbor.IsFullThreadDevice())
         {
-            // Clear all EID-to-RLOC entries associated with the child.
             Get<AddressResolver>().RemoveEntriesForRloc16(aNeighbor.GetRloc16());
         }
 
@@ -3275,11 +3236,9 @@
         ExitNow();
     }
 
-    // loop exists
     router = mRouterTable.FindRouterByRloc16(aDestRloc16);
     VerifyOrExit(router != nullptr);
 
-    // invalidate next hop
     router->SetNextHopToInvalid();
     ResetAdvertiseInterval();
 
@@ -3289,46 +3248,39 @@
 
 Error MleRouter::CheckReachability(uint16_t aMeshDest, const Ip6::Header &aIp6Header)
 {
-    Error error = kErrorNone;
+    bool isReachable = false;
 
     if (IsChild())
     {
-        error = Mle::CheckReachability(aMeshDest, aIp6Header);
+        if (aMeshDest == GetRloc16())
+        {
+            isReachable = Get<ThreadNetif>().HasUnicastAddress(aIp6Header.GetDestination());
+        }
+        else
+        {
+            isReachable = true;
+        }
+
         ExitNow();
     }
 
-    if (aMeshDest == Get<Mac::Mac>().GetShortAddress())
+    if (aMeshDest == GetRloc16())
     {
-        // mesh destination is this device
-        if (Get<ThreadNetif>().HasUnicastAddress(aIp6Header.GetDestination()))
-        {
-            // IPv6 destination is this device
-            ExitNow();
-        }
-        else if (mNeighborTable.FindNeighbor(aIp6Header.GetDestination()) != nullptr)
-        {
-            // IPv6 destination is an RFD child
-            ExitNow();
-        }
-    }
-    else if (RouterIdFromRloc16(aMeshDest) == mRouterId)
-    {
-        // mesh destination is a child of this device
-        if (mChildTable.FindChild(aMeshDest, Child::kInStateValidOrRestoring))
-        {
-            ExitNow();
-        }
-    }
-    else if (GetNextHop(aMeshDest) != Mac::kShortAddrInvalid)
-    {
-        // forwarding to another router and route is known
+        isReachable = Get<ThreadNetif>().HasUnicastAddress(aIp6Header.GetDestination()) ||
+                      (mNeighborTable.FindNeighbor(aIp6Header.GetDestination()) != nullptr);
         ExitNow();
     }
 
-    error = kErrorNoRoute;
+    if (RouterIdFromRloc16(aMeshDest) == mRouterId)
+    {
+        isReachable = (mChildTable.FindChild(aMeshDest, Child::kInStateValidOrRestoring) != nullptr);
+        ExitNow();
+    }
+
+    isReachable = (GetNextHop(aMeshDest) != Mac::kShortAddrInvalid);
 
 exit:
-    return error;
+    return isReachable ? kErrorNone : kErrorNoRoute;
 }
 
 Error MleRouter::SendAddressSolicit(ThreadStatusTlv::Status aStatus)
@@ -3442,7 +3394,6 @@
     SuccessOrExit(Tlv::FindTlv(*aMessage, routerMaskTlv));
     VerifyOrExit(routerMaskTlv.IsValid());
 
-    // assign short address
     SetRouterId(routerId);
 
     SetStateRouter(Rloc16FromRouterId(mRouterId));
@@ -3505,7 +3456,6 @@
     }
 
 exit:
-    // Send announce after received address solicit reply if needed
     InformPreviousChannel();
 }
 
@@ -3566,7 +3516,6 @@
     }
 #endif
 
-    // Check if allocation already exists
     router = mRouterTable.FindRouter(extAddress);
 
     if (router != nullptr)
@@ -3658,7 +3607,10 @@
 
     // 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.
+    // with the old RLOC16 unless the sender is a direct child. For
+    // direct children, we retain the cache entries to allow
+    // association with the promoted router's new RLOC16 upon
+    // receiving its Link Advertisement.
 
     if ((aResponseStatus == ThreadStatusTlv::kSuccess) && (aRouter != nullptr))
     {
@@ -3668,6 +3620,7 @@
         oldRloc16 = aMessageInfo.GetPeerAddr().GetIid().GetLocator();
 
         VerifyOrExit(oldRloc16 != aRouter->GetRloc16());
+        VerifyOrExit(!RouterIdMatch(oldRloc16, GetRloc16()));
         Get<AddressResolver>().RemoveEntriesForRloc16(oldRloc16);
     }
 
@@ -3744,13 +3697,11 @@
     {
         if (router.GetRloc16() == GetRloc16())
         {
-            // skip self
             continue;
         }
 
         if (!router.IsStateValid())
         {
-            // skip non-neighbor routers
             continue;
         }
 
@@ -3950,20 +3901,6 @@
 }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-void MleRouter::HandleTimeSync(RxInfo &aRxInfo)
-{
-    Log(kMessageReceive, kTypeTimeSync, aRxInfo.mMessageInfo.GetPeerAddr());
-
-    VerifyOrExit(aRxInfo.IsNeighborStateValid());
-
-    aRxInfo.mClass = RxInfo::kPeerMessage;
-
-    Get<TimeSync>().HandleTimeSyncMessage(aRxInfo.mMessage);
-
-exit:
-    return;
-}
-
 Error MleRouter::SendTimeSync(void)
 {
     Error        error = kErrorNone;
diff --git a/src/core/thread/mle_router.hpp b/src/core/thread/mle_router.hpp
index 65bfe11..955674c 100644
--- a/src/core/thread/mle_router.hpp
+++ b/src/core/thread/mle_router.hpp
@@ -649,9 +649,6 @@
     void  HandleDataRequest(RxInfo &aRxInfo);
     void  HandleNetworkDataUpdateRouter(void);
     void  HandleDiscoveryRequest(RxInfo &aRxInfo);
-#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-    void HandleTimeSync(RxInfo &aRxInfo);
-#endif
 
     Error ProcessRouteTlv(const RouteTlv &aRouteTlv, RxInfo &aRxInfo);
     Error ReadAndProcessRouteTlvOnFed(RxInfo &aRxInfo, uint8_t aParentId);
@@ -666,10 +663,10 @@
                                      const Ip6::MessageInfo &aMessageInfo);
     void  SendAddressRelease(void);
     void  SendAdvertisement(void);
-    Error SendLinkAccept(const Ip6::MessageInfo &aMessageInfo,
-                         Neighbor               *aNeighbor,
-                         const TlvList          &aRequestedTlvList,
-                         const RxChallenge      &aChallenge);
+    Error SendLinkAccept(const RxInfo      &aRxInfo,
+                         Neighbor          *aNeighbor,
+                         const TlvList     &aRequestedTlvList,
+                         const RxChallenge &aChallenge);
     void  SendParentResponse(Child *aChild, const RxChallenge &aChallenge, bool aRoutersOnlyRequest);
     Error SendChildIdResponse(Child &aChild);
     Error SendChildUpdateRequest(Child &aChild);
diff --git a/src/core/thread/network_data.cpp b/src/core/thread/network_data.cpp
index 30ebe00..28675ad 100644
--- a/src/core/thread/network_data.cpp
+++ b/src/core/thread/network_data.cpp
@@ -50,6 +50,9 @@
 
 RegisterLogModule("NetworkData");
 
+//---------------------------------------------------------------------------------------------------------------------
+// NetworkData
+
 Error NetworkData::CopyNetworkData(Type aType, uint8_t *aData, uint8_t &aDataLength) const
 {
     Error              error;
@@ -402,153 +405,6 @@
     return contains;
 }
 
-void MutableNetworkData::RemoveTemporaryData(void)
-{
-    NetworkDataTlv *cur = GetTlvsStart();
-
-    while (cur < GetTlvsEnd())
-    {
-        switch (cur->GetType())
-        {
-        case NetworkDataTlv::kTypePrefix:
-        {
-            PrefixTlv *prefix = As<PrefixTlv>(cur);
-
-            RemoveTemporaryDataIn(*prefix);
-
-            if (prefix->GetSubTlvsLength() == 0)
-            {
-                RemoveTlv(cur);
-                continue;
-            }
-
-            break;
-        }
-
-        case NetworkDataTlv::kTypeService:
-        {
-            ServiceTlv *service = As<ServiceTlv>(cur);
-
-            RemoveTemporaryDataIn(*service);
-
-            if (service->GetSubTlvsLength() == 0)
-            {
-                RemoveTlv(cur);
-                continue;
-            }
-
-            break;
-        }
-
-        default:
-            // remove temporary tlv
-            if (!cur->IsStable())
-            {
-                RemoveTlv(cur);
-                continue;
-            }
-
-            break;
-        }
-
-        cur = cur->GetNext();
-    }
-}
-
-void MutableNetworkData::RemoveTemporaryDataIn(PrefixTlv &aPrefix)
-{
-    NetworkDataTlv *cur = aPrefix.GetSubTlvs();
-
-    while (cur < aPrefix.GetNext())
-    {
-        if (cur->IsStable())
-        {
-            switch (cur->GetType())
-            {
-            case NetworkDataTlv::kTypeBorderRouter:
-            {
-                BorderRouterTlv *borderRouter = As<BorderRouterTlv>(cur);
-                ContextTlv      *context      = aPrefix.FindSubTlv<ContextTlv>();
-
-                // Replace p_border_router_16
-                for (BorderRouterEntry *entry = borderRouter->GetFirstEntry(); entry <= borderRouter->GetLastEntry();
-                     entry                    = entry->GetNext())
-                {
-                    if ((entry->IsDhcp() || entry->IsConfigure()) && (context != nullptr))
-                    {
-                        entry->SetRloc(0xfc00 | context->GetContextId());
-                    }
-                    else
-                    {
-                        entry->SetRloc(0xfffe);
-                    }
-                }
-
-                break;
-            }
-
-            case NetworkDataTlv::kTypeHasRoute:
-            {
-                HasRouteTlv *hasRoute = As<HasRouteTlv>(cur);
-
-                // Replace r_border_router_16
-                for (HasRouteEntry *entry = hasRoute->GetFirstEntry(); entry <= hasRoute->GetLastEntry();
-                     entry                = entry->GetNext())
-                {
-                    entry->SetRloc(0xfffe);
-                }
-
-                break;
-            }
-
-            default:
-                break;
-            }
-
-            // keep stable tlv
-            cur = cur->GetNext();
-        }
-        else
-        {
-            // remove temporary tlv
-            uint8_t subTlvSize = cur->GetSize();
-            RemoveTlv(cur);
-            aPrefix.SetSubTlvsLength(aPrefix.GetSubTlvsLength() - subTlvSize);
-        }
-    }
-}
-
-void MutableNetworkData::RemoveTemporaryDataIn(ServiceTlv &aService)
-{
-    NetworkDataTlv *cur = aService.GetSubTlvs();
-
-    while (cur < aService.GetNext())
-    {
-        if (cur->IsStable())
-        {
-            switch (cur->GetType())
-            {
-            case NetworkDataTlv::kTypeServer:
-                As<ServerTlv>(cur)->SetServer16(Mle::ServiceAlocFromId(aService.GetServiceId()));
-                break;
-
-            default:
-                break;
-            }
-
-            // keep stable tlv
-            cur = cur->GetNext();
-        }
-        else
-        {
-            // remove temporary tlv
-            uint8_t subTlvSize = cur->GetSize();
-            RemoveTlv(cur);
-            aService.SetSubTlvsLength(aService.GetSubTlvsLength() - subTlvSize);
-        }
-    }
-}
-
 const PrefixTlv *NetworkData::FindPrefix(const uint8_t *aPrefix, uint8_t aPrefixLength) const
 {
     TlvIterator      tlvIterator(mTlvs, mLength);
@@ -639,6 +495,251 @@
     return match;
 }
 
+void NetworkData::FindRlocs(BorderRouterFilter aBrFilter, RoleFilter aRoleFilter, Rlocs &aRlocs) const
+{
+    Iterator            iterator = kIteratorInit;
+    OnMeshPrefixConfig  prefix;
+    ExternalRouteConfig route;
+    ServiceConfig       service;
+    Config              config;
+
+    aRlocs.Clear();
+
+    while (true)
+    {
+        config.mOnMeshPrefix  = &prefix;
+        config.mExternalRoute = &route;
+        config.mService       = &service;
+        config.mLowpanContext = nullptr;
+
+        SuccessOrExit(Iterate(iterator, Mac::kShortAddrBroadcast, config));
+
+        if (config.mOnMeshPrefix != nullptr)
+        {
+            bool matches = true;
+
+            switch (aBrFilter)
+            {
+            case kAnyBrOrServer:
+                break;
+            case kBrProvidingExternalIpConn:
+                matches = prefix.mOnMesh && (prefix.mDefaultRoute || prefix.mDp);
+                break;
+            }
+
+            if (matches)
+            {
+                AddRloc16ToRlocs(prefix.mRloc16, aRlocs, aRoleFilter);
+            }
+        }
+        else if (config.mExternalRoute != nullptr)
+        {
+            AddRloc16ToRlocs(route.mRloc16, aRlocs, aRoleFilter);
+        }
+        else if (config.mService != nullptr)
+        {
+            switch (aBrFilter)
+            {
+            case kAnyBrOrServer:
+                AddRloc16ToRlocs(service.mServerConfig.mRloc16, aRlocs, aRoleFilter);
+                break;
+            case kBrProvidingExternalIpConn:
+                break;
+            }
+        }
+    }
+
+exit:
+    return;
+}
+
+uint8_t NetworkData::CountBorderRouters(RoleFilter aRoleFilter) const
+{
+    Rlocs rlocs;
+
+    FindRlocs(kBrProvidingExternalIpConn, aRoleFilter, rlocs);
+
+    return rlocs.GetLength();
+}
+
+bool NetworkData::ContainsBorderRouterWithRloc(uint16_t aRloc16) const
+{
+    Rlocs rlocs;
+
+    FindRlocs(kBrProvidingExternalIpConn, kAnyRole, rlocs);
+
+    return rlocs.Contains(aRloc16);
+}
+
+void NetworkData::AddRloc16ToRlocs(uint16_t aRloc16, Rlocs &aRlocs, RoleFilter aRoleFilter)
+{
+    switch (aRoleFilter)
+    {
+    case kAnyRole:
+        break;
+
+    case kRouterRoleOnly:
+        VerifyOrExit(Mle::IsActiveRouter(aRloc16));
+        break;
+
+    case kChildRoleOnly:
+        VerifyOrExit(!Mle::IsActiveRouter(aRloc16));
+        break;
+    }
+
+    VerifyOrExit(!aRlocs.Contains(aRloc16));
+    IgnoreError(aRlocs.PushBack(aRloc16));
+
+exit:
+    return;
+}
+
+Error NetworkData::FindDomainIdFor(const Ip6::Prefix &aPrefix, uint8_t &aDomainId) const
+{
+    Error            error     = kErrorNone;
+    const PrefixTlv *prefixTlv = FindPrefix(aPrefix);
+
+    VerifyOrExit(prefixTlv != nullptr, error = kErrorNotFound);
+    aDomainId = prefixTlv->GetDomainId();
+
+exit:
+    return error;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// MutableNetworkData
+
+void MutableNetworkData::RemoveTemporaryData(void)
+{
+    NetworkDataTlv *cur = GetTlvsStart();
+
+    while (cur < GetTlvsEnd())
+    {
+        bool shouldRemove = false;
+
+        switch (cur->GetType())
+        {
+        case NetworkDataTlv::kTypePrefix:
+            shouldRemove = RemoveTemporaryDataIn(*As<PrefixTlv>(cur));
+            break;
+
+        case NetworkDataTlv::kTypeService:
+            shouldRemove = RemoveTemporaryDataIn(*As<ServiceTlv>(cur));
+            break;
+
+        default:
+            shouldRemove = !cur->IsStable();
+            break;
+        }
+
+        if (shouldRemove)
+        {
+            RemoveTlv(cur);
+            continue;
+        }
+
+        cur = cur->GetNext();
+    }
+}
+
+bool MutableNetworkData::RemoveTemporaryDataIn(PrefixTlv &aPrefix)
+{
+    NetworkDataTlv *cur = aPrefix.GetSubTlvs();
+
+    while (cur < aPrefix.GetNext())
+    {
+        if (cur->IsStable())
+        {
+            switch (cur->GetType())
+            {
+            case NetworkDataTlv::kTypeBorderRouter:
+            {
+                BorderRouterTlv *borderRouter = As<BorderRouterTlv>(cur);
+                ContextTlv      *context      = aPrefix.FindSubTlv<ContextTlv>();
+
+                // Replace p_border_router_16
+                for (BorderRouterEntry *entry = borderRouter->GetFirstEntry(); entry <= borderRouter->GetLastEntry();
+                     entry                    = entry->GetNext())
+                {
+                    if ((entry->IsDhcp() || entry->IsConfigure()) && (context != nullptr))
+                    {
+                        entry->SetRloc(0xfc00 | context->GetContextId());
+                    }
+                    else
+                    {
+                        entry->SetRloc(0xfffe);
+                    }
+                }
+
+                break;
+            }
+
+            case NetworkDataTlv::kTypeHasRoute:
+            {
+                HasRouteTlv *hasRoute = As<HasRouteTlv>(cur);
+
+                // Replace r_border_router_16
+                for (HasRouteEntry *entry = hasRoute->GetFirstEntry(); entry <= hasRoute->GetLastEntry();
+                     entry                = entry->GetNext())
+                {
+                    entry->SetRloc(0xfffe);
+                }
+
+                break;
+            }
+
+            default:
+                break;
+            }
+
+            // keep stable tlv
+            cur = cur->GetNext();
+        }
+        else
+        {
+            // remove temporary tlv
+            uint8_t subTlvSize = cur->GetSize();
+            RemoveTlv(cur);
+            aPrefix.SetSubTlvsLength(aPrefix.GetSubTlvsLength() - subTlvSize);
+        }
+    }
+
+    return (aPrefix.GetSubTlvsLength() == 0);
+}
+
+bool MutableNetworkData::RemoveTemporaryDataIn(ServiceTlv &aService)
+{
+    NetworkDataTlv *cur = aService.GetSubTlvs();
+
+    while (cur < aService.GetNext())
+    {
+        if (cur->IsStable())
+        {
+            switch (cur->GetType())
+            {
+            case NetworkDataTlv::kTypeServer:
+                As<ServerTlv>(cur)->SetServer16(Mle::ServiceAlocFromId(aService.GetServiceId()));
+                break;
+
+            default:
+                break;
+            }
+
+            // keep stable tlv
+            cur = cur->GetNext();
+        }
+        else
+        {
+            // remove temporary tlv
+            uint8_t subTlvSize = cur->GetSize();
+            RemoveTlv(cur);
+            aService.SetSubTlvsLength(aService.GetSubTlvsLength() - subTlvSize);
+        }
+    }
+
+    return (aService.GetSubTlvsLength() == 0);
+}
+
 NetworkDataTlv *MutableNetworkData::AppendTlv(uint16_t aTlvSize)
 {
     NetworkDataTlv *tlv;
@@ -675,190 +776,5 @@
 
 void MutableNetworkData::RemoveTlv(NetworkDataTlv *aTlv) { Remove(aTlv, aTlv->GetSize()); }
 
-Error NetworkData::GetNextServer(Iterator &aIterator, uint16_t &aRloc16) const
-{
-    Error               error;
-    OnMeshPrefixConfig  prefixConfig;
-    ExternalRouteConfig routeConfig;
-    ServiceConfig       serviceConfig;
-    Config              config;
-
-    config.mOnMeshPrefix  = &prefixConfig;
-    config.mExternalRoute = &routeConfig;
-    config.mService       = &serviceConfig;
-    config.mLowpanContext = nullptr;
-
-    SuccessOrExit(error = Iterate(aIterator, Mac::kShortAddrBroadcast, config));
-
-    if (config.mOnMeshPrefix != nullptr)
-    {
-        aRloc16 = config.mOnMeshPrefix->mRloc16;
-    }
-    else if (config.mExternalRoute != nullptr)
-    {
-        aRloc16 = config.mExternalRoute->mRloc16;
-    }
-    else if (config.mService != nullptr)
-    {
-        aRloc16 = config.mService->mServerConfig.mRloc16;
-    }
-    else
-    {
-        OT_ASSERT(false);
-    }
-
-exit:
-    return error;
-}
-
-Error NetworkData::FindBorderRouters(RoleFilter aRoleFilter, uint16_t aRlocs[], uint8_t &aRlocsLength) const
-{
-    class Rlocs // Wrapper over an array of RLOC16s.
-    {
-    public:
-        Rlocs(RoleFilter aRoleFilter, uint16_t *aRlocs, uint8_t aRlocsMaxLength)
-            : mRoleFilter(aRoleFilter)
-            , mRlocs(aRlocs)
-            , mLength(0)
-            , mMaxLength(aRlocsMaxLength)
-        {
-        }
-
-        uint8_t GetLength(void) const { return mLength; }
-
-        Error AddRloc16(uint16_t aRloc16)
-        {
-            // Add `aRloc16` into the array if it matches `RoleFilter` and
-            // it is not in the array already. If we need to add the `aRloc16`
-            // but there is no more room in the array, return `kErrorNoBufs`.
-
-            Error   error = kErrorNone;
-            uint8_t index;
-
-            switch (mRoleFilter)
-            {
-            case kAnyRole:
-                break;
-
-            case kRouterRoleOnly:
-                VerifyOrExit(Mle::IsActiveRouter(aRloc16));
-                break;
-
-            case kChildRoleOnly:
-                VerifyOrExit(!Mle::IsActiveRouter(aRloc16));
-                break;
-            }
-
-            for (index = 0; index < mLength; index++)
-            {
-                if (mRlocs[index] == aRloc16)
-                {
-                    break;
-                }
-            }
-
-            if (index == mLength)
-            {
-                VerifyOrExit(mLength < mMaxLength, error = kErrorNoBufs);
-                mRlocs[mLength++] = aRloc16;
-            }
-
-        exit:
-            return error;
-        }
-
-    private:
-        RoleFilter mRoleFilter;
-        uint16_t  *mRlocs;
-        uint8_t    mLength;
-        uint8_t    mMaxLength;
-    };
-
-    Error               error = kErrorNone;
-    Rlocs               rlocs(aRoleFilter, aRlocs, aRlocsLength);
-    Iterator            iterator = kIteratorInit;
-    ExternalRouteConfig route;
-    OnMeshPrefixConfig  prefix;
-
-    while (GetNextExternalRoute(iterator, route) == kErrorNone)
-    {
-        SuccessOrExit(error = rlocs.AddRloc16(route.mRloc16));
-    }
-
-    iterator = kIteratorInit;
-
-    while (GetNextOnMeshPrefix(iterator, prefix) == kErrorNone)
-    {
-        if (!prefix.mDefaultRoute || !prefix.mOnMesh)
-        {
-            continue;
-        }
-
-        SuccessOrExit(error = rlocs.AddRloc16(prefix.mRloc16));
-    }
-
-exit:
-    aRlocsLength = rlocs.GetLength();
-    return error;
-}
-
-uint8_t NetworkData::CountBorderRouters(RoleFilter aRoleFilter) const
-{
-    // We use an over-estimate of max number of border routers in the
-    // Network Data using the facts that network data is limited to 254
-    // bytes and that an external route entry uses at minimum 3 bytes
-    // for RLOC16 and flag, so `ceil(254/3) = 85`.
-
-    static constexpr uint16_t kMaxRlocs = 85;
-
-    uint16_t rlocs[kMaxRlocs];
-    uint8_t  rlocsLength = kMaxRlocs;
-
-    SuccessOrAssert(FindBorderRouters(aRoleFilter, rlocs, rlocsLength));
-
-    return rlocsLength;
-}
-
-bool NetworkData::ContainsBorderRouterWithRloc(uint16_t aRloc16) const
-{
-    bool                contains = false;
-    Iterator            iterator = kIteratorInit;
-    ExternalRouteConfig route;
-    OnMeshPrefixConfig  prefix;
-
-    while (GetNextExternalRoute(iterator, route) == kErrorNone)
-    {
-        if (route.mRloc16 == aRloc16)
-        {
-            ExitNow(contains = true);
-        }
-    }
-
-    iterator = kIteratorInit;
-
-    while (GetNextOnMeshPrefix(iterator, prefix) == kErrorNone)
-    {
-        if ((prefix.mRloc16 == aRloc16) && prefix.mOnMesh && (prefix.mDefaultRoute || prefix.mDp))
-        {
-            ExitNow(contains = true);
-        }
-    }
-
-exit:
-    return contains;
-}
-
-Error NetworkData::FindDomainIdFor(const Ip6::Prefix &aPrefix, uint8_t &aDomainId) const
-{
-    Error            error     = kErrorNone;
-    const PrefixTlv *prefixTlv = FindPrefix(aPrefix);
-
-    VerifyOrExit(prefixTlv != nullptr, error = kErrorNotFound);
-    aDomainId = prefixTlv->GetDomainId();
-
-exit:
-    return error;
-}
-
 } // namespace NetworkData
 } // namespace ot
diff --git a/src/core/thread/network_data.hpp b/src/core/thread/network_data.hpp
index 17c5151..d1d8270 100644
--- a/src/core/thread/network_data.hpp
+++ b/src/core/thread/network_data.hpp
@@ -325,18 +325,6 @@
     bool ContainsEntriesFrom(const NetworkData &aCompare, uint16_t aRloc16) const;
 
     /**
-     * Provides the next server RLOC16 in the Thread Network Data.
-     *
-     * @param[in,out]  aIterator  A reference to the Network Data iterator.
-     * @param[out]     aRloc16    The RLOC16 value.
-     *
-     * @retval kErrorNone       Successfully found the next server.
-     * @retval kErrorNotFound   No subsequent server exists in the Thread Network Data.
-     *
-     */
-    Error GetNextServer(Iterator &aIterator, uint16_t &aRloc16) const;
-
-    /**
      * Finds and returns Domain ID associated with a given prefix in the Thread Network data.
      *
      * @param[in]  aPrefix     The prefix to search for.
@@ -349,30 +337,30 @@
     Error FindDomainIdFor(const Ip6::Prefix &aPrefix, uint8_t &aDomainId) const;
 
     /**
-     * Finds and returns the list of RLOCs of border routers providing external IP connectivity.
+     * Finds border routers and servers in the Network Data matching specified filters, returning their RLOC16s.
      *
-     * A border router is considered to provide external IP connectivity if it has added at least one external route
-     * entry, or an on-mesh prefix with default-route and on-mesh flags set.
+     * @p aBrFilter can be used to filter the type of BRs. It can be set to `kAnyBrOrServer` to include all BRs and
+     * servers. `kBrProvidingExternalIpConn` restricts it to BRs providing external IP connectivity where at least one
+     * the below conditions hold:
+     *
+     * - It has added at least one external route entry.
+     * - It has added at least one prefix entry with default-route and on-mesh flags set.
+     * - It has added at least one domain prefix (domain and on-mesh flags set).
      *
      * Should be used when the RLOC16s are present in the Network Data (when the Network Data contains the
      * full set and not the stable subset).
      *
-     * @param[in]      aRoleFilter   Indicates which devices to include (any role, router role only, or child only).
-     * @param[out]     aRlocs        Array to output the list of RLOCs.
-     * @param[in,out]  aRlocsLength  On entry, @p aRlocs array length (max number of elements).
-     *                               On exit, number RLOC16 entries added in @p aRlocs.
-     *
-     * @retval kErrorNone     Successfully found all RLOC16s and updated @p aRlocs and @p aRlocsLength.
-     * @retval kErrorNoBufs   Ran out of space in @p aRlocs array. @p aRlocs and @p aRlocsLength are still updated up
-     *                        to the maximum array length.
+     * @param[in]  aBrFilter    Indicates BR filter.
+     * @param[in]  aRoleFilter  Indicates role filter (any role, router role only, or child only).
+     * @param[out] aRlocs       Array to output the list of RLOC16s.
      *
      */
-    Error FindBorderRouters(RoleFilter aRoleFilter, uint16_t aRlocs[], uint8_t &aRlocsLength) const;
+    void FindRlocs(BorderRouterFilter aBrFilter, RoleFilter aRoleFilter, Rlocs &aRlocs) const;
 
     /**
      * Counts the number of border routers providing external IP connectivity.
      *
-     * A border router is considered to provide external IP connectivity if at least one of the below conditions hold
+     * A border router is considered to provide external IP connectivity if at least one of the below conditions hold:
      *
      * - It has added at least one external route entry.
      * - It has added at least one prefix entry with default-route and on-mesh flags set.
@@ -591,6 +579,8 @@
                              const ServiceData &aServiceData,
                              ServiceMatchMode   aServiceMatchMode);
 
+    static void AddRloc16ToRlocs(uint16_t aRloc16, Rlocs &aRlocs, RoleFilter aRoleFilter);
+
     const uint8_t *mTlvs;
     uint8_t        mLength;
 };
@@ -779,8 +769,8 @@
     void RemoveTemporaryData(void);
 
 private:
-    void RemoveTemporaryDataIn(PrefixTlv &aPrefix);
-    void RemoveTemporaryDataIn(ServiceTlv &aService);
+    bool RemoveTemporaryDataIn(PrefixTlv &aPrefix);
+    bool RemoveTemporaryDataIn(ServiceTlv &aService);
 
     uint8_t mSize;
 };
diff --git a/src/core/thread/network_data_leader_ftd.cpp b/src/core/thread/network_data_leader_ftd.cpp
index fe423ef..5cfc44e 100644
--- a/src/core/thread/network_data_leader_ftd.cpp
+++ b/src/core/thread/network_data_leader_ftd.cpp
@@ -680,10 +680,7 @@
     if (!mIsClone)
 #endif
     {
-        if (error != kErrorNone)
-        {
-            LogNote("Failed to register network data: %s", ErrorToString(error));
-        }
+        LogWarnOnError(error, "register network data");
     }
 }
 
@@ -1218,10 +1215,10 @@
 {
     const PrefixTlv *prefix;
     TlvIterator      tlvIterator(GetTlvsStart(), GetTlvsEnd());
-    Iterator         iterator = kIteratorInit;
     ChangedFlags     flags;
     uint16_t         rloc16;
     uint16_t         sessionId;
+    Rlocs            rlocs;
 
     mWaitingForNetDataSync = false;
 
@@ -1232,16 +1229,13 @@
     // got the chance to send the updated Network Data to other
     // routers.
 
-    while (GetNextServer(iterator, rloc16) == kErrorNone)
-    {
-        if (!Get<RouterTable>().IsAllocated(Mle::RouterIdFromRloc16(rloc16)))
-        {
-            // After we `RemoveRloc()` the Network Data gets changed
-            // and the `iterator` will not be valid anymore. So we set
-            // it to `kIteratorInit` to restart the loop.
+    FindRlocs(kAnyBrOrServer, kAnyRole, rlocs);
 
-            RemoveRloc(rloc16, kMatchModeRouterId, flags);
-            iterator = kIteratorInit;
+    for (uint16_t rloc : rlocs)
+    {
+        if (!Get<RouterTable>().IsAllocated(Mle::RouterIdFromRloc16(rloc)))
+        {
+            RemoveRloc(rloc, kMatchModeRouterId, flags);
         }
     }
 
diff --git a/src/core/thread/network_data_notifier.cpp b/src/core/thread/network_data_notifier.cpp
index 750b379..e451a73 100644
--- a/src/core/thread/network_data_notifier.cpp
+++ b/src/core/thread/network_data_notifier.cpp
@@ -129,13 +129,14 @@
     // - `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;
+    Error error = kErrorNotFound;
+    Rlocs rlocs;
 
     VerifyOrExit(Get<Mle::MleRouter>().IsRouterOrLeader());
 
-    while (Get<Leader>().GetNextServer(iterator, rloc16) == kErrorNone)
+    Get<Leader>().FindRlocs(kAnyBrOrServer, kAnyRole, rlocs);
+
+    for (uint16_t rloc16 : rlocs)
     {
         if (!Mle::IsActiveRouter(rloc16) && Mle::RouterIdMatch(Get<Mle::MleRouter>().GetRloc16(), rloc16) &&
             Get<ChildTable>().FindChild(rloc16, Child::kInStateValid) == nullptr)
diff --git a/src/core/thread/network_data_publisher.cpp b/src/core/thread/network_data_publisher.cpp
index ec925b8..a64f0c3 100644
--- a/src/core/thread/network_data_publisher.cpp
+++ b/src/core/thread/network_data_publisher.cpp
@@ -661,16 +661,17 @@
     case kTypeUnicastMeshLocalEid:
     {
         Service::DnsSrpAnycast::Info anycastInfo;
+        bool                         hasServiceDataEntry;
 
-        CountUnicastEntries(numEntries, numPreferredEntries);
+        CountServerDataUnicastEntries(numEntries, numPreferredEntries, hasServiceDataEntry);
         desiredNumEntries = kDesiredNumUnicast;
 
-        if (Get<Service::Manager>().FindPreferredDnsSrpAnycastInfo(anycastInfo) == kErrorNone)
+        if (hasServiceDataEntry || (Get<Service::Manager>().FindPreferredDnsSrpAnycastInfo(anycastInfo) == kErrorNone))
         {
-            // If there is any anycast entry in netdata, we set the
-            // desired number of unicast entries (with address added
-            // in server TLV) to zero to remove any added unicast
-            // entry.
+            // If there is any service data unicast entry or anycast
+            // entry, we set the desired number of server data
+            // unicast entries to zero to remove any such previously
+            // added unicast entry.
 
             desiredNumEntries = 0;
         }
@@ -680,7 +681,7 @@
 
     case kTypeUnicast:
         desiredNumEntries = kDesiredNumUnicast;
-        CountUnicastEntries(numEntries, numPreferredEntries);
+        CountServiceDataUnicastEntries(numEntries, numPreferredEntries);
         break;
     }
 
@@ -721,9 +722,53 @@
     }
 }
 
-void Publisher::DnsSrpServiceEntry::CountUnicastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const
+void Publisher::DnsSrpServiceEntry::CountServerDataUnicastEntries(uint8_t &aNumEntries,
+                                                                  uint8_t &aNumPreferredEntries,
+                                                                  bool    &aHasServiceDataEntry) const
 {
-    // Count the number of "DNS/SRP Unicast" service entries in
+    // Count the number of server data DNS/SRP unicast entries in the
+    // Network Data. Also determine whether there is any service data
+    // DNS/SRP unicast entry (update `aHasServiceDataEntry`).
+
+    const ServiceTlv *serviceTlv = nullptr;
+    ServiceData       data;
+
+    aHasServiceDataEntry = false;
+
+    data.InitFrom(Service::DnsSrpUnicast::kServiceData);
+
+    while ((serviceTlv = Get<Leader>().FindNextThreadService(serviceTlv, data, NetworkData::kServicePrefixMatch)) !=
+           nullptr)
+    {
+        TlvIterator      subTlvIterator(*serviceTlv);
+        const ServerTlv *serverSubTlv;
+
+        if (serviceTlv->GetServiceDataLength() >= sizeof(Service::DnsSrpUnicast::ServiceData))
+        {
+            aHasServiceDataEntry = true;
+        }
+
+        while (((serverSubTlv = subTlvIterator.Iterate<ServerTlv>())) != nullptr)
+        {
+            if (serverSubTlv->GetServerDataLength() < sizeof(Service::DnsSrpUnicast::ServerData))
+            {
+                continue;
+            }
+
+            aNumEntries++;
+
+            if (IsPreferred(serverSubTlv->GetServer16()))
+            {
+                aNumPreferredEntries++;
+            }
+        }
+    }
+}
+
+void Publisher::DnsSrpServiceEntry::CountServiceDataUnicastEntries(uint8_t &aNumEntries,
+                                                                   uint8_t &aNumPreferredEntries) const
+{
+    // Count the number of service data DNS/SRP unicast entries in
     // the Network Data.
 
     const ServiceTlv *serviceTlv = nullptr;
@@ -737,45 +782,18 @@
         TlvIterator      subTlvIterator(*serviceTlv);
         const ServerTlv *serverSubTlv;
 
+        if (serviceTlv->GetServiceDataLength() < sizeof(Service::DnsSrpUnicast::ServiceData))
+        {
+            continue;
+        }
+
         while (((serverSubTlv = subTlvIterator.Iterate<ServerTlv>())) != nullptr)
         {
-            if (serviceTlv->GetServiceDataLength() >= sizeof(Service::DnsSrpUnicast::ServiceData))
+            aNumEntries++;
+
+            if (IsPreferred(serverSubTlv->GetServer16()))
             {
-                aNumEntries++;
-
-                // Generally, we prefer entries where the SRP/DNS server
-                // address/port info is included in the service TLV data
-                // over the ones where the info is included in the
-                // server TLV data (i.e., we prefer infra-provided
-                // SRP/DNS entry over a BR local one using ML-EID). If
-                // our entry itself uses the service TLV data, then we
-                // prefer based on the associated RLOC16.
-
-                if (GetType() == kTypeUnicast)
-                {
-                    if (IsPreferred(serverSubTlv->GetServer16()))
-                    {
-                        aNumPreferredEntries++;
-                    }
-                }
-                else
-                {
-                    aNumPreferredEntries++;
-                }
-            }
-
-            if (serverSubTlv->GetServerDataLength() >= sizeof(Service::DnsSrpUnicast::ServerData))
-            {
-                aNumEntries++;
-
-                // If our entry also uses the server TLV data (with
-                // ML-EID address), then the we prefer based on the
-                // associated RLOC16.
-
-                if ((GetType() == kTypeUnicastMeshLocalEid) && IsPreferred(serverSubTlv->GetServer16()))
-                {
-                    aNumPreferredEntries++;
-                }
+                aNumPreferredEntries++;
             }
         }
     }
diff --git a/src/core/thread/network_data_publisher.hpp b/src/core/thread/network_data_publisher.hpp
index 3ac2c82..4503efb 100644
--- a/src/core/thread/network_data_publisher.hpp
+++ b/src/core/thread/network_data_publisher.hpp
@@ -437,7 +437,10 @@
         void Notify(Event aEvent) const;
         void Process(void);
         void CountAnycastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const;
-        void CountUnicastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const;
+        void CountServiceDataUnicastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const;
+        void CountServerDataUnicastEntries(uint8_t &aNumEntries,
+                                           uint8_t &aNumPreferredEntries,
+                                           bool    &aHasServiceDataEntry) const;
 
         Info                            mInfo;
         Callback<DnsSrpServiceCallback> mCallback;
diff --git a/src/core/thread/network_data_types.hpp b/src/core/thread/network_data_types.hpp
index 1457aa2..af63ff5 100644
--- a/src/core/thread/network_data_types.hpp
+++ b/src/core/thread/network_data_types.hpp
@@ -38,6 +38,7 @@
 
 #include <openthread/netdata.h>
 
+#include "common/array.hpp"
 #include "common/as_core_type.hpp"
 #include "common/clearable.hpp"
 #include "common/data.hpp"
@@ -108,6 +109,38 @@
 };
 
 /**
+ * Represents the entry filter used when searching for RLOC16 of border routers or servers in the Network Data.
+ *
+ * Regarding `kBrProvidingExternalIpConn`, a border router is considered to provide external IP connectivity if at
+ * least one of the below conditions hold:
+ *
+ * - It has added at least one external route entry.
+ * - It has added at least one prefix entry with default-route and on-mesh flags set.
+ * - It has added at least one domain prefix (domain and on-mesh flags set).
+ *
+ */
+enum BorderRouterFilter : uint8_t
+{
+    kAnyBrOrServer,             ///< Include any border router or server entry.
+    kBrProvidingExternalIpConn, ///< Include border routers providing external IP connectivity.
+};
+
+/**
+ * Maximum length of `Rlocs` array containing RLOC16 of all border routers and servers in the Network Data.
+ *
+ * This limit is derived from the maximum Network Data size (254 bytes) and the minimum size of an external route entry
+ * (3 bytes including the RLOC16 and flags) as `ceil(254/3) = 85`.
+ *
+ */
+static constexpr uint8_t kMaxRlocs = 85;
+
+/**
+ * An array containing RLOC16 of all border routers and server in the Network Data.
+ *
+ */
+typedef Array<uint16_t, kMaxRlocs> Rlocs;
+
+/**
  * Indicates whether a given `int8_t` preference value is a valid route preference (i.e., one of the
  * values from `RoutePreference` enumeration).
  *
diff --git a/src/core/thread/network_diagnostic.cpp b/src/core/thread/network_diagnostic.cpp
index 43c1ea7..f32e781 100644
--- a/src/core/thread/network_diagnostic.cpp
+++ b/src/core/thread/network_diagnostic.cpp
@@ -61,6 +61,7 @@
 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;
+const char Server::kVendorAppUrl[]    = OPENTHREAD_CONFIG_NET_DIAG_VENDOR_APP_URL;
 
 //---------------------------------------------------------------------------------------------------------------------
 // Server
@@ -71,11 +72,13 @@
     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");
+    static_assert(sizeof(kVendorAppUrl) <= sizeof(VendorAppUrlTlv::StringType), "VENDOR_APP_URL 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));
+    memcpy(mVendorAppUrl, kVendorAppUrl, sizeof(kVendorAppUrl));
 #endif
 }
 
@@ -96,6 +99,11 @@
     return StringCopy(mVendorSwVersion, aVendorSwVersion, kStringCheckUtf8Encoding);
 }
 
+Error Server::SetVendorAppUrl(const char *aVendorAppUrl)
+{
+    return StringCopy(mVendorAppUrl, aVendorAppUrl, kStringCheckUtf8Encoding);
+}
+
 #endif
 
 void Server::PrepareMessageInfoForDest(const Ip6::Address &aDestination, Tmf::MessageInfo &aMessageInfo) const
@@ -329,6 +337,10 @@
         error = Tlv::Append<VendorSwVersionTlv>(aMessage, GetVendorSwVersion());
         break;
 
+    case Tlv::kVendorAppUrl:
+        error = Tlv::Append<VendorAppUrlTlv>(aMessage, GetVendorAppUrl());
+        break;
+
     case Tlv::kThreadStackVersion:
         error = Tlv::Append<ThreadStackVersionTlv>(aMessage, otGetVersionString());
         break;
@@ -1225,6 +1237,10 @@
             SuccessOrExit(error = Tlv::Read<VendorSwVersionTlv>(aMessage, offset, aTlvInfo.mData.mVendorSwVersion));
             break;
 
+        case Tlv::kVendorAppUrl:
+            SuccessOrExit(error = Tlv::Read<VendorAppUrlTlv>(aMessage, offset, aTlvInfo.mData.mVendorAppUrl));
+            break;
+
         case Tlv::kThreadStackVersion:
             SuccessOrExit(error =
                               Tlv::Read<ThreadStackVersionTlv>(aMessage, offset, aTlvInfo.mData.mThreadStackVersion));
diff --git a/src/core/thread/network_diagnostic.hpp b/src/core/thread/network_diagnostic.hpp
index de16b7c..fb9d2b8 100644
--- a/src/core/thread/network_diagnostic.hpp
+++ b/src/core/thread/network_diagnostic.hpp
@@ -141,10 +141,30 @@
      */
     Error SetVendorSwVersion(const char *aVendorSwVersion);
 
+    /**
+     * Returns the vendor app URL string.
+     *
+     * @returns the vendor app URL string.
+     *
+     */
+    const char *GetVendorAppUrl(void) const { return mVendorAppUrl; }
+
+    /**
+     * Sets the vendor app URL string.
+     *
+     * @param[in] aVendorAppUrl     The vendor app URL string
+     *
+     * @retval kErrorNone         Successfully set the vendor app URL.
+     * @retval kErrorInvalidArgs  @p aVendorAppUrl is not valid (too long or not UTF8).
+     *
+     */
+    Error SetVendorAppUrl(const char *aVendorAppUrl);
+
 #else
     const char *GetVendorName(void) const { return kVendorName; }
     const char *GetVendorModel(void) const { return kVendorModel; }
     const char *GetVendorSwVersion(void) const { return kVendorSwVersion; }
+    const char *GetVendorAppUrl(void) const { return kVendorAppUrl; }
 #endif // OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
 
 private:
@@ -173,6 +193,7 @@
     static const char kVendorName[];
     static const char kVendorModel[];
     static const char kVendorSwVersion[];
+    static const char kVendorAppUrl[];
 
     Error AppendDiagTlv(uint8_t aTlvType, Message &aMessage);
     Error AppendIp6AddressList(Message &aMessage);
@@ -211,6 +232,7 @@
     VendorNameTlv::StringType      mVendorName;
     VendorModelTlv::StringType     mVendorModel;
     VendorSwVersionTlv::StringType mVendorSwVersion;
+    VendorAppUrlTlv::StringType    mVendorAppUrl;
 #endif
 
 #if OPENTHREAD_FTD
diff --git a/src/core/thread/network_diagnostic_tlvs.hpp b/src/core/thread/network_diagnostic_tlvs.hpp
index 2efa744..acbaf5c 100644
--- a/src/core/thread/network_diagnostic_tlvs.hpp
+++ b/src/core/thread/network_diagnostic_tlvs.hpp
@@ -96,6 +96,7 @@
         kAnswer              = OT_NETWORK_DIAGNOSTIC_TLV_ANSWER,
         kQueryId             = OT_NETWORK_DIAGNOSTIC_TLV_QUERY_ID,
         kMleCounters         = OT_NETWORK_DIAGNOSTIC_TLV_MLE_COUNTERS,
+        kVendorAppUrl        = OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_APP_URL,
     };
 
     /**
@@ -123,6 +124,12 @@
     static constexpr uint8_t kMaxThreadStackVersionLength = OT_NETWORK_DIAGNOSTIC_MAX_THREAD_STACK_VERSION_TLV_LENGTH;
 
     /**
+     * Maximum length of Vendor SW Version TLV.
+     *
+     */
+    static constexpr uint8_t kMaxVendorAppUrlLength = OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_APP_URL_TLV_LENGTH;
+
+    /**
      * Returns the Type value.
      *
      * @returns The Type value.
@@ -237,6 +244,12 @@
 typedef StringTlvInfo<Tlv::kThreadStackVersion, Tlv::kMaxThreadStackVersionLength> ThreadStackVersionTlv;
 
 /**
+ * Defines Vendor App URL TLV constants and types.
+ *
+ */
+typedef StringTlvInfo<Tlv::kVendorAppUrl, Tlv::kMaxVendorAppUrlLength> VendorAppUrlTlv;
+
+/**
  * Defines Child IPv6 Address List TLV constants and types.
  *
  */
diff --git a/src/core/thread/panid_query_server.cpp b/src/core/thread/panid_query_server.cpp
index cfa1399..59dd591 100644
--- a/src/core/thread/panid_query_server.cpp
+++ b/src/core/thread/panid_query_server.cpp
@@ -124,7 +124,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    MeshCoP::LogError("send panid conflict", error);
+    LogWarnOnError(error, "send panid conflict");
 }
 
 void PanIdQueryServer::HandleTimer(void)
diff --git a/src/core/thread/version.hpp b/src/core/thread/version.hpp
index c3584aa..0277ede 100644
--- a/src/core/thread/version.hpp
+++ b/src/core/thread/version.hpp
@@ -42,10 +42,12 @@
 
 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 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
+// Support projects on legacy "1.3.1" version, which is now "1.4"
 constexpr uint16_t kThreadVersion1p3p1 = OT_THREAD_VERSION_1_3_1; ///< Thread Version 1.3.1
+constexpr uint16_t kThreadVersion1p4   = OT_THREAD_VERSION_1_4;   ///< Thread Version 1.4
 
 } // namespace ot
 
diff --git a/src/core/utils/channel_manager.cpp b/src/core/utils/channel_manager.cpp
index 7ed7df4..e0b13e4 100644
--- a/src/core/utils/channel_manager.cpp
+++ b/src/core/utils/channel_manager.cpp
@@ -34,7 +34,9 @@
 
 #include "channel_manager.hpp"
 
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
 
 #include "common/code_utils.hpp"
 #include "common/locator_getters.hpp"
@@ -54,26 +56,51 @@
     : InstanceLocator(aInstance)
     , mSupportedChannelMask(0)
     , mFavoredChannelMask(0)
+#if OPENTHREAD_FTD
     , mDelay(kMinimumDelay)
+#endif
     , mChannel(0)
+    , mChannelSelected(0)
     , mState(kStateIdle)
     , mTimer(aInstance)
     , mAutoSelectInterval(kDefaultAutoSelectInterval)
+#if OPENTHREAD_FTD
     , mAutoSelectEnabled(false)
+#endif
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    , mAutoSelectCslEnabled(false)
+#endif
     , mCcaFailureRateThreshold(kCcaFailureRateThreshold)
 {
 }
 
 void ChannelManager::RequestChannelChange(uint8_t aChannel)
 {
-    LogInfo("Request to change to channel %d with delay %d sec", aChannel, mDelay);
+#if OPENTHREAD_FTD
+    if (Get<Mle::Mle>().IsFullThreadDevice() && Get<Mle::Mle>().IsRxOnWhenIdle() && mAutoSelectEnabled)
+    {
+        RequestNetworkChannelChange(aChannel);
+    }
+#endif
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    if (mAutoSelectCslEnabled)
+    {
+        ChangeCslChannel(aChannel);
+    }
+#endif
+}
 
+#if OPENTHREAD_FTD
+void ChannelManager::RequestNetworkChannelChange(uint8_t aChannel)
+{
+    // Check requested channel != current channel
     if (aChannel == Get<Mac::Mac>().GetPanChannel())
     {
         LogInfo("Already operating on the requested channel %d", aChannel);
         ExitNow();
     }
 
+    LogInfo("Request to change to channel %d with delay %d sec", aChannel, mDelay);
     if (mState == kStateChangeInProgress)
     {
         VerifyOrExit(mChannel != aChannel);
@@ -89,7 +116,36 @@
 exit:
     return;
 }
+#endif
 
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+void ChannelManager::ChangeCslChannel(uint8_t aChannel)
+{
+    if (!(!Get<Mle::Mle>().IsRxOnWhenIdle() && Get<Mac::Mac>().IsCslEnabled()))
+    {
+        // cannot select or use other channel
+        ExitNow();
+    }
+
+    if (aChannel == Get<Mac::Mac>().GetCslChannel())
+    {
+        LogInfo("Already operating on the requested channel %d", aChannel);
+        ExitNow();
+    }
+
+    VerifyOrExit(Radio::IsCslChannelValid(aChannel));
+
+    LogInfo("Change to Csl channel %d now.", aChannel);
+
+    mChannel = aChannel;
+    Get<Mac::Mac>().SetCslChannel(aChannel);
+
+exit:
+    return;
+}
+#endif // (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+
+#if OPENTHREAD_FTD
 Error ChannelManager::SetDelay(uint16_t aDelay)
 {
     Error error = kErrorNone;
@@ -106,8 +162,8 @@
     MeshCoP::Dataset::Info dataset;
 
     dataset.Clear();
-    dataset.SetChannel(mChannel);
-    dataset.SetDelay(Time::SecToMsec(mDelay));
+    dataset.Set<MeshCoP::Dataset::kChannel>(mChannel);
+    dataset.Set<MeshCoP::Dataset::kDelay>(Time::SecToMsec(mDelay));
 
     switch (Get<MeshCoP::DatasetUpdater>().RequestUpdate(dataset, HandleDatasetUpdateDone, this))
     {
@@ -153,6 +209,7 @@
     mState = kStateIdle;
     StartAutoSelectTimer();
 }
+#endif // OPENTHREAD_FTD
 
 void ChannelManager::HandleTimer(void)
 {
@@ -160,12 +217,14 @@
     {
     case kStateIdle:
         LogInfo("Auto-triggered channel select");
-        IgnoreError(RequestChannelSelect(false));
+        IgnoreError(RequestAutoChannelSelect(false));
         StartAutoSelectTimer();
         break;
 
     case kStateChangeRequested:
+#if OPENTHREAD_FTD
         StartDatasetUpdate();
+#endif
         break;
 
     case kStateChangeInProgress:
@@ -236,6 +295,53 @@
     return shouldAttempt;
 }
 
+#if OPENTHREAD_FTD
+Error ChannelManager::RequestNetworkChannelSelect(bool aSkipQualityCheck)
+{
+    Error error = kErrorNone;
+
+    SuccessOrExit(error = RequestChannelSelect(aSkipQualityCheck));
+    RequestNetworkChannelChange(mChannelSelected);
+
+exit:
+    if ((error == kErrorAbort) || (error == kErrorAlready))
+    {
+        // ignore aborted channel change
+        error = kErrorNone;
+    }
+    return error;
+}
+#endif
+
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+Error ChannelManager::RequestCslChannelSelect(bool aSkipQualityCheck)
+{
+    Error error = kErrorNone;
+
+    SuccessOrExit(error = RequestChannelSelect(aSkipQualityCheck));
+    ChangeCslChannel(mChannelSelected);
+
+exit:
+    if ((error == kErrorAbort) || (error == kErrorAlready))
+    {
+        // ignore aborted channel change
+        error = kErrorNone;
+    }
+    return error;
+}
+#endif
+
+Error ChannelManager::RequestAutoChannelSelect(bool aSkipQualityCheck)
+{
+    Error error = kErrorNone;
+
+    SuccessOrExit(error = RequestChannelSelect(aSkipQualityCheck));
+    RequestChannelChange(mChannelSelected);
+
+exit:
+    return error;
+}
+
 Error ChannelManager::RequestChannelSelect(bool aSkipQualityCheck)
 {
     Error    error = kErrorNone;
@@ -246,17 +352,27 @@
 
     VerifyOrExit(!Get<Mle::Mle>().IsDisabled(), error = kErrorInvalidState);
 
-    VerifyOrExit(aSkipQualityCheck || ShouldAttemptChannelChange());
+    VerifyOrExit(aSkipQualityCheck || ShouldAttemptChannelChange(), error = kErrorAbort);
 
     SuccessOrExit(error = FindBetterChannel(newChannel, newOccupancy));
 
-    curChannel   = Get<Mac::Mac>().GetPanChannel();
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    if (Get<Mac::Mac>().IsCslEnabled() && (Get<Mac::Mac>().GetCslChannel() != 0))
+    {
+        curChannel = Get<Mac::Mac>().GetCslChannel();
+    }
+    else
+#endif
+    {
+        curChannel = Get<Mac::Mac>().GetPanChannel();
+    }
+
     curOccupancy = Get<ChannelMonitor>().GetChannelOccupancy(curChannel);
 
     if (newChannel == curChannel)
     {
         LogInfo("Already on best possible channel %d", curChannel);
-        ExitNow();
+        ExitNow(error = kErrorAlready);
     }
 
     LogInfo("Cur channel %d, occupancy 0x%04x - Best channel %d, occupancy 0x%04x", curChannel, curOccupancy,
@@ -269,18 +385,13 @@
         (static_cast<uint16_t>(curOccupancy - newOccupancy) < kThresholdToChangeChannel))
     {
         LogInfo("Occupancy rate diff too small to change channel");
-        ExitNow();
+        ExitNow(error = kErrorAbort);
     }
 
-    RequestChannelChange(newChannel);
+    mChannelSelected = newChannel;
 
 exit:
-
-    if (error != kErrorNone)
-    {
-        LogInfo("Request to select better channel failed, error: %s", ErrorToString(error));
-    }
-
+    LogWarnOnError(error, "select better channel");
     return error;
 }
 #endif // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
@@ -289,7 +400,14 @@
 {
     VerifyOrExit(mState == kStateIdle);
 
+#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && \
+     OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    if (mAutoSelectEnabled || mAutoSelectCslEnabled)
+#elif OPENTHREAD_FTD
     if (mAutoSelectEnabled)
+#elif (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    if (mAutoSelectCslEnabled)
+#endif
     {
         mTimer.Start(Time::SecToMsec(mAutoSelectInterval));
     }
@@ -302,15 +420,29 @@
     return;
 }
 
-void ChannelManager::SetAutoChannelSelectionEnabled(bool aEnabled)
+#if OPENTHREAD_FTD
+void ChannelManager::SetAutoNetworkChannelSelectionEnabled(bool aEnabled)
 {
     if (aEnabled != mAutoSelectEnabled)
     {
         mAutoSelectEnabled = aEnabled;
-        IgnoreError(RequestChannelSelect(false));
+        IgnoreError(RequestNetworkChannelSelect(false));
         StartAutoSelectTimer();
     }
 }
+#endif
+
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+void ChannelManager::SetAutoCslChannelSelectionEnabled(bool aEnabled)
+{
+    if (aEnabled != mAutoSelectCslEnabled)
+    {
+        mAutoSelectCslEnabled = aEnabled;
+        IgnoreError(RequestAutoChannelSelect(false));
+        StartAutoSelectTimer();
+    }
+}
+#endif
 
 Error ChannelManager::SetAutoChannelSelectionInterval(uint32_t aInterval)
 {
@@ -321,9 +453,19 @@
 
     mAutoSelectInterval = aInterval;
 
-    if (mAutoSelectEnabled && (mState == kStateIdle) && mTimer.IsRunning() && (prevInterval != aInterval))
+#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && \
+     OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    if (mAutoSelectEnabled || mAutoSelectCslEnabled)
+#elif OPENTHREAD_FTD
+    if (mAutoSelectEnabled)
+#elif (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    if (mAutoSelectCslEnabled)
+#endif
     {
-        mTimer.StartAt(mTimer.GetFireTime() - Time::SecToMsec(prevInterval), Time::SecToMsec(aInterval));
+        if ((mState == kStateIdle) && mTimer.IsRunning() && (prevInterval != aInterval))
+        {
+            mTimer.StartAt(mTimer.GetFireTime() - Time::SecToMsec(prevInterval), Time::SecToMsec(aInterval));
+        }
     }
 
 exit:
diff --git a/src/core/utils/channel_manager.hpp b/src/core/utils/channel_manager.hpp
index ad46141..02d9305 100644
--- a/src/core/utils/channel_manager.hpp
+++ b/src/core/utils/channel_manager.hpp
@@ -36,7 +36,9 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
+#if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && \
+    (OPENTHREAD_FTD ||                          \
+     (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE))
 
 #include <openthread/platform/radio.h>
 
@@ -64,11 +66,13 @@
 class ChannelManager : public InstanceLocator, private NonCopyable
 {
 public:
+#if OPENTHREAD_FTD
     /**
      * Minimum delay (in seconds) used for network channel change.
      *
      */
     static constexpr uint16_t kMinimumDelay = OPENTHREAD_CONFIG_CHANNEL_MANAGER_MINIMUM_DELAY;
+#endif
 
     /**
      * Initializes a `ChanelManager` object.
@@ -78,6 +82,7 @@
      */
     explicit ChannelManager(Instance &aInstance);
 
+#if OPENTHREAD_FTD
     /**
      * Requests a Thread network channel change.
      *
@@ -91,16 +96,18 @@
      * @param[in] aChannel             The new channel for the Thread network.
      *
      */
-    void RequestChannelChange(uint8_t aChannel);
+    void RequestNetworkChannelChange(uint8_t aChannel);
+#endif
 
     /**
-     * Gets the channel from the last successful call to `RequestChannelChange()`.
+     * Gets the channel from the last successful call to `RequestNetworkChannelChange()` or `ChangeCslChannel()`.
      *
      * @returns The last requested channel, or zero if there has been no channel change request yet.
      *
      */
     uint8_t GetRequestedChannel(void) const { return mChannel; }
 
+#if OPENTHREAD_FTD
     /**
      * Gets the delay (in seconds) used for a channel change.
      *
@@ -122,9 +129,11 @@
      *
      */
     Error SetDelay(uint16_t aDelay);
+#endif // OPENTHREAD_FTD
 
+#if OPENTHREAD_FTD
     /**
-     * Requests that `ChannelManager` checks and selects a new channel and starts a channel change.
+     * Requests that `ChannelManager` checks and selects a new network channel and starts a network channel change.
      *
      * Unlike the `RequestChannelChange()`  where the channel must be given as a parameter, this method asks the
      * `ChannelManager` to select a channel by itself (based on the collected channel quality info).
@@ -142,7 +151,7 @@
      *    (@sa SetSupportedChannels, @sa SetFavoredChannels).
      *
      * 3) If the newly selected channel is different from the current channel, `ChannelManager` requests/starts the
-     *    channel change process (internally invoking a `RequestChannelChange()`).
+     *    channel change process (internally invoking a `RequestNetworkChannelChange()`).
      *
      *
      * @param[in] aSkipQualityCheck        Indicates whether the quality check (step 1) should be skipped.
@@ -152,8 +161,40 @@
      * @retval kErrorInvalidState      Thread is not enabled or not enough data to select new channel.
      *
      */
-    Error RequestChannelSelect(bool aSkipQualityCheck);
+    Error RequestNetworkChannelSelect(bool aSkipQualityCheck);
+#endif // OPENTHREAD_FTD
 
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    /**
+     * Requests that `ChannelManager` checks and selects a new Csl channel and starts a channel change.
+     *
+     * Once called, the `ChannelManager` will perform the following 3 steps:
+     *
+     * 1) `ChannelManager` decides if the channel change would be helpful. This check can be skipped if
+     *    `aSkipQualityCheck` is set to true (forcing a channel selection to happen and skipping the quality check).
+     *    This step uses the collected link quality metrics on the device (such as CCA failure rate, frame and message
+     *    error rates per neighbor, etc.) to determine if the current channel quality is at the level that justifies
+     *    a channel change.
+     *
+     * 2) If the first step passes, then `ChannelManager` selects a potentially better channel. It uses the collected
+     *    channel occupancy data by `ChannelMonitor` module. The supported and favored channels are used at this step.
+     *    (@sa SetSupportedChannels, @sa SetFavoredChannels).
+     *
+     * 3) If the newly selected channel is different from the current Csl channel, `ChannelManager` starts the
+     *    channel change process (internally invoking a `ChangeCslChannel()`).
+     *
+     *
+     * @param[in] aSkipQualityCheck        Indicates whether the quality check (step 1) should be skipped.
+     *
+     * @retval kErrorNone              Channel selection finished successfully.
+     * @retval kErrorNotFound          Supported channels is empty, therefore could not select a channel.
+     * @retval kErrorInvalidState      Thread is not enabled or not enough data to select new channel.
+     *
+     */
+    Error RequestCslChannelSelect(bool aSkipQualityCheck);
+#endif // (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+
+#if OPENTHREAD_FTD
     /**
      * Enables/disables the auto-channel-selection functionality.
      *
@@ -163,7 +204,7 @@
      * @param[in]  aEnabled  Indicates whether to enable or disable this functionality.
      *
      */
-    void SetAutoChannelSelectionEnabled(bool aEnabled);
+    void SetAutoNetworkChannelSelectionEnabled(bool aEnabled);
 
     /**
      * Indicates whether the auto-channel-selection functionality is enabled or not.
@@ -171,7 +212,29 @@
      * @returns TRUE if enabled, FALSE if disabled.
      *
      */
-    bool GetAutoChannelSelectionEnabled(void) const { return mAutoSelectEnabled; }
+    bool GetAutoNetworkChannelSelectionEnabled(void) const { return mAutoSelectEnabled; }
+#endif
+
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    /**
+     * Enables/disables the auto-channel-selection functionality.
+     *
+     * When enabled, `ChannelManager` will periodically invoke a `RequestChannelSelect(false)`. The period interval
+     * can be set by `SetAutoChannelSelectionInterval()`.
+     *
+     * @param[in]  aEnabled  Indicates whether to enable or disable this functionality.
+     *
+     */
+    void SetAutoCslChannelSelectionEnabled(bool aEnabled);
+
+    /**
+     * Indicates whether the auto-channel-selection functionality is enabled or not.
+     *
+     * @returns TRUE if enabled, FALSE if disabled.
+     *
+     */
+    bool GetAutoCslChannelSelectionEnabled(void) const { return mAutoSelectCslEnabled; }
+#endif
 
     /**
      * Sets the period interval (in seconds) used by auto-channel-selection functionality.
@@ -244,7 +307,7 @@
     // Retry interval to resend Pending Dataset in case of tx failure (in ms).
     static constexpr uint32_t kPendingDatasetTxRetryInterval = 20000;
 
-    // Maximum jitter/wait time to start a requested channel change (in ms).
+    // Maximum jitter/wait time to start a requested network channel change (in ms).
     static constexpr uint32_t kRequestStartJitterInterval = 10000;
 
     // The minimum number of RSSI samples required before using the collected data (by `ChannelMonitor`) to select
@@ -273,28 +336,45 @@
         kStateChangeInProgress,
     };
 
+#if OPENTHREAD_FTD
     void        StartDatasetUpdate(void);
     static void HandleDatasetUpdateDone(Error aError, void *aContext);
     void        HandleDatasetUpdateDone(Error aError);
-    void        HandleTimer(void);
-    void        StartAutoSelectTimer(void);
+#endif
+    void  HandleTimer(void);
+    void  StartAutoSelectTimer(void);
+    Error RequestChannelSelect(bool aSkipQualityCheck);
+    Error RequestAutoChannelSelect(bool aSkipQualityCheck);
+    void  RequestChannelChange(uint8_t aChannel);
 
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
     Error FindBetterChannel(uint8_t &aNewChannel, uint16_t &aOccupancy);
     bool  ShouldAttemptChannelChange(void);
 #endif
 
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    void ChangeCslChannel(uint8_t aChannel);
+#endif
+
     using ManagerTimer = TimerMilliIn<ChannelManager, &ChannelManager::HandleTimer>;
 
     Mac::ChannelMask mSupportedChannelMask;
     Mac::ChannelMask mFavoredChannelMask;
-    uint16_t         mDelay;
-    uint8_t          mChannel;
-    State            mState;
-    ManagerTimer     mTimer;
-    uint32_t         mAutoSelectInterval;
-    bool             mAutoSelectEnabled;
-    uint16_t         mCcaFailureRateThreshold;
+#if OPENTHREAD_FTD
+    uint16_t mDelay;
+#endif
+    uint8_t      mChannel;
+    uint8_t      mChannelSelected;
+    State        mState;
+    ManagerTimer mTimer;
+    uint32_t     mAutoSelectInterval;
+#if OPENTHREAD_FTD
+    bool mAutoSelectEnabled;
+#endif
+#if (OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE)
+    bool mAutoSelectCslEnabled;
+#endif
+    uint16_t mCcaFailureRateThreshold;
 };
 
 /**
diff --git a/src/core/utils/link_metrics_manager.hpp b/src/core/utils/link_metrics_manager.hpp
index 098865e..df42809 100644
--- a/src/core/utils/link_metrics_manager.hpp
+++ b/src/core/utils/link_metrics_manager.hpp
@@ -152,6 +152,15 @@
     explicit LinkMetricsManager(Instance &aInstance);
 
     /**
+     * Is the LinkMetricsManager feature enabled.
+     *
+     * @retval TRUE   Link Metrics Manager is enabled.
+     * @retval FALSE  Link Metrics Manager is not enabled.
+     *
+     */
+    bool IsEnabled(void) { return mEnabled; }
+
+    /**
      * Enable/Disable the LinkMetricsManager feature.
      *
      * @param[in]   aEnable  A boolean to indicate enable or disable.
diff --git a/src/core/utils/mesh_diag.cpp b/src/core/utils/mesh_diag.cpp
index e21cb18..07566b6 100644
--- a/src/core/utils/mesh_diag.cpp
+++ b/src/core/utils/mesh_diag.cpp
@@ -106,7 +106,7 @@
     }
 
     mDiscover.mCallback.Set(aCallback, aContext);
-    mState = kStateDicoverTopology;
+    mState = kStateDiscoverTopology;
     mTimer.Start(kResponseTimeout);
 
 exit:
@@ -133,7 +133,7 @@
 
     SuccessOrExit(aResult);
     VerifyOrExit(aMessage != nullptr);
-    VerifyOrExit(mState == kStateDicoverTopology);
+    VerifyOrExit(mState == kStateDiscoverTopology);
 
     SuccessOrExit(routerInfo.ParseFrom(*aMessage));
 
@@ -437,7 +437,7 @@
     case kStateQueryRouterNeighborTable:
         break;
 
-    case kStateDicoverTopology:
+    case kStateDiscoverTopology:
         IgnoreError(Get<Tmf::Agent>().AbortTransaction(HandleDiagGetResponse, this));
         break;
     }
@@ -460,7 +460,7 @@
     case kStateIdle:
         break;
 
-    case kStateDicoverTopology:
+    case kStateDiscoverTopology:
         mDiscover.mCallback.InvokeIfSet(aError, nullptr);
         break;
 
diff --git a/src/core/utils/mesh_diag.hpp b/src/core/utils/mesh_diag.hpp
index ff6d3b1..3e7b04c 100644
--- a/src/core/utils/mesh_diag.hpp
+++ b/src/core/utils/mesh_diag.hpp
@@ -246,7 +246,7 @@
     enum State : uint8_t
     {
         kStateIdle,
-        kStateDicoverTopology,
+        kStateDiscoverTopology,
         kStateQueryChildTable,
         kStateQueryChildrenIp6Addrs,
         kStateQueryRouterNeighborTable,
diff --git a/src/core/utils/otns.cpp b/src/core/utils/otns.cpp
index f19185f..a130ca3 100644
--- a/src/core/utils/otns.cpp
+++ b/src/core/utils/otns.cpp
@@ -166,11 +166,9 @@
 
     EmitStatus("coap=send,%d,%d,%d,%s,%s,%d", aMessage.GetMessageId(), aMessage.GetType(), aMessage.GetCode(), uriPath,
                aMessageInfo.GetPeerAddr().ToString().AsCString(), aMessageInfo.GetPeerPort());
+
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("EmitCoapSend failed: %s", ErrorToString(error));
-    }
+    LogWarnOnError(error, "EmitCoapSend");
 }
 
 void Otns::EmitCoapReceive(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
@@ -183,10 +181,7 @@
     EmitStatus("coap=recv,%d,%d,%d,%s,%s,%d", aMessage.GetMessageId(), aMessage.GetType(), aMessage.GetCode(), uriPath,
                aMessageInfo.GetPeerAddr().ToString().AsCString(), aMessageInfo.GetPeerPort());
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("EmitCoapReceive failed: %s", ErrorToString(error));
-    }
+    LogWarnOnError(error, "EmitCoapReceive");
 }
 
 void Otns::EmitCoapSendFailure(Error aError, Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
@@ -200,10 +195,7 @@
                uriPath, aMessageInfo.GetPeerAddr().ToString().AsCString(), aMessageInfo.GetPeerPort(),
                ErrorToString(aError));
 exit:
-    if (error != kErrorNone)
-    {
-        LogWarn("EmitCoapSendFailure failed: %s", ErrorToString(error));
-    }
+    LogWarnOnError(error, "EmitCoapSendFailure");
 }
 
 } // namespace Utils
diff --git a/src/core/utils/verhoeff_checksum.cpp b/src/core/utils/verhoeff_checksum.cpp
new file mode 100644
index 0000000..bf41709
--- /dev/null
+++ b/src/core/utils/verhoeff_checksum.cpp
@@ -0,0 +1,149 @@
+/*
+ *  Copyright (c) 2024, 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 Verhoeff checksum calculation and validation.
+ */
+
+#include "verhoeff_checksum.hpp"
+
+#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
+#include "common/code_utils.hpp"
+#include "common/string.hpp"
+
+namespace ot {
+namespace Utils {
+
+uint8_t VerhoeffChecksum::Lookup(uint8_t aIndex, const uint8_t aCompressedArray[])
+{
+    // The values in the array are [0-9]. To save space, two
+    // entries are saved as a single byte in @p aCompressedArray,
+    // such that higher 4-bit corresponds to one entry, and the
+    // lower 4-bit to the next entry.
+
+    uint8_t result = aCompressedArray[aIndex / 2];
+
+    if ((aIndex & 1) == 0)
+    {
+        result >>= 4;
+    }
+    else
+    {
+        result &= 0x0f;
+    }
+
+    return result;
+}
+
+uint8_t VerhoeffChecksum::Multiply(uint8_t aFirst, uint8_t aSecond)
+{
+    static uint8_t kMultiplication[][5] = {{0x01, 0x23, 0x45, 0x67, 0x89}, {0x12, 0x34, 0x06, 0x78, 0x95},
+                                           {0x23, 0x40, 0x17, 0x89, 0x56}, {0x34, 0x01, 0x28, 0x95, 0x67},
+                                           {0x40, 0x12, 0x39, 0x56, 0x78}, {0x59, 0x87, 0x60, 0x43, 0x21},
+                                           {0x65, 0x98, 0x71, 0x04, 0x32}, {0x76, 0x59, 0x82, 0x10, 0x43},
+                                           {0x87, 0x65, 0x93, 0x21, 0x04}, {0x98, 0x76, 0x54, 0x32, 0x10}};
+
+    return Lookup(aSecond, kMultiplication[aFirst]);
+}
+
+uint8_t VerhoeffChecksum::Permute(uint8_t aPosition, uint8_t aValue)
+{
+    static uint8_t kPermutation[][5] = {{0x01, 0x23, 0x45, 0x67, 0x89}, {0x15, 0x76, 0x28, 0x30, 0x94},
+                                        {0x58, 0x03, 0x79, 0x61, 0x42}, {0x89, 0x16, 0x04, 0x35, 0x27},
+                                        {0x94, 0x53, 0x12, 0x68, 0x70}, {0x42, 0x86, 0x57, 0x39, 0x01},
+                                        {0x27, 0x93, 0x80, 0x64, 0x15}, {0x70, 0x46, 0x91, 0x32, 0x58}};
+
+    return Lookup(aValue, kPermutation[aPosition]);
+}
+
+uint8_t VerhoeffChecksum::InverseOf(uint8_t aValue)
+{
+    static uint8_t kInverse[] = {0x04, 0x32, 0x15, 0x67, 0x89};
+
+    return Lookup(aValue, kInverse);
+}
+
+Error VerhoeffChecksum::Calculate(const char *aDecimalString, char &aChecksum)
+{
+    Error   error;
+    uint8_t code;
+
+    SuccessOrExit(error = ComputeCode(aDecimalString, code, /* aValidate */ false));
+    aChecksum = static_cast<char>('0' + InverseOf(code));
+
+exit:
+    return error;
+}
+
+Error VerhoeffChecksum::Validate(const char *aDecimalString)
+{
+    Error   error;
+    uint8_t code;
+
+    SuccessOrExit(error = ComputeCode(aDecimalString, code, /* aValidate */ true));
+    VerifyOrExit(code == 0, error = kErrorFailed);
+
+exit:
+    return error;
+}
+
+Error VerhoeffChecksum::ComputeCode(const char *aDecimalString, uint8_t &aCode, bool aValidate)
+{
+    Error    error  = kErrorNone;
+    uint8_t  code   = 0;
+    uint16_t index  = 0;
+    uint16_t length = StringLength(aDecimalString, kMaxStringLength + 1);
+
+    VerifyOrExit(length <= kMaxStringLength, error = kErrorInvalidArgs);
+
+    if (!aValidate)
+    {
+        length++;
+        index++;
+    }
+
+    for (; index < length; ++index)
+    {
+        char digit = aDecimalString[length - index - 1];
+
+        VerifyOrExit(digit >= '0' && digit <= '9', error = kErrorInvalidArgs);
+        code = Multiply(code, Permute(index % 8, static_cast<uint8_t>(digit - '0')));
+    }
+
+    aCode = code;
+
+exit:
+    return error;
+}
+
+} // namespace Utils
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
diff --git a/src/core/utils/verhoeff_checksum.hpp b/src/core/utils/verhoeff_checksum.hpp
new file mode 100644
index 0000000..9249e23
--- /dev/null
+++ b/src/core/utils/verhoeff_checksum.hpp
@@ -0,0 +1,99 @@
+/*
+ *  Copyright (c) 2024, 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 Verhoeff checksum calculation and validation.
+ */
+
+#ifndef VERHOEFF_CHECKSUM_HPP_
+#define VERHOEFF_CHECKSUM_HPP_
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
+#include <openthread/verhoeff_checksum.h>
+
+#include "common/error.hpp"
+
+namespace ot {
+namespace Utils {
+
+class VerhoeffChecksum
+{
+public:
+    /**
+     * Specifies the maximum length of decimal string input.
+     *
+     */
+    static constexpr uint16_t kMaxStringLength = OT_VERHOEFF_CHECKSUM_MAX_STRING_LENGTH;
+
+    /**
+     * Calculates the Verhoeff checksum for a given decimal string.
+     *
+     *
+     * @param[in] a DecimalString  The string containing decimal digits.
+     * @param[out] aChecksum       Reference to a `char` to return the calculated checksum.
+     *
+     * @retval kErrorNone          Successfully calculated the checksum, @p aChecksum is updated.
+     * @retval kErrorInvalidArgs   The @p aDecimalString is not valid, i.e. it either contains chars other than
+     *                             ['0'-'9'], or is longer than `kMaxStringLength`.
+     *
+     */
+    static Error Calculate(const char *aDecimalString, char &aChecksum);
+
+    /**
+     * Validates the Verhoeff checksum for a given decimal string.
+     *
+     * @param[in] aDecimalString   The string containing decimal digits (last char is treated as checksum).
+     *
+     * @retval kErrorNone            Successfully validated the checksum in @p aDecimalString.
+     * @retval kErrorFailed          Checksum is not valid.
+     * @retval kErrorInvalidArgs     The @p aDecimalString is not valid, i.e. it either contains chars other than
+     *                               ['0'-'9'], or is longer than `kMaxStringLength`.
+     *
+     */
+    static Error Validate(const char *aDecimalString);
+
+    VerhoeffChecksum(void) = delete;
+
+private:
+    static Error   ComputeCode(const char *aDecimalString, uint8_t &aCode, bool aValidate);
+    static uint8_t Lookup(uint8_t aIndex, const uint8_t aCompressedArray[]);
+    static uint8_t Multiply(uint8_t aFirst, uint8_t aSecond);
+    static uint8_t Permute(uint8_t aPosition, uint8_t aValue);
+    static uint8_t InverseOf(uint8_t aValue);
+};
+
+} // namespace Utils
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
+#endif // VERHOEFF_CHECKSUM_HPP_
diff --git a/src/lib/platform/reset_util.h b/src/lib/platform/reset_util.h
index 52f5681..56e75da 100644
--- a/src/lib/platform/reset_util.h
+++ b/src/lib/platform/reset_util.h
@@ -32,8 +32,8 @@
 #if defined(OPENTHREAD_ENABLE_COVERAGE) && OPENTHREAD_ENABLE_COVERAGE && defined(__GNUC__)
 #if __GNUC__ >= 11 || (defined(__clang__) && (defined(__APPLE__) && (__clang_major__ >= 13)) || \
                        (!defined(__APPLE__) && (__clang_major__ >= 12)))
-void __gcov_dump();
-void __gcov_reset();
+void __gcov_dump(void);
+void __gcov_reset(void);
 
 static void flush_gcov(void)
 {
diff --git a/src/lib/spinel/BUILD.gn b/src/lib/spinel/BUILD.gn
index 33e0eb1..cd3fb83 100644
--- a/src/lib/spinel/BUILD.gn
+++ b/src/lib/spinel/BUILD.gn
@@ -34,6 +34,8 @@
 
 spinel_sources = [
   "openthread-spinel-config.h",
+  "logger.hpp",
+  "logger.cpp",
   "multi_frame_buffer.hpp",
   "radio_spinel.cpp",
   "radio_spinel.hpp",
@@ -43,6 +45,8 @@
   "spinel_buffer.hpp",
   "spinel_decoder.cpp",
   "spinel_decoder.hpp",
+  "spinel_driver.cpp",
+  "spinel_driver.hpp",
   "spinel_encoder.cpp",
   "spinel_encoder.hpp",
   "spinel_platform.h",
diff --git a/src/lib/spinel/CMakeLists.txt b/src/lib/spinel/CMakeLists.txt
index b8be41c..5ff22c0 100644
--- a/src/lib/spinel/CMakeLists.txt
+++ b/src/lib/spinel/CMakeLists.txt
@@ -89,7 +89,12 @@
 target_include_directories(openthread-spinel-ncp PUBLIC ${OT_PUBLIC_INCLUDES} PRIVATE ${COMMON_INCLUDES})
 target_include_directories(openthread-spinel-rcp PUBLIC ${OT_PUBLIC_INCLUDES} PRIVATE ${COMMON_INCLUDES})
 
-target_sources(openthread-radio-spinel PRIVATE radio_spinel.cpp)
+target_sources(openthread-radio-spinel
+    PRIVATE
+        logger.cpp
+        radio_spinel.cpp
+        spinel_driver.cpp
+)
 target_sources(openthread-spinel-ncp PRIVATE ${COMMON_SOURCES})
 target_sources(openthread-spinel-rcp PRIVATE ${COMMON_SOURCES})
 
diff --git a/src/lib/spinel/logger.cpp b/src/lib/spinel/logger.cpp
new file mode 100644
index 0000000..46636c7
--- /dev/null
+++ b/src/lib/spinel/logger.cpp
@@ -0,0 +1,748 @@
+/*
+ *  Copyright (c) 2024, 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 "logger.hpp"
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <openthread/error.h>
+#include <openthread/logging.h>
+#include <openthread/platform/radio.h>
+
+#include "common/code_utils.hpp"
+#include "common/num_utils.hpp"
+#include "lib/spinel/spinel.h"
+
+namespace ot {
+namespace Spinel {
+
+Logger::Logger(const char *aModuleName)
+    : mModuleName(aModuleName)
+{
+}
+
+void Logger::LogIfFail(const char *aText, otError aError)
+{
+    OT_UNUSED_VARIABLE(aText);
+
+    if (aError != OT_ERROR_NONE && aError != OT_ERROR_NO_ACK)
+    {
+        LogWarn("%s: %s", aText, otThreadErrorToString(aError));
+    }
+}
+
+void Logger::LogCrit(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_CRIT, mModuleName, aFormat, args);
+    va_end(args);
+}
+
+void Logger::LogWarn(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_WARN, mModuleName, aFormat, args);
+    va_end(args);
+}
+
+void Logger::LogNote(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_NOTE, mModuleName, aFormat, args);
+    va_end(args);
+}
+
+void Logger::LogInfo(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_INFO, mModuleName, aFormat, args);
+    va_end(args);
+}
+
+void Logger::LogDebg(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_DEBG, mModuleName, aFormat, args);
+    va_end(args);
+}
+
+uint32_t Logger::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);
+}
+
+void Logger::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, iid:%d, tid:%u, cmd:%s", prefix,
+                      SPINEL_HEADER_GET_FLAG(header), SPINEL_HEADER_GET_IID(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);
+            OT_UNUSED_VARIABLE(start); // Avoid static analysis error
+            LogDebg("%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);
+            OT_UNUSED_VARIABLE(start); // Avoid static analysis error
+            LogDebg("%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);
+
+        LogDebg("%s ...", buf);
+        LogDebg(" txRequest:%lu", ToUlong(metrics.mNumTxRequest));
+        LogDebg(" txGrantImmediate:%lu", ToUlong(metrics.mNumTxGrantImmediate));
+        LogDebg(" txGrantWait:%lu", ToUlong(metrics.mNumTxGrantWait));
+        LogDebg(" txGrantWaitActivated:%lu", ToUlong(metrics.mNumTxGrantWaitActivated));
+        LogDebg(" txGrantWaitTimeout:%lu", ToUlong(metrics.mNumTxGrantWaitTimeout));
+        LogDebg(" txGrantDeactivatedDuringRequest:%lu", ToUlong(metrics.mNumTxGrantDeactivatedDuringRequest));
+        LogDebg(" txDelayedGrant:%lu", ToUlong(metrics.mNumTxDelayedGrant));
+        LogDebg(" avgTxRequestToGrantTime:%lu", ToUlong(metrics.mAvgTxRequestToGrantTime));
+        LogDebg(" rxRequest:%lu", ToUlong(metrics.mNumRxRequest));
+        LogDebg(" rxGrantImmediate:%lu", ToUlong(metrics.mNumRxGrantImmediate));
+        LogDebg(" rxGrantWait:%lu", ToUlong(metrics.mNumRxGrantWait));
+        LogDebg(" rxGrantWaitActivated:%lu", ToUlong(metrics.mNumRxGrantWaitActivated));
+        LogDebg(" rxGrantWaitTimeout:%lu", ToUlong(metrics.mNumRxGrantWaitTimeout));
+        LogDebg(" rxGrantDeactivatedDuringRequest:%lu", ToUlong(metrics.mNumRxGrantDeactivatedDuringRequest));
+        LogDebg(" rxDelayedGrant:%lu", ToUlong(metrics.mNumRxDelayedGrant));
+        LogDebg(" avgRxRequestToGrantTime:%lu", ToUlong(metrics.mAvgRxRequestToGrantTime));
+        LogDebg(" rxGrantNone:%lu", ToUlong(metrics.mNumRxGrantNone));
+        LogDebg(" 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 (spinel_size_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 (unsigned int 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:
+    OT_UNUSED_VARIABLE(start); // Avoid static analysis error
+    if (error == OT_ERROR_NONE)
+    {
+        LogDebg("%s", buf);
+    }
+    else if (prefix != nullptr)
+    {
+        LogDebg("%s, failed to parse spinel frame !", prefix);
+    }
+}
+
+} // namespace Spinel
+} // namespace ot
diff --git a/src/lib/spinel/logger.hpp b/src/lib/spinel/logger.hpp
new file mode 100644
index 0000000..ea86f4d
--- /dev/null
+++ b/src/lib/spinel/logger.hpp
@@ -0,0 +1,69 @@
+/*
+ *  Copyright (c) 2024, 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 SPINEL_LOGGER_HPP_
+#define SPINEL_LOGGER_HPP_
+
+#include "openthread-core-config.h"
+
+#include <openthread/error.h>
+#include <openthread/platform/toolchain.h>
+
+#include "ncp/ncp_config.h"
+
+namespace ot {
+namespace Spinel {
+
+class Logger
+{
+protected:
+    explicit Logger(const char *aModuleName);
+
+    void LogIfFail(const char *aText, otError aError);
+
+    void LogCrit(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
+    void LogWarn(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
+    void LogNote(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
+    void LogInfo(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
+    void LogDebg(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
+
+    uint32_t Snprintf(char *aDest, uint32_t aSize, const char *aFormat, ...);
+    void     LogSpinelFrame(const uint8_t *aFrame, uint16_t aLength, bool aTx);
+
+    enum
+    {
+        kChannelMaskBufferSize = 32, ///< Max buffer size used to store `SPINEL_PROP_PHY_CHAN_SUPPORTED` value.
+    };
+
+    const char *mModuleName;
+};
+
+} // namespace Spinel
+} // namespace ot
+
+#endif // SPINEL_LOG_HPP_
diff --git a/src/lib/spinel/multi_frame_buffer.hpp b/src/lib/spinel/multi_frame_buffer.hpp
index ab1ef83..61945a5 100644
--- a/src/lib/spinel/multi_frame_buffer.hpp
+++ b/src/lib/spinel/multi_frame_buffer.hpp
@@ -371,7 +371,7 @@
 
         aFrame = (aFrame == nullptr) ? mBuffer : aFrame + aLength;
 
-        if (aFrame != mWriteFrameStart)
+        if (HasSavedFrame() && (aFrame != mWriteFrameStart))
         {
             uint16_t totalLength = LittleEndian::ReadUint16(aFrame + kHeaderTotalLengthOffset);
             uint16_t skipLength  = LittleEndian::ReadUint16(aFrame + kHeaderSkipLengthOffset);
diff --git a/src/lib/spinel/radio_spinel.cpp b/src/lib/spinel/radio_spinel.cpp
index b41fde3..87abaef 100644
--- a/src/lib/spinel/radio_spinel.cpp
+++ b/src/lib/spinel/radio_spinel.cpp
@@ -46,7 +46,9 @@
 #include "common/encoding.hpp"
 #include "common/new.hpp"
 #include "lib/platform/exit_code.h"
+#include "lib/spinel/logger.hpp"
 #include "lib/spinel/spinel_decoder.hpp"
+#include "lib/spinel/spinel_driver.hpp"
 
 namespace ot {
 namespace Spinel {
@@ -64,24 +66,9 @@
 
 otRadioCaps RadioSpinel::sRadioCaps = OT_RADIO_CAPS_NONE;
 
-inline bool RadioSpinel::IsFrameForUs(spinel_iid_t aIid)
-{
-    bool found = false;
-
-    for (spinel_iid_t iid : mIidList)
-    {
-        if (aIid == iid)
-        {
-            ExitNow(found = true);
-        }
-    }
-
-exit:
-    return found;
-}
-
 RadioSpinel::RadioSpinel(void)
-    : mInstance(nullptr)
+    : Logger("RadioSpinel")
+    , mInstance(nullptr)
     , mSpinelInterface(nullptr)
     , mCmdTidsInUse(0)
     , mCmdNextTid(1)
@@ -106,6 +93,7 @@
     , mRcpFailure(kRcpFailureNone)
     , mSrcMatchShortEntryCount(0)
     , mSrcMatchExtEntryCount(0)
+    , mSrcMatchEnabled(false)
     , mMacKeySet(false)
     , mCcaEnergyDetectThresholdSet(false)
     , mTransmitPowerSet(false)
@@ -113,6 +101,7 @@
     , mFemLnaGainSet(false)
     , mEnergyScanning(false)
     , mMacFrameCounterSet(false)
+    , mSrcMatchSet(false)
 #endif
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
     , mDiagMode(false)
@@ -127,7 +116,6 @@
     , mVendorRestorePropertiesContext(nullptr)
 #endif
 {
-    memset(mIidList, SPINEL_HEADER_INVALID_IID, sizeof(mIidList));
     memset(&mRadioSpinelMetrics, 0, sizeof(mRadioSpinelMetrics));
     memset(&mCallbacks, 0, sizeof(mCallbacks));
 }
@@ -147,20 +135,17 @@
 #endif
 
     mSpinelInterface = &aSpinelInterface;
-    SuccessOrDie(mSpinelInterface->Init(HandleReceivedFrame, this, mRxFrameBuffer));
 
-    VerifyOrDie(aIidList != nullptr, OT_EXIT_INVALID_ARGUMENTS);
-    VerifyOrDie(aIidListLength != 0 && aIidListLength <= OT_ARRAY_LENGTH(mIidList), OT_EXIT_INVALID_ARGUMENTS);
-    mIid = aIidList[0];
-    memset(mIidList, SPINEL_HEADER_INVALID_IID, sizeof(mIidList));
-    memcpy(mIidList, aIidList, aIidListLength * sizeof(spinel_iid_t));
+#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    memset(&mTxIeInfo, 0, sizeof(otRadioIeInfo));
+    mTxRadioFrame.mInfo.mTxInfo.mIeInfo = &mTxIeInfo;
+#endif
 
-    ResetRcp(aResetRadio);
-    SuccessOrExit(error = CheckSpinelVersion());
-    SuccessOrExit(error = Get(SPINEL_PROP_NCP_VERSION, SPINEL_DATATYPE_UTF8_S, sVersion, sizeof(sVersion)));
+    mSpinelDriver.Init(aSpinelInterface, aResetRadio, aIidList, aIidListLength);
+    mSpinelDriver.SetFrameHandler(&HandleReceivedFrame, &HandleSavedFrame, this);
+
     SuccessOrExit(error = Get(SPINEL_PROP_HWADDR, SPINEL_DATATYPE_EUI64_S, sIeeeEui64.m8));
-
-    VerifyOrDie(IsRcp(supportsRcpApiVersion, supportsRcpMinHostApiVersion), OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
+    InitializeCaps(supportsRcpApiVersion, supportsRcpMinHostApiVersion);
 
     if (!aSkipRcpCompatibilityCheck)
     {
@@ -190,50 +175,6 @@
     mCallbacks = aCallbacks;
 }
 
-void RadioSpinel::ResetRcp(bool aResetRadio)
-{
-    bool hardwareReset;
-    bool resetDone = false;
-
-    // Avoid resetting the device twice in a row in Multipan RCP architecture
-    VerifyOrExit(!sIsReady, resetDone = true);
-
-    mWaitingKey = SPINEL_PROP_LAST_STATUS;
-
-    if (aResetRadio && (SendReset(SPINEL_RESET_STACK) == OT_ERROR_NONE) && (!sIsReady) &&
-        (WaitResponse(false) == OT_ERROR_NONE))
-    {
-        VerifyOrExit(sIsReady, resetDone = false);
-        LogInfo("Software reset RCP successfully");
-        ExitNow(resetDone = true);
-    }
-
-    hardwareReset = (mSpinelInterface->HardwareReset() == OT_ERROR_NONE);
-
-    if (hardwareReset)
-    {
-        SuccessOrExit(WaitResponse(false));
-    }
-
-    resetDone = true;
-
-    if (hardwareReset)
-    {
-        LogInfo("Hardware reset RCP successfully");
-    }
-    else
-    {
-        LogInfo("RCP self reset successfully");
-    }
-
-exit:
-    if (!resetDone)
-    {
-        LogCrit("Failed to reset RCP!");
-        DieNow(OT_EXIT_FAILURE);
-    }
-}
-
 otError RadioSpinel::CheckSpinelVersion(void)
 {
     otError      error = OT_ERROR_NONE;
@@ -256,68 +197,23 @@
     return error;
 }
 
-bool RadioSpinel::IsRcp(bool &aSupportsRcpApiVersion, bool &aSupportsRcpMinHostApiVersion)
+void RadioSpinel::InitializeCaps(bool &aSupportsRcpApiVersion, bool &aSupportsRcpMinHostApiVersion)
 {
-    uint8_t        capsBuffer[kCapsBufferSize];
-    const uint8_t *capsData         = capsBuffer;
-    spinel_size_t  capsLength       = sizeof(capsBuffer);
-    bool           supportsRawRadio = false;
-    bool           isRcp            = false;
-
-    aSupportsRcpApiVersion        = false;
-    aSupportsRcpMinHostApiVersion = false;
-
-    SuccessOrDie(Get(SPINEL_PROP_CAPS, SPINEL_DATATYPE_DATA_S, capsBuffer, &capsLength));
-
-    while (capsLength > 0)
+    if (!mSpinelDriver.CoprocessorHasCap(SPINEL_CAP_CONFIG_RADIO))
     {
-        unsigned int   capability;
-        spinel_ssize_t unpacked;
-
-        unpacked = spinel_datatype_unpack(capsData, capsLength, SPINEL_DATATYPE_UINT_PACKED_S, &capability);
-        VerifyOrDie(unpacked > 0, OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
-
-        if (capability == SPINEL_CAP_MAC_RAW)
-        {
-            supportsRawRadio = true;
-        }
-
-        if (capability == SPINEL_CAP_CONFIG_RADIO)
-        {
-            isRcp = true;
-        }
-
-        if (capability == SPINEL_CAP_OPENTHREAD_LOG_METADATA)
-        {
-            sSupportsLogStream = true;
-        }
-
-        if (capability == SPINEL_CAP_RCP_API_VERSION)
-        {
-            aSupportsRcpApiVersion = true;
-        }
-
-        if (capability == SPINEL_CAP_RCP_RESET_TO_BOOTLOADER)
-        {
-            sSupportsResetToBootloader = true;
-        }
-
-        if (capability == SPINEL_PROP_RCP_MIN_HOST_API_VERSION)
-        {
-            aSupportsRcpMinHostApiVersion = true;
-        }
-
-        capsData += unpacked;
-        capsLength -= static_cast<spinel_size_t>(unpacked);
+        LogCrit("The co-processor isn't a RCP!");
+        DieNow(OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
     }
-
-    if (!supportsRawRadio && isRcp)
+    if (!mSpinelDriver.CoprocessorHasCap(SPINEL_CAP_MAC_RAW))
     {
         LogCrit("RCP capability list does not include support for radio/raw mode");
         DieNow(OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
     }
 
-    return isRcp;
+    sSupportsLogStream            = mSpinelDriver.CoprocessorHasCap(SPINEL_CAP_OPENTHREAD_LOG_METADATA);
+    aSupportsRcpApiVersion        = mSpinelDriver.CoprocessorHasCap(SPINEL_CAP_RCP_API_VERSION);
+    sSupportsResetToBootloader    = mSpinelDriver.CoprocessorHasCap(SPINEL_CAP_RCP_RESET_TO_BOOTLOADER);
+    aSupportsRcpMinHostApiVersion = mSpinelDriver.CoprocessorHasCap(SPINEL_CAP_RCP_MIN_HOST_API_VERSION);
 }
 
 otError RadioSpinel::CheckRadioCapabilities(void)
@@ -414,53 +310,10 @@
     }
 
     // This allows implementing pseudo reset.
-    sIsReady = false;
     new (this) RadioSpinel();
 }
 
-void RadioSpinel::HandleReceivedFrame(void *aContext) { static_cast<RadioSpinel *>(aContext)->HandleReceivedFrame(); }
-
-void RadioSpinel::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);
-
-    // Accept spinel messages with the correct IID or broadcast IID.
-    spinel_iid_t iid = SPINEL_HEADER_GET_IID(header);
-
-    if (!IsFrameForUs(iid))
-    {
-        mRxFrameBuffer.DiscardFrame();
-        ExitNow();
-    }
-
-    VerifyOrExit(unpacked > 0 && (header & SPINEL_HEADER_FLAG) == SPINEL_HEADER_FLAG, error = OT_ERROR_PARSE);
-
-    if (SPINEL_HEADER_GET_TID(header) == 0)
-    {
-        HandleNotification(mRxFrameBuffer);
-    }
-    else
-    {
-        HandleResponse(mRxFrameBuffer.GetFrame(), mRxFrameBuffer.GetLength());
-        mRxFrameBuffer.DiscardFrame();
-    }
-
-exit:
-    if (error != OT_ERROR_NONE)
-    {
-        mRxFrameBuffer.DiscardFrame();
-        LogWarn("Error handling hdlc frame: %s", otThreadErrorToString(error));
-    }
-
-    UpdateParseErrorCount(error);
-}
-
-void RadioSpinel::HandleNotification(SpinelInterface::RxFrameBuffer &aFrameBuffer)
+void RadioSpinel::HandleNotification(const uint8_t *aFrame, uint16_t aLength, bool &aShouldSaveFrame)
 {
     spinel_prop_key_t key;
     spinel_size_t     len = 0;
@@ -468,11 +321,12 @@
     uint8_t          *data = nullptr;
     uint32_t          cmd;
     uint8_t           header;
-    otError           error           = OT_ERROR_NONE;
-    bool              shouldSaveFrame = false;
+    otError           error = OT_ERROR_NONE;
 
-    unpacked = spinel_datatype_unpack(aFrameBuffer.GetFrame(), aFrameBuffer.GetLength(), "CiiD", &header, &cmd, &key,
-                                      &data, &len);
+    aShouldSaveFrame = false;
+
+    unpacked = spinel_datatype_unpack(aFrame, aLength, "CiiD", &header, &cmd, &key, &data, &len);
+
     VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
     VerifyOrExit(SPINEL_HEADER_GET_TID(header) == 0, error = OT_ERROR_PARSE);
 
@@ -485,7 +339,7 @@
 
         if (!IsSafeToHandleNow(key))
         {
-            ExitNow(shouldSaveFrame = true);
+            ExitNow(aShouldSaveFrame = true);
         }
 
         HandleValueIs(key, data, static_cast<uint16_t>(len));
@@ -501,15 +355,6 @@
     }
 
 exit:
-    if (!shouldSaveFrame || aFrameBuffer.SaveFrame() != OT_ERROR_NONE)
-    {
-        aFrameBuffer.DiscardFrame();
-
-        if (shouldSaveFrame)
-        {
-            LogCrit("RX Spinel buffer full, dropped incoming frame");
-        }
-    }
 
     UpdateParseErrorCount(error);
     LogIfFail("Error processing notification", error);
@@ -670,6 +515,10 @@
                 ExitNow();
             }
 
+            // this clear is necessary in case the RCP has sent messages between disable and reset
+            mSpinelDriver.ClearRxBuffer();
+            mSpinelDriver.SetCoprocessorReady();
+
             LogInfo("RCP reset: %s", spinel_status_to_cstr(status));
             sIsReady = true;
         }
@@ -772,6 +621,20 @@
 }
 #endif
 
+otError RadioSpinel::SendReset(uint8_t aResetType)
+{
+    otError error;
+
+    if ((aResetType == SPINEL_RESET_BOOTLOADER) && !sSupportsResetToBootloader)
+    {
+        ExitNow(error = OT_ERROR_NOT_CAPABLE);
+    }
+    error = mSpinelDriver.SendReset(aResetType);
+
+exit:
+    return error;
+}
+
 otError RadioSpinel::ParseRadioFrame(otRadioFrame   &aFrame,
                                      const uint8_t  *aBuffer,
                                      uint16_t        aLength,
@@ -852,19 +715,6 @@
     return error;
 }
 
-void RadioSpinel::ProcessFrameQueue(void)
-{
-    uint8_t *frame = nullptr;
-    uint16_t length;
-
-    while (mRxFrameBuffer.GetNextSavedFrame(frame, length) == OT_ERROR_NONE)
-    {
-        HandleNotification(frame, length);
-    }
-
-    mRxFrameBuffer.ClearSavedFrames();
-}
-
 void RadioSpinel::RadioReceive(void)
 {
     if (!mIsPromiscuous)
@@ -929,20 +779,7 @@
 
 void RadioSpinel::Process(const void *aContext)
 {
-    if (mRxFrameBuffer.HasSavedFrame())
-    {
-        ProcessFrameQueue();
-        RecoverFromRcpFailure();
-    }
-
-    mSpinelInterface->Process(aContext);
-    RecoverFromRcpFailure();
-
-    if (mRxFrameBuffer.HasSavedFrame())
-    {
-        ProcessFrameQueue();
-        RecoverFromRcpFailure();
-    }
+    mSpinelDriver.Process(aContext);
 
     ProcessRadioStateMachine();
     RecoverFromRcpFailure();
@@ -1109,7 +946,17 @@
 
 otError RadioSpinel::EnableSrcMatch(bool aEnable)
 {
-    return Set(SPINEL_PROP_MAC_SRC_MATCH_ENABLED, SPINEL_DATATYPE_BOOL_S, aEnable);
+    otError error;
+
+    SuccessOrExit(error = Set(SPINEL_PROP_MAC_SRC_MATCH_ENABLED, SPINEL_DATATYPE_BOOL_S, aEnable));
+
+#if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
+    mSrcMatchSet     = true;
+    mSrcMatchEnabled = aEnable;
+#endif
+
+exit:
+    return error;
 }
 
 otError RadioSpinel::AddSrcMatchShortEntry(uint16_t aShortAddress)
@@ -1536,7 +1383,7 @@
             }
             ExitNow(mError = OT_ERROR_RESPONSE_TIMEOUT);
         }
-    } while (mWaitingTid || !sIsReady);
+    } while (mWaitingTid);
 
     LogIfFail("Error waiting response", mError);
     // This indicates end of waiting response.
@@ -1570,65 +1417,6 @@
     return tid;
 }
 
-otError RadioSpinel::SendReset(uint8_t aResetType)
-{
-    otError        error = OT_ERROR_NONE;
-    uint8_t        buffer[kMaxSpinelFrame];
-    spinel_ssize_t packed;
-
-    if ((aResetType == SPINEL_RESET_BOOTLOADER) && !sSupportsResetToBootloader)
-    {
-        ExitNow(error = OT_ERROR_NOT_CAPABLE);
-    }
-
-    // Pack the header, command and key
-    packed = spinel_datatype_pack(buffer, sizeof(buffer), SPINEL_DATATYPE_COMMAND_S SPINEL_DATATYPE_UINT8_S,
-                                  SPINEL_HEADER_FLAG | SPINEL_HEADER_IID(mIid), SPINEL_CMD_RESET, aResetType);
-
-    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;
-}
-
-otError RadioSpinel::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];
-    spinel_ssize_t packed;
-    uint16_t       offset;
-
-    // Pack the header, command and key
-    packed = spinel_datatype_pack(buffer, sizeof(buffer), "Cii", SPINEL_HEADER_FLAG | SPINEL_HEADER_IID(mIid) | tid,
-                                  aCommand, aKey);
-
-    VerifyOrExit(packed > 0 && static_cast<size_t>(packed) <= sizeof(buffer), error = OT_ERROR_NO_BUFS);
-
-    offset = static_cast<uint16_t>(packed);
-
-    // Pack the data (if any)
-    if (aFormat)
-    {
-        packed = spinel_datatype_vpack(buffer + offset, sizeof(buffer) - offset, aFormat, args);
-        VerifyOrExit(packed > 0 && static_cast<size_t>(packed + offset) <= sizeof(buffer), error = OT_ERROR_NO_BUFS);
-
-        offset += static_cast<uint16_t>(packed);
-    }
-
-    SuccessOrExit(error = mSpinelInterface->SendFrame(buffer, offset));
-    LogSpinelFrame(buffer, offset, true);
-
-exit:
-    return error;
-}
-
 otError RadioSpinel::RequestV(uint32_t command, spinel_prop_key_t aKey, const char *aFormat, va_list aArgs)
 {
     otError      error = OT_ERROR_NONE;
@@ -1636,7 +1424,7 @@
 
     VerifyOrExit(tid > 0, error = OT_ERROR_BUSY);
 
-    error = SendCommand(command, aKey, tid, aFormat, aArgs);
+    error = mSpinelDriver.SendCommand(command, aKey, tid, aFormat, aArgs);
     SuccessOrExit(error);
 
     if (aKey == SPINEL_PROP_STREAM_RAW)
@@ -1800,6 +1588,43 @@
 
     mTransmitFrame = &aFrame;
 
+#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    if (mTransmitFrame->mInfo.mTxInfo.mIeInfo->mTimeIeOffset != 0)
+    {
+        uint64_t netRadioTime = otPlatRadioGetNow(mInstance);
+        uint64_t netSyncTime;
+        uint8_t *timeIe = mTransmitFrame->mPsdu + mTransmitFrame->mInfo.mTxInfo.mIeInfo->mTimeIeOffset;
+
+        if (netRadioTime == UINT64_MAX)
+        {
+            // If we can't get the radio time, get the platform time
+            netSyncTime = static_cast<uint64_t>(static_cast<int64_t>(otPlatTimeGet()) +
+                                                mTransmitFrame->mInfo.mTxInfo.mIeInfo->mNetworkTimeOffset);
+        }
+        else
+        {
+            uint32_t transmitDelay = 0;
+
+            // If supported, add a delay and transmit the network time at a precise moment
+#if !OPENTHREAD_MTD && OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
+            transmitDelay                                  = kTxWaitUs / 10;
+            mTransmitFrame->mInfo.mTxInfo.mTxDelayBaseTime = static_cast<uint32_t>(netRadioTime);
+            mTransmitFrame->mInfo.mTxInfo.mTxDelay         = transmitDelay;
+#endif
+            netSyncTime = static_cast<uint64_t>(static_cast<int64_t>(netRadioTime) + transmitDelay +
+                                                mTransmitFrame->mInfo.mTxInfo.mIeInfo->mNetworkTimeOffset);
+        }
+
+        *(timeIe++) = mTransmitFrame->mInfo.mTxInfo.mIeInfo->mTimeSyncSeq;
+
+        for (uint8_t i = 0; i < sizeof(uint64_t); i++)
+        {
+            *(timeIe++) = static_cast<uint8_t>(netSyncTime & 0xff);
+            netSyncTime = netSyncTime >> 8;
+        }
+    }
+#endif // OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+
     // `otPlatRadioTxStarted()` is triggered immediately for now, which may be earlier than real started time.
     mCallbacks.mTxStarted(mInstance, mTransmitFrame);
 
@@ -2078,13 +1903,10 @@
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
     mRcpFailure = kRcpFailureTimeout;
 #else
-    if (!sIsReady)
-    {
-        LogCrit("Failed to communicate with RCP - no response from RCP during initialization");
-        LogCrit("This is not a bug and typically due a config error (wrong URL parameters) or bad RCP image:");
-        LogCrit("- Make sure RCP is running the correct firmware");
-        LogCrit("- Double check the config parameters passed as `RadioURL` input");
-    }
+    LogCrit("Failed to communicate with RCP - no response from RCP during initialization");
+    LogCrit("This is not a bug and typically due a config error (wrong URL parameters) or bad RCP image:");
+    LogCrit("- Make sure RCP is running the correct firmware");
+    LogCrit("- Double check the config parameters passed as `RadioURL` input");
 
     DieNow(OT_EXIT_RADIO_SPINEL_NO_RESPONSE);
 #endif
@@ -2121,7 +1943,17 @@
     LogWarn("Trying to recover (%d/%d)", mRcpFailureCount, kMaxFailureCount);
 
     mState = kStateDisabled;
-    mRxFrameBuffer.Clear();
+
+    mSpinelDriver.ClearRxBuffer();
+    if (skipReset)
+    {
+        mSpinelDriver.SetCoprocessorReady();
+    }
+    else
+    {
+        mSpinelDriver.ResetCoprocessor(mResetRadioOnStartup);
+    }
+
     mCmdTidsInUse = 0;
     mCmdNextTid   = 1;
     mTxRadioTid   = 0;
@@ -2129,15 +1961,6 @@
     mError        = OT_ERROR_NONE;
     mIsTimeSynced = false;
 
-    if (skipReset)
-    {
-        sIsReady = true;
-    }
-    else
-    {
-        ResetRcp(mResetRadioOnStartup);
-    }
-
     SuccessOrDie(Set(SPINEL_PROP_PHY_ENABLED, SPINEL_DATATYPE_BOOL_S, true));
     mState = kStateSleep;
 
@@ -2185,6 +2008,35 @@
 #endif // OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
 }
 
+void RadioSpinel::HandleReceivedFrame(const uint8_t *aFrame,
+                                      uint16_t       aLength,
+                                      uint8_t        aHeader,
+                                      bool          &aSave,
+                                      void          *aContext)
+{
+    static_cast<RadioSpinel *>(aContext)->HandleReceivedFrame(aFrame, aLength, aHeader, aSave);
+}
+
+void RadioSpinel::HandleReceivedFrame(const uint8_t *aFrame, uint16_t aLength, uint8_t aHeader, bool &aShouldSaveFrame)
+{
+    if (SPINEL_HEADER_GET_TID(aHeader) == 0)
+    {
+        HandleNotification(aFrame, aLength, aShouldSaveFrame);
+    }
+    else
+    {
+        HandleResponse(aFrame, aLength);
+        aShouldSaveFrame = false;
+    }
+}
+
+void RadioSpinel::HandleSavedFrame(const uint8_t *aFrame, uint16_t aLength, void *aContext)
+{
+    static_cast<RadioSpinel *>(aContext)->HandleSavedFrame(aFrame, aLength);
+}
+
+void RadioSpinel::HandleSavedFrame(const uint8_t *aFrame, uint16_t aLength) { HandleNotification(aFrame, aLength); }
+
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
 void RadioSpinel::RestoreProperties(void)
 {
@@ -2237,6 +2089,11 @@
             Insert(SPINEL_PROP_MAC_SRC_MATCH_EXTENDED_ADDRESSES, SPINEL_DATATYPE_EUI64_S, mSrcMatchExtEntries[i].m8));
     }
 
+    if (mSrcMatchSet)
+    {
+        SuccessOrDie(Set(SPINEL_PROP_MAC_SRC_MATCH_ENABLED, SPINEL_DATATYPE_BOOL_S, mSrcMatchEnabled));
+    }
+
     if (mCcaEnergyDetectThresholdSet)
     {
         SuccessOrDie(Set(SPINEL_PROP_PHY_CCA_THRESHOLD, SPINEL_DATATYPE_INT8_S, mCcaEnergyDetectThreshold));
@@ -2448,647 +2305,6 @@
 }
 #endif // OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
 
-uint32_t RadioSpinel::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);
-}
-
-void RadioSpinel::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, iid:%d, tid:%u, cmd:%s", prefix,
-                      SPINEL_HEADER_GET_FLAG(header), SPINEL_HEADER_GET_IID(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);
-            OT_UNUSED_VARIABLE(start); // Avoid static analysis error
-            LogDebg("%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);
-            OT_UNUSED_VARIABLE(start); // Avoid static analysis error
-            LogDebg("%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);
-
-        LogDebg("%s ...", buf);
-        LogDebg(" txRequest:%lu", ToUlong(metrics.mNumTxRequest));
-        LogDebg(" txGrantImmediate:%lu", ToUlong(metrics.mNumTxGrantImmediate));
-        LogDebg(" txGrantWait:%lu", ToUlong(metrics.mNumTxGrantWait));
-        LogDebg(" txGrantWaitActivated:%lu", ToUlong(metrics.mNumTxGrantWaitActivated));
-        LogDebg(" txGrantWaitTimeout:%lu", ToUlong(metrics.mNumTxGrantWaitTimeout));
-        LogDebg(" txGrantDeactivatedDuringRequest:%lu", ToUlong(metrics.mNumTxGrantDeactivatedDuringRequest));
-        LogDebg(" txDelayedGrant:%lu", ToUlong(metrics.mNumTxDelayedGrant));
-        LogDebg(" avgTxRequestToGrantTime:%lu", ToUlong(metrics.mAvgTxRequestToGrantTime));
-        LogDebg(" rxRequest:%lu", ToUlong(metrics.mNumRxRequest));
-        LogDebg(" rxGrantImmediate:%lu", ToUlong(metrics.mNumRxGrantImmediate));
-        LogDebg(" rxGrantWait:%lu", ToUlong(metrics.mNumRxGrantWait));
-        LogDebg(" rxGrantWaitActivated:%lu", ToUlong(metrics.mNumRxGrantWaitActivated));
-        LogDebg(" rxGrantWaitTimeout:%lu", ToUlong(metrics.mNumRxGrantWaitTimeout));
-        LogDebg(" rxGrantDeactivatedDuringRequest:%lu", ToUlong(metrics.mNumRxGrantDeactivatedDuringRequest));
-        LogDebg(" rxDelayedGrant:%lu", ToUlong(metrics.mNumRxDelayedGrant));
-        LogDebg(" avgRxRequestToGrantTime:%lu", ToUlong(metrics.mAvgRxRequestToGrantTime));
-        LogDebg(" rxGrantNone:%lu", ToUlong(metrics.mNumRxGrantNone));
-        LogDebg(" 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 (spinel_size_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 (unsigned int 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:
-    OT_UNUSED_VARIABLE(start); // Avoid static analysis error
-    if (error == OT_ERROR_NONE)
-    {
-        LogDebg("%s", buf);
-    }
-    else if (prefix != nullptr)
-    {
-        LogDebg("%s, failed to parse spinel frame !", prefix);
-    }
-}
-
 otError RadioSpinel::SpinelStatusToOtError(spinel_status_t aStatus)
 {
     otError ret;
@@ -3166,62 +2382,5 @@
     return ret;
 }
 
-void RadioSpinel::LogIfFail(const char *aText, otError aError)
-{
-    OT_UNUSED_VARIABLE(aText);
-
-    if (aError != OT_ERROR_NONE && aError != OT_ERROR_NO_ACK)
-    {
-        LogWarn("%s: %s", aText, otThreadErrorToString(aError));
-    }
-}
-
-static const char kModuleName[] = "RadioSpinel";
-
-void RadioSpinel::LogCrit(const char *aFormat, ...)
-{
-    va_list args;
-
-    va_start(args, aFormat);
-    otLogPlatArgs(OT_LOG_LEVEL_CRIT, kModuleName, aFormat, args);
-    va_end(args);
-}
-
-void RadioSpinel::LogWarn(const char *aFormat, ...)
-{
-    va_list args;
-
-    va_start(args, aFormat);
-    otLogPlatArgs(OT_LOG_LEVEL_WARN, kModuleName, aFormat, args);
-    va_end(args);
-}
-
-void RadioSpinel::LogNote(const char *aFormat, ...)
-{
-    va_list args;
-
-    va_start(args, aFormat);
-    otLogPlatArgs(OT_LOG_LEVEL_NOTE, kModuleName, aFormat, args);
-    va_end(args);
-}
-
-void RadioSpinel::LogInfo(const char *aFormat, ...)
-{
-    va_list args;
-
-    va_start(args, aFormat);
-    otLogPlatArgs(OT_LOG_LEVEL_INFO, kModuleName, aFormat, args);
-    va_end(args);
-}
-
-void RadioSpinel::LogDebg(const char *aFormat, ...)
-{
-    va_list args;
-
-    va_start(args, aFormat);
-    otLogPlatArgs(OT_LOG_LEVEL_DEBG, kModuleName, aFormat, args);
-    va_end(args);
-}
-
 } // namespace Spinel
 } // namespace ot
diff --git a/src/lib/spinel/radio_spinel.hpp b/src/lib/spinel/radio_spinel.hpp
index 7cdd1f8..1117cfb 100644
--- a/src/lib/spinel/radio_spinel.hpp
+++ b/src/lib/spinel/radio_spinel.hpp
@@ -38,24 +38,16 @@
 
 #include "openthread-spinel-config.h"
 #include "core/radio/max_power_table.hpp"
+#include "lib/spinel/logger.hpp"
 #include "lib/spinel/radio_spinel_metrics.h"
 #include "lib/spinel/spinel.h"
+#include "lib/spinel/spinel_driver.hpp"
 #include "lib/spinel/spinel_interface.hpp"
 #include "ncp/ncp_config.h"
 
 namespace ot {
 namespace Spinel {
 
-/**
- * Maximum number of Spinel Interface IDs.
- *
- */
-#if OPENTHREAD_CONFIG_MULTIPAN_RCP_ENABLE
-static constexpr uint8_t kSpinelHeaderMaxNumIid = 4;
-#else
-static constexpr uint8_t kSpinelHeaderMaxNumIid = 1;
-#endif
-
 struct RadioSpinelCallbacks
 {
     /**
@@ -148,7 +140,7 @@
  * co-processor(RCP).
  *
  */
-class RadioSpinel
+class RadioSpinel : private Logger
 {
 public:
     /**
@@ -349,14 +341,6 @@
     otError SetFemLnaGain(int8_t aGain);
 
     /**
-     * Returns the radio sw version string.
-     *
-     * @returns A pointer to the radio version string.
-     *
-     */
-    const char *GetVersion(void) const { return sVersion; }
-
-    /**
      * Returns the radio capabilities.
      *
      * @returns The radio capability bit vector.
@@ -724,14 +708,6 @@
     uint32_t GetRadioChannelMask(bool aPreferred);
 
     /**
-     * Processes a received Spinel frame.
-     *
-     * The newly received frame is available in `RxFrameBuffer` from `SpinelInterface::GetRxFrameBuffer()`.
-     *
-     */
-    void HandleReceivedFrame(void);
-
-    /**
      * Sets MAC key and key index to RCP.
      *
      * @param[in] aKeyIdMode  The key ID mode.
@@ -834,26 +810,12 @@
 #endif
 
     /**
-     * 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 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 &aSupportsRcpMinHostApiVersion);
-
-    /**
      * Checks whether there is pending frame in the buffer.
      *
      * @returns Whether there is pending frame in the buffer.
      *
      */
-    bool HasPendingFrame(void) const { return mRxFrameBuffer.HasSavedFrame(); }
+    bool HasPendingFrame(void) const { return mSpinelDriver.HasPendingFrame(); }
 
     /**
      * Returns the next timepoint to recalculate RCP time offset.
@@ -880,6 +842,14 @@
     uint32_t GetBusSpeed(void) const;
 
     /**
+     * Returns the co-processor sw version string.
+     *
+     * @returns A pointer to the co-processor version string.
+     *
+     */
+    const char *GetVersion(void) const { return mSpinelDriver.GetVersion(); }
+
+    /**
      * Sets the max transmit power.
      *
      * @param[in] aChannel    The radio channel.
@@ -968,13 +938,13 @@
     otError Remove(spinel_prop_key_t aKey, const char *aFormat, ...);
 
     /**
-     * Tries to reset the co-processor.
+     * Sends a reset command to the RCP.
      *
-     * @prarm[in] aResetType    The reset type, SPINEL_RESET_PLATFORM, SPINEL_RESET_STACK, or SPINEL_RESET_BOOTLOADER.
+     * @param[in] aResetType The reset type, SPINEL_RESET_PLATFORM, SPINEL_RESET_STACK, or SPINEL_RESET_BOOTLOADER.
      *
-     * @retval  OT_ERROR_NONE               Successfully removed item from the property.
+     * @retval  OT_ERROR_NONE               Successfully sent the reset command.
      * @retval  OT_ERROR_BUSY               Failed due to another operation is on going.
-     * @retval  OT_ERROR_NOT_CAPABLE        Requested reset type is not supported by the co-processor
+     * @retval  OT_ERROR_NOT_CAPABLE        Requested reset type is not supported by the co-processor.
      *
      */
     otError SendReset(uint8_t aResetType);
@@ -1107,7 +1077,6 @@
 private:
     enum
     {
-        kMaxSpinelFrame        = SPINEL_FRAME_MAX_SIZE,
         kMaxWaitTime           = 2000, ///< Max time to wait for response in milliseconds.
         kVersionStringSize     = 128,  ///< Max size of version string.
         kCapsBufferSize        = 100,  ///< Max buffer size used to store `SPINEL_PROP_CAPS` value.
@@ -1132,12 +1101,10 @@
 
     typedef otError (RadioSpinel::*ResponseHandler)(const uint8_t *aBuffer, uint16_t aLength);
 
-    static void HandleReceivedFrame(void *aContext);
-
-    void    ResetRcp(bool aResetRadio);
     otError CheckSpinelVersion(void);
     otError CheckRadioCapabilities(void);
     otError CheckRcpApiVersion(bool aSupportsRcpApiVersion, bool aSupportsRcpMinHostApiVersion);
+    void    InitializeCaps(bool &aSupportsRcpApiVersion, bool &aSupportsRcpMinHostApiVersion);
 
     /**
      * Triggers a state transfer of the state machine.
@@ -1172,11 +1139,6 @@
                                         const char       *aFormat,
                                         va_list           aArgs);
     otError WaitResponse(bool aHandleRcpTimeout = true);
-    otError SendCommand(uint32_t          aCommand,
-                        spinel_prop_key_t aKey,
-                        spinel_tid_t      aTid,
-                        const char       *aFormat,
-                        va_list           aArgs);
     otError ParseRadioFrame(otRadioFrame &aFrame, const uint8_t *aBuffer, uint16_t aLength, spinel_ssize_t &aUnpacked);
 
     /**
@@ -1195,18 +1157,7 @@
         return !(aKey == SPINEL_PROP_STREAM_RAW || aKey == SPINEL_PROP_MAC_ENERGY_SCAN_RESULT);
     }
 
-    /**
-     * Checks whether given interface ID is part of list of IIDs to be allowed.
-     *
-     * @param[in] aIid    Spinel Interface ID.
-     *
-     * @retval  TRUE    Given IID present in allow list.
-     * @retval  FALSE   Otherwise.
-     *
-     */
-    inline bool IsFrameForUs(spinel_iid_t aIid);
-
-    void HandleNotification(SpinelInterface::RxFrameBuffer &aFrameBuffer);
+    void HandleNotification(const uint8_t *aFrame, uint16_t aLength, bool &aShouldSaveFrame);
     void HandleNotification(const uint8_t *aFrame, uint16_t aLength);
     void HandleValueIs(spinel_prop_key_t aKey, const uint8_t *aBuffer, uint16_t aLength);
 
@@ -1224,6 +1175,15 @@
     void HandleRcpTimeout(void);
     void RecoverFromRcpFailure(void);
 
+    static void HandleReceivedFrame(const uint8_t *aFrame,
+                                    uint16_t       aLength,
+                                    uint8_t        aHeader,
+                                    bool          &aSave,
+                                    void          *aContext);
+    void        HandleReceivedFrame(const uint8_t *aFrame, uint16_t aLength, uint8_t aHeader, bool &aShouldSaveFrame);
+    static void HandleSavedFrame(const uint8_t *aFrame, uint16_t aLength, void *aContext);
+    void        HandleSavedFrame(const uint8_t *aFrame, uint16_t aLength);
+
     void UpdateParseErrorCount(otError aError)
     {
         mRadioSpinelMetrics.mSpinelParseErrorCount += (aError == OT_ERROR_PARSE) ? 1 : 0;
@@ -1238,17 +1198,6 @@
     static otError ReadMacKey(const otMacKeyMaterial &aKeyMaterial, otMacKey &aKey);
 #endif
 
-    static void LogIfFail(const char *aText, otError aError);
-
-    static void LogCrit(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
-    static void LogWarn(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
-    static void LogNote(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
-    static void LogInfo(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
-    static void LogDebg(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
-
-    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;
@@ -1276,6 +1225,10 @@
     otRadioFrame  mAckRadioFrame;
     otRadioFrame *mTransmitFrame; ///< Points to the frame to send
 
+#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
+    otRadioIeInfo mTxIeInfo;
+#endif
+
     otExtAddress        mExtendedAddress;
     uint16_t            mShortAddress;
     uint16_t            mPanId;
@@ -1325,6 +1278,7 @@
     int8_t       mFemLnaGain;
     uint32_t     mMacFrameCounter;
     bool         mCoexEnabled : 1;
+    bool         mSrcMatchEnabled : 1;
 
     bool mMacKeySet : 1;                   ///< Whether MAC key has been set.
     bool mCcaEnergyDetectThresholdSet : 1; ///< Whether CCA energy detect threshold has been set.
@@ -1333,6 +1287,7 @@
     bool mFemLnaGainSet : 1;               ///< Whether FEM LNA gain has been set.
     bool mEnergyScanning : 1;              ///< If fails while scanning, restarts scanning.
     bool mMacFrameCounterSet : 1;          ///< Whether the MAC frame counter has been set.
+    bool mSrcMatchSet : 1;                 ///< Whether the source match feature has been set.
 
 #endif // OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
 
@@ -1354,6 +1309,8 @@
     otRadioSpinelVendorRestorePropertiesCallback mVendorRestorePropertiesCallback;
     void                                        *mVendorRestorePropertiesContext;
 #endif
+
+    SpinelDriver mSpinelDriver;
 };
 
 } // namespace Spinel
diff --git a/src/lib/spinel/spinel.h b/src/lib/spinel/spinel.h
index d91a039..c1865f3 100644
--- a/src/lib/spinel/spinel.h
+++ b/src/lib/spinel/spinel.h
@@ -607,6 +607,7 @@
     SPINEL_IPV6_ICMP_PING_OFFLOAD_UNICAST_ONLY   = 1,
     SPINEL_IPV6_ICMP_PING_OFFLOAD_MULTICAST_ONLY = 2,
     SPINEL_IPV6_ICMP_PING_OFFLOAD_ALL            = 3,
+    SPINEL_IPV6_ICMP_PING_OFFLOAD_RLOC_ALOC_ONLY = 4,
 } spinel_ipv6_icmp_ping_offload_mode_t;
 
 typedef enum
@@ -3457,6 +3458,7 @@
      *   SPINEL_IPV6_ICMP_PING_OFFLOAD_UNICAST_ONLY   = 1
      *   SPINEL_IPV6_ICMP_PING_OFFLOAD_MULTICAST_ONLY = 2
      *   SPINEL_IPV6_ICMP_PING_OFFLOAD_ALL            = 3
+     *   SPINEL_IPV6_ICMP_PING_OFFLOAD_RLOC_ALOC_ONLY = 4
      *
      * Default value is `NET_IPV6_ICMP_PING_OFFLOAD_DISABLED`.
      *
diff --git a/src/lib/spinel/spinel_driver.cpp b/src/lib/spinel/spinel_driver.cpp
new file mode 100644
index 0000000..dcf2445
--- /dev/null
+++ b/src/lib/spinel/spinel_driver.cpp
@@ -0,0 +1,466 @@
+/*
+ *  Copyright (c) 2024, 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 "spinel_driver.hpp"
+
+#include <assert.h>
+
+#include <openthread/platform/time.h>
+
+#include "common/code_utils.hpp"
+#include "common/new.hpp"
+#include "common/num_utils.hpp"
+#include "lib/platform/exit_code.h"
+#include "lib/spinel/spinel.h"
+
+namespace ot {
+namespace Spinel {
+
+constexpr spinel_tid_t sTid = 1; ///< In Spinel Driver, only use Tid as 1.
+
+SpinelDriver::SpinelDriver(void)
+    : Logger("SpinelDriver")
+    , mSpinelInterface(nullptr)
+    , mWaitingKey(SPINEL_PROP_LAST_STATUS)
+    , mIsWaitingForResponse(false)
+    , mIid(SPINEL_HEADER_INVALID_IID)
+    , mSpinelVersionMajor(-1)
+    , mSpinelVersionMinor(-1)
+    , mIsCoprocessorReady(false)
+{
+    memset(mVersion, 0, sizeof(mVersion));
+
+    mReceivedFrameHandler = &HandleInitialFrame;
+    mFrameHandlerContext  = this;
+}
+
+void SpinelDriver::Init(SpinelInterface    &aSpinelInterface,
+                        bool                aSoftwareReset,
+                        const spinel_iid_t *aIidList,
+                        uint8_t             aIidListLength)
+{
+    mSpinelInterface = &aSpinelInterface;
+    mRxFrameBuffer.Clear();
+    SuccessOrDie(mSpinelInterface->Init(HandleReceivedFrame, this, mRxFrameBuffer));
+
+    VerifyOrDie(aIidList != nullptr, OT_EXIT_INVALID_ARGUMENTS);
+    VerifyOrDie(aIidListLength != 0 && aIidListLength <= mIidList.GetMaxSize(), OT_EXIT_INVALID_ARGUMENTS);
+
+    for (uint8_t i = 0; i < aIidListLength; i++)
+    {
+        SuccessOrDie(mIidList.PushBack(aIidList[i]));
+    }
+    mIid = aIidList[0];
+
+    ResetCoprocessor(aSoftwareReset);
+    SuccessOrDie(CheckSpinelVersion());
+    SuccessOrDie(GetCoprocessorVersion());
+    SuccessOrDie(GetCoprocessorCaps());
+}
+
+void SpinelDriver::Deinit(void)
+{
+    // This allows implementing pseudo reset.
+    new (this) SpinelDriver();
+}
+
+otError SpinelDriver::SendReset(uint8_t aResetType)
+{
+    otError        error = OT_ERROR_NONE;
+    uint8_t        buffer[kMaxSpinelFrame];
+    spinel_ssize_t packed;
+
+    // Pack the header, command and key
+    packed = spinel_datatype_pack(buffer, sizeof(buffer), SPINEL_DATATYPE_COMMAND_S SPINEL_DATATYPE_UINT8_S,
+                                  SPINEL_HEADER_FLAG | SPINEL_HEADER_IID(mIid), SPINEL_CMD_RESET, aResetType);
+
+    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 /* aTx */);
+
+exit:
+    return error;
+}
+
+void SpinelDriver::ResetCoprocessor(bool aSoftwareReset)
+{
+    bool hardwareReset;
+    bool resetDone = false;
+
+    // Avoid resetting the device twice in a row in Multipan RCP architecture
+    VerifyOrExit(!mIsCoprocessorReady, resetDone = true);
+
+    mWaitingKey = SPINEL_PROP_LAST_STATUS;
+
+    if (aSoftwareReset && (SendReset(SPINEL_RESET_STACK) == OT_ERROR_NONE) && (!mIsCoprocessorReady) &&
+        (WaitResponse() == OT_ERROR_NONE))
+    {
+        VerifyOrExit(mIsCoprocessorReady, resetDone = false);
+        LogCrit("Software reset co-processor successfully");
+        ExitNow(resetDone = true);
+    }
+
+    hardwareReset = (mSpinelInterface->HardwareReset() == OT_ERROR_NONE);
+
+    if (hardwareReset)
+    {
+        SuccessOrExit(WaitResponse());
+    }
+
+    resetDone = true;
+
+    if (hardwareReset)
+    {
+        LogInfo("Hardware reset co-processor successfully");
+    }
+    else
+    {
+        LogInfo("co-processor self reset successfully");
+    }
+
+exit:
+    if (!resetDone)
+    {
+        LogCrit("Failed to reset co-processor!");
+        DieNow(OT_EXIT_FAILURE);
+    }
+}
+
+void SpinelDriver::Process(const void *aContext)
+{
+    if (mRxFrameBuffer.HasSavedFrame())
+    {
+        ProcessFrameQueue();
+    }
+
+    mSpinelInterface->Process(aContext);
+
+    if (mRxFrameBuffer.HasSavedFrame())
+    {
+        ProcessFrameQueue();
+    }
+}
+
+otError SpinelDriver::SendCommand(uint32_t aCommand, spinel_prop_key_t aKey, spinel_tid_t aTid)
+{
+    otError        error = OT_ERROR_NONE;
+    uint8_t        buffer[kMaxSpinelFrame];
+    spinel_ssize_t packed;
+    uint16_t       offset;
+
+    // Pack the header, command and key
+    packed = spinel_datatype_pack(buffer, sizeof(buffer), "Cii", SPINEL_HEADER_FLAG | SPINEL_HEADER_IID(mIid) | aTid,
+                                  aCommand, aKey);
+
+    VerifyOrExit(packed > 0 && static_cast<size_t>(packed) <= sizeof(buffer), error = OT_ERROR_NO_BUFS);
+
+    offset = static_cast<uint16_t>(packed);
+
+    SuccessOrExit(error = mSpinelInterface->SendFrame(buffer, offset));
+
+exit:
+    return error;
+}
+
+otError SpinelDriver::SendCommand(uint32_t          aCommand,
+                                  spinel_prop_key_t aKey,
+                                  spinel_tid_t      aTid,
+                                  const char       *aFormat,
+                                  va_list           aArgs)
+{
+    otError        error = OT_ERROR_NONE;
+    uint8_t        buffer[kMaxSpinelFrame];
+    spinel_ssize_t packed;
+    uint16_t       offset;
+
+    // Pack the header, command and key
+    packed = spinel_datatype_pack(buffer, sizeof(buffer), "Cii", SPINEL_HEADER_FLAG | SPINEL_HEADER_IID(mIid) | aTid,
+                                  aCommand, aKey);
+
+    VerifyOrExit(packed > 0 && static_cast<size_t>(packed) <= sizeof(buffer), error = OT_ERROR_NO_BUFS);
+
+    offset = static_cast<uint16_t>(packed);
+
+    // Pack the data (if any)
+    if (aFormat)
+    {
+        packed = spinel_datatype_vpack(buffer + offset, sizeof(buffer) - offset, aFormat, aArgs);
+        VerifyOrExit(packed > 0 && static_cast<size_t>(packed + offset) <= sizeof(buffer), error = OT_ERROR_NO_BUFS);
+
+        offset += static_cast<uint16_t>(packed);
+    }
+
+    SuccessOrExit(error = mSpinelInterface->SendFrame(buffer, offset));
+
+exit:
+    return error;
+}
+
+void SpinelDriver::SetFrameHandler(ReceivedFrameHandler aReceivedFrameHandler,
+                                   SavedFrameHandler    aSavedFrameHandler,
+                                   void                *aContext)
+{
+    mReceivedFrameHandler = aReceivedFrameHandler;
+    mSavedFrameHandler    = aSavedFrameHandler;
+    mFrameHandlerContext  = aContext;
+}
+
+otError SpinelDriver::WaitResponse(void)
+{
+    otError  error = OT_ERROR_NONE;
+    uint64_t end   = otPlatTimeGet() + kMaxWaitTime * kUsPerMs;
+
+    LogDebg("Waiting response: key=%lu", ToUlong(mWaitingKey));
+
+    do
+    {
+        uint64_t now = otPlatTimeGet();
+
+        if ((end <= now) || (mSpinelInterface->WaitForFrame(end - now) != OT_ERROR_NONE))
+        {
+            LogWarn("Wait for response timeout");
+            ExitNow(error = OT_ERROR_RESPONSE_TIMEOUT);
+        }
+    } while (mIsWaitingForResponse || !mIsCoprocessorReady);
+
+    mWaitingKey = SPINEL_PROP_LAST_STATUS;
+
+exit:
+    return error;
+}
+
+void SpinelDriver::HandleReceivedFrame(void *aContext) { static_cast<SpinelDriver *>(aContext)->HandleReceivedFrame(); }
+
+void SpinelDriver::HandleReceivedFrame(void)
+{
+    otError        error = OT_ERROR_NONE;
+    uint8_t        header;
+    spinel_ssize_t unpacked;
+    bool           shouldSave = true;
+    spinel_iid_t   iid;
+
+    LogSpinelFrame(mRxFrameBuffer.GetFrame(), mRxFrameBuffer.GetLength(), false);
+    unpacked = spinel_datatype_unpack(mRxFrameBuffer.GetFrame(), mRxFrameBuffer.GetLength(), "C", &header);
+
+    // Accept spinel messages with the correct IID or broadcast IID.
+    iid = SPINEL_HEADER_GET_IID(header);
+
+    if (!mIidList.Contains(iid))
+    {
+        mRxFrameBuffer.DiscardFrame();
+        ExitNow();
+    }
+
+    VerifyOrExit(unpacked > 0 && (header & SPINEL_HEADER_FLAG) == SPINEL_HEADER_FLAG, error = OT_ERROR_PARSE);
+
+    assert(mReceivedFrameHandler != nullptr && mFrameHandlerContext != nullptr);
+    mReceivedFrameHandler(mRxFrameBuffer.GetFrame(), mRxFrameBuffer.GetLength(), header, shouldSave,
+                          mFrameHandlerContext);
+
+    if (shouldSave)
+    {
+        error = mRxFrameBuffer.SaveFrame();
+    }
+    else
+    {
+        mRxFrameBuffer.DiscardFrame();
+    }
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        mRxFrameBuffer.DiscardFrame();
+        LogWarn("Error handling spinel frame: %s", otThreadErrorToString(error));
+    }
+}
+
+void SpinelDriver::HandleInitialFrame(const uint8_t *aFrame,
+                                      uint16_t       aLength,
+                                      uint8_t        aHeader,
+                                      bool          &aSave,
+                                      void          *aContext)
+{
+    static_cast<SpinelDriver *>(aContext)->HandleInitialFrame(aFrame, aLength, aHeader, aSave);
+}
+
+void SpinelDriver::HandleInitialFrame(const uint8_t *aFrame, uint16_t aLength, uint8_t aHeader, bool &aSave)
+{
+    spinel_prop_key_t key;
+    uint8_t          *data   = nullptr;
+    spinel_size_t     len    = 0;
+    uint8_t           header = 0;
+    uint32_t          cmd    = 0;
+    spinel_ssize_t    rval   = 0;
+    spinel_ssize_t    unpacked;
+    otError           error = OT_ERROR_NONE;
+
+    OT_UNUSED_VARIABLE(aHeader);
+
+    rval = spinel_datatype_unpack(aFrame, aLength, "CiiD", &header, &cmd, &key, &data, &len);
+    VerifyOrExit(rval > 0 && cmd >= SPINEL_CMD_PROP_VALUE_IS && cmd <= SPINEL_CMD_PROP_VALUE_REMOVED,
+                 error = OT_ERROR_PARSE);
+
+    VerifyOrExit(cmd == SPINEL_CMD_PROP_VALUE_IS, error = OT_ERROR_DROP);
+
+    if (key == SPINEL_PROP_LAST_STATUS)
+    {
+        spinel_status_t status = SPINEL_STATUS_OK;
+
+        unpacked = spinel_datatype_unpack(data, len, "i", &status);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        if (status >= SPINEL_STATUS_RESET__BEGIN && status <= SPINEL_STATUS_RESET__END)
+        {
+            // this clear is necessary in case the RCP has sent messages between disable and reset
+            mRxFrameBuffer.Clear();
+
+            LogInfo("co-processor reset: %s", spinel_status_to_cstr(status));
+            mIsCoprocessorReady = true;
+        }
+        else
+        {
+            LogInfo("co-processor last status: %s", spinel_status_to_cstr(status));
+            ExitNow();
+        }
+    }
+    else
+    {
+        // Drop other frames when the key isn't waiting key.
+        VerifyOrExit(mWaitingKey == key, error = OT_ERROR_DROP);
+
+        if (key == SPINEL_PROP_PROTOCOL_VERSION)
+        {
+            unpacked = spinel_datatype_unpack(data, len, (SPINEL_DATATYPE_UINT_PACKED_S SPINEL_DATATYPE_UINT_PACKED_S),
+                                              &mSpinelVersionMajor, &mSpinelVersionMinor);
+
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        }
+        else if (key == SPINEL_PROP_NCP_VERSION)
+        {
+            unpacked = spinel_datatype_unpack_in_place(data, len, SPINEL_DATATYPE_UTF8_S, mVersion, sizeof(mVersion));
+
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        }
+        else if (key == SPINEL_PROP_CAPS)
+        {
+            uint8_t        capsBuffer[kCapsBufferSize];
+            spinel_size_t  capsLength = sizeof(capsBuffer);
+            const uint8_t *capsData   = capsBuffer;
+
+            unpacked = spinel_datatype_unpack_in_place(data, len, SPINEL_DATATYPE_DATA_S, capsBuffer, &capsLength);
+
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+            while (capsLength > 0)
+            {
+                unsigned int capability;
+
+                unpacked = spinel_datatype_unpack(capsData, capsLength, SPINEL_DATATYPE_UINT_PACKED_S, &capability);
+                VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+                SuccessOrExit(error = mCoprocessorCaps.PushBack(capability));
+
+                capsData += unpacked;
+                capsLength -= static_cast<spinel_size_t>(unpacked);
+            }
+        }
+
+        mIsWaitingForResponse = false;
+    }
+
+exit:
+    aSave = false;
+    LogIfFail("Error processing frame", error);
+}
+
+otError SpinelDriver::CheckSpinelVersion(void)
+{
+    otError error = OT_ERROR_NONE;
+
+    SuccessOrExit(error = SendCommand(SPINEL_CMD_PROP_VALUE_GET, SPINEL_PROP_PROTOCOL_VERSION, sTid));
+    mIsWaitingForResponse = true;
+    mWaitingKey           = SPINEL_PROP_PROTOCOL_VERSION;
+
+    SuccessOrExit(error = WaitResponse());
+
+    if ((mSpinelVersionMajor != SPINEL_PROTOCOL_VERSION_THREAD_MAJOR) ||
+        (mSpinelVersionMinor != SPINEL_PROTOCOL_VERSION_THREAD_MINOR))
+    {
+        LogCrit("Spinel version mismatch - Posix:%d.%d, co-processor:%d.%d", SPINEL_PROTOCOL_VERSION_THREAD_MAJOR,
+                SPINEL_PROTOCOL_VERSION_THREAD_MINOR, mSpinelVersionMajor, mSpinelVersionMinor);
+        DieNow(OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
+    }
+
+exit:
+    return error;
+}
+
+otError SpinelDriver::GetCoprocessorVersion(void)
+{
+    otError error = OT_ERROR_NONE;
+
+    SuccessOrExit(error = SendCommand(SPINEL_CMD_PROP_VALUE_GET, SPINEL_PROP_NCP_VERSION, sTid));
+    mIsWaitingForResponse = true;
+    mWaitingKey           = SPINEL_PROP_NCP_VERSION;
+
+    SuccessOrExit(error = WaitResponse());
+exit:
+    return error;
+}
+
+otError SpinelDriver::GetCoprocessorCaps(void)
+{
+    otError error = OT_ERROR_NONE;
+
+    SuccessOrExit(error = SendCommand(SPINEL_CMD_PROP_VALUE_GET, SPINEL_PROP_CAPS, sTid));
+    mIsWaitingForResponse = true;
+    mWaitingKey           = SPINEL_PROP_CAPS;
+
+    SuccessOrExit(error = WaitResponse());
+exit:
+    return error;
+}
+
+void SpinelDriver::ProcessFrameQueue(void)
+{
+    uint8_t *frame = nullptr;
+    uint16_t length;
+
+    assert(mSavedFrameHandler != nullptr && mFrameHandlerContext != nullptr);
+
+    while (mRxFrameBuffer.GetNextSavedFrame(frame, length) == OT_ERROR_NONE)
+    {
+        mSavedFrameHandler(frame, length, mFrameHandlerContext);
+    }
+
+    mRxFrameBuffer.ClearSavedFrames();
+}
+
+} // namespace Spinel
+} // namespace ot
diff --git a/src/lib/spinel/spinel_driver.hpp b/src/lib/spinel/spinel_driver.hpp
new file mode 100644
index 0000000..fe412c1
--- /dev/null
+++ b/src/lib/spinel/spinel_driver.hpp
@@ -0,0 +1,306 @@
+/*
+ *  Copyright (c) 2024, 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 SPINEL_DRIVER_HPP_
+#define SPINEL_DRIVER_HPP_
+
+#include <openthread/instance.h>
+
+#include "lib/spinel/logger.hpp"
+#include "lib/spinel/spinel.h"
+#include "lib/spinel/spinel_interface.hpp"
+
+namespace ot {
+namespace Spinel {
+
+/**
+ * Maximum number of Spinel Interface IDs.
+ *
+ */
+#if OPENTHREAD_CONFIG_MULTIPAN_RCP_ENABLE
+static constexpr uint8_t kSpinelHeaderMaxNumIid = 4;
+#else
+static constexpr uint8_t kSpinelHeaderMaxNumIid = 1;
+#endif
+
+class SpinelDriver : public Logger
+{
+public:
+    typedef void (
+        *ReceivedFrameHandler)(const uint8_t *aFrame, uint16_t aLength, uint8_t aHeader, bool &aSave, void *aContext);
+    typedef void (*SavedFrameHandler)(const uint8_t *aFrame, uint16_t aLength, void *aContext);
+
+    /**
+     * Constructor of the SpinelDriver.
+     *
+     */
+    SpinelDriver(void);
+
+    /**
+     * Initialize this SpinelDriver Instance.
+     *
+     * @param[in]  aSpinelInterface            A reference to the Spinel interface.
+     * @param[in]  aSoftwareReset              TRUE to reset on init, FALSE to not reset on init.
+     * @param[in]  aIidList                    A Pointer to the list of IIDs to receive spinel frame from.
+     *                                         First entry must be the IID of the Host Application.
+     * @param[in]  aIidListLength              The Length of the @p aIidList.
+     *
+     */
+    void Init(SpinelInterface    &aSpinelInterface,
+              bool                aSoftwareReset,
+              const spinel_iid_t *aIidList,
+              uint8_t             aIidListLength);
+
+    /**
+     * Deinitialize this SpinelDriver Instance.
+     *
+     */
+    void Deinit(void);
+
+    /**
+     * Clear the rx frame buffer.
+     *
+     */
+    void ClearRxBuffer(void) { mRxFrameBuffer.Clear(); }
+
+    /**
+     * Set the internal state of co-processor as ready.
+     *
+     * This method is used to skip a reset.
+     */
+    void SetCoprocessorReady(void) { mIsCoprocessorReady = true; }
+
+    /**
+     * Send a reset command to the co-processor.
+     *
+     * @prarm[in] aResetType    The reset type, SPINEL_RESET_PLATFORM, SPINEL_RESET_STACK, or SPINEL_RESET_BOOTLOADER.
+     *
+     * @retval  OT_ERROR_NONE               Successfully removed item from the property.
+     * @retval  OT_ERROR_BUSY               Failed due to another operation is on going.
+     *
+     */
+    otError SendReset(uint8_t aResetType);
+
+    /**
+     * Reset the co-processor.
+     *
+     * This method will reset the co-processor and wait until the co-process is ready (receiving SPINEL_PROP_LAST_STATUS
+     * from the it). The reset will be either a software or hardware reset. If `aSoftwareReset` is `true`, then the
+     * method will first try a software reset. If the software reset succeeds, the method exits. Otherwise the method
+     * will then try a hardware reset. If `aSoftwareReset` is `false`, then method will directly try a hardware reset.
+     *
+     * @param[in]  aSoftwareReset                 TRUE to try SW reset first, FALSE to directly try HW reset.
+     *
+     */
+    void ResetCoprocessor(bool aSoftwareReset);
+
+    /**
+     * Processes any pending the I/O data.
+     *
+     * The method should be called by the system loop to process received spinel frames.
+     *
+     * @param[in]  aContext   The process context.
+     *
+     */
+    void Process(const void *aContext);
+
+    /**
+     * Checks whether there is pending frame in the buffer.
+     *
+     * The method is required by the system loop to update timer fd.
+     *
+     * @returns Whether there is pending frame in the buffer.
+     *
+     */
+    bool HasPendingFrame(void) const { return mRxFrameBuffer.HasSavedFrame(); }
+
+    /**
+     * Returns the co-processor sw version string.
+     *
+     * @returns A pointer to the co-processor version string.
+     *
+     */
+    const char *GetVersion(void) const { return mVersion; }
+
+    /*
+     * Sends a spinel command to the co-processor.
+     *
+     * @param[in] aCommand    The spinel command.
+     * @param[in] aKey        The spinel property key.
+     * @param[in] aTid        The spinel transaction id.
+     * @param[in] aFormat     The format string of the arguments to send.
+     * @param[in] aArgs       The argument list.
+     *
+     * @retval  OT_ERROR_NONE           Successfully sent the command through spinel interface.
+     * @retval  OT_ERROR_INVALID_STATE  The spinel interface is in an invalid state.
+     * @retval  OT_ERROR_NO_BUFS        The spinel interface doesn't have enough buffer.
+     *
+     */
+    otError SendCommand(uint32_t          aCommand,
+                        spinel_prop_key_t aKey,
+                        spinel_tid_t      aTid,
+                        const char       *aFormat,
+                        va_list           aArgs);
+
+    /*
+     * Sets the handler to process the received spinel frame.
+     *
+     * @param[in] aReceivedFrameHandler  The handler to process received spinel frames.
+     * @param[in] aSavedFrameHandler     The handler to process saved spinel frames.
+     * @param[in] aContext               The context to call the handler.
+     *
+     */
+    void SetFrameHandler(ReceivedFrameHandler aReceivedFrameHandler,
+                         SavedFrameHandler    aSavedFrameHandler,
+                         void                *aContext);
+
+    /*
+     * Returns the spinel interface.
+     *
+     * @returns A pointer to the spinel interface object.
+     *
+     */
+    SpinelInterface *GetSpinelInterface(void) const { return mSpinelInterface; }
+
+    /**
+     * Returns if the co-processor has some capability
+     *
+     * @param[in] aCapability  The capability queried.
+     *
+     * @returns `true` if the co-processor has the capability. `false` otherwise.
+     *
+     */
+    bool CoprocessorHasCap(unsigned int aCapability) { return mCoprocessorCaps.Contains(aCapability); }
+
+private:
+    static constexpr uint16_t kMaxSpinelFrame    = SPINEL_FRAME_MAX_SIZE;
+    static constexpr uint16_t kVersionStringSize = 128;
+    static constexpr uint32_t kUsPerMs           = 1000; ///< Microseconds per millisecond.
+    static constexpr uint32_t kMaxWaitTime       = 2000; ///< Max time to wait for response in milliseconds.
+    static constexpr uint16_t kCapsBufferSize    = 100;  ///< Max buffer size used to store `SPINEL_PROP_CAPS` value.
+
+    /**
+     * Represents an array of elements with a fixed max size.
+     *
+     * @tparam Type        The array element type.
+     * @tparam kMaxSize    Specifies the max array size (maximum number of elements in the array).
+     *
+     */
+    template <typename Type, uint16_t kMaxSize> class Array
+    {
+        static_assert(kMaxSize != 0, "Array `kMaxSize` cannot be zero");
+
+    public:
+        Array(void)
+            : mLength(0)
+        {
+        }
+
+        uint16_t GetMaxSize(void) const { return kMaxSize; }
+
+        bool IsFull(void) const { return (mLength == GetMaxSize()); }
+
+        otError PushBack(const Type &aEntry)
+        {
+            return IsFull() ? OT_ERROR_NO_BUFS : (mElements[mLength++] = aEntry, OT_ERROR_NONE);
+        }
+
+        const Type *Find(const Type &aEntry) const
+        {
+            const Type *matched = nullptr;
+
+            for (const Type &element : *this)
+            {
+                if (element == aEntry)
+                {
+                    matched = &element;
+                    break;
+                }
+            }
+
+            return matched;
+        }
+
+        bool Contains(const Type &aEntry) const { return Find(aEntry) != nullptr; }
+
+        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]; }
+
+    private:
+        Type     mElements[kMaxSize];
+        uint16_t mLength;
+    };
+
+    otError WaitResponse(void);
+
+    static void HandleReceivedFrame(void *aContext);
+    void        HandleReceivedFrame(void);
+
+    static void HandleInitialFrame(const uint8_t *aFrame,
+                                   uint16_t       aLength,
+                                   uint8_t        aHeader,
+                                   bool          &aSave,
+                                   void          *aContext);
+    void        HandleInitialFrame(const uint8_t *aFrame, uint16_t aLength, uint8_t aHeader, bool &aSave);
+
+    otError SendCommand(uint32_t aCommand, spinel_prop_key_t aKey, spinel_tid_t aTid);
+
+    otError CheckSpinelVersion(void);
+    otError GetCoprocessorVersion(void);
+    otError GetCoprocessorCaps(void);
+
+    void ProcessFrameQueue(void);
+
+    SpinelInterface::RxFrameBuffer mRxFrameBuffer;
+    SpinelInterface               *mSpinelInterface;
+
+    spinel_prop_key_t mWaitingKey; ///< The property key of current transaction.
+    bool              mIsWaitingForResponse;
+
+    spinel_iid_t                                mIid;
+    Array<spinel_iid_t, kSpinelHeaderMaxNumIid> mIidList;
+
+    ReceivedFrameHandler mReceivedFrameHandler;
+    SavedFrameHandler    mSavedFrameHandler;
+    void                *mFrameHandlerContext;
+
+    int mSpinelVersionMajor;
+    int mSpinelVersionMinor;
+
+    bool mIsCoprocessorReady;
+    char mVersion[kVersionStringSize];
+
+    Array<unsigned int, kCapsBufferSize> mCoprocessorCaps;
+};
+
+} // namespace Spinel
+} // namespace ot
+
+#endif // SPINEL_DRIVER_HPP_
diff --git a/src/lib/spinel/spinel_encoder.cpp b/src/lib/spinel/spinel_encoder.cpp
index 91a9ec3..6cd3904 100644
--- a/src/lib/spinel/spinel_encoder.cpp
+++ b/src/lib/spinel/spinel_encoder.cpp
@@ -291,5 +291,7 @@
     return error;
 }
 
+void Encoder::ClearNcpBuffer(void) { mNcpBuffer.Clear(); }
+
 } // namespace Spinel
 } // namespace ot
diff --git a/src/lib/spinel/spinel_encoder.hpp b/src/lib/spinel/spinel_encoder.hpp
index 6b9ea98..c678519 100644
--- a/src/lib/spinel/spinel_encoder.hpp
+++ b/src/lib/spinel/spinel_encoder.hpp
@@ -676,6 +676,12 @@
      */
     otError ResetToSaved(void);
 
+    /**
+     * Clear NCP buffer on reset command.
+     *
+     */
+    void ClearNcpBuffer(void);
+
 private:
     enum
     {
diff --git a/src/ncp/ncp_base.cpp b/src/ncp/ncp_base.cpp
index e0b4c18..96e3625 100644
--- a/src/ncp/ncp_base.cpp
+++ b/src/ncp/ncp_base.cpp
@@ -340,7 +340,7 @@
 #if OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
     otUdpForwardSetForwarder(mInstance, &NcpBase::HandleUdpForwardStream, this);
 #endif
-    otIcmp6SetEchoMode(mInstance, OT_ICMP6_ECHO_HANDLER_DISABLED);
+    otIcmp6SetEchoMode(mInstance, OT_ICMP6_ECHO_HANDLER_RLOC_ALOC_ONLY);
 #if OPENTHREAD_FTD
     otThreadRegisterNeighborTableCallback(mInstance, &NcpBase::HandleNeighborTableChanged);
 #if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
@@ -1285,6 +1285,8 @@
 
         ResetCounters();
 
+        mEncoder.ClearNcpBuffer();
+
         SuccessOrAssert(error = WriteLastStatusFrame(SPINEL_HEADER_FLAG | SPINEL_HEADER_TX_NOTIFICATION_IID,
                                                      SPINEL_STATUS_RESET_POWER_ON));
     }
diff --git a/src/ncp/ncp_base_mtd.cpp b/src/ncp/ncp_base_mtd.cpp
index badc6a5..227ce61 100644
--- a/src/ncp/ncp_base_mtd.cpp
+++ b/src/ncp/ncp_base_mtd.cpp
@@ -690,7 +690,7 @@
 
     SuccessOrExit(error = mDecoder.ReadUint32(keyGuardTime));
 
-    otThreadSetKeySwitchGuardTime(mInstance, keyGuardTime);
+    otThreadSetKeySwitchGuardTime(mInstance, static_cast<uint16_t>(keyGuardTime));
 
 exit:
     return error;
@@ -2072,6 +2072,9 @@
     case OT_ICMP6_ECHO_HANDLER_ALL:
         mode = SPINEL_IPV6_ICMP_PING_OFFLOAD_ALL;
         break;
+    case OT_ICMP6_ECHO_HANDLER_RLOC_ALOC_ONLY:
+        mode = SPINEL_IPV6_ICMP_PING_OFFLOAD_RLOC_ALOC_ONLY;
+        break;
     };
 
     return mEncoder.WriteUint8(mode);
@@ -2099,6 +2102,9 @@
     case SPINEL_IPV6_ICMP_PING_OFFLOAD_ALL:
         mode = OT_ICMP6_ECHO_HANDLER_ALL;
         break;
+    case SPINEL_IPV6_ICMP_PING_OFFLOAD_RLOC_ALOC_ONLY:
+        mode = OT_ICMP6_ECHO_HANDLER_RLOC_ALOC_ONLY;
+        break;
     };
 
     otIcmp6SetEchoMode(mInstance, mode);
diff --git a/src/posix/platform/CMakeLists.txt b/src/posix/platform/CMakeLists.txt
index e479583..825d106 100644
--- a/src/posix/platform/CMakeLists.txt
+++ b/src/posix/platform/CMakeLists.txt
@@ -135,6 +135,7 @@
     infra_if.cpp
     logging.cpp
     mainloop.cpp
+    mdns_socket.cpp
     memory.cpp
     misc.cpp
     multicast_routing.cpp
diff --git a/src/posix/platform/config_file.hpp b/src/posix/platform/config_file.hpp
index 51d16ca..1590ed2 100644
--- a/src/posix/platform/config_file.hpp
+++ b/src/posix/platform/config_file.hpp
@@ -26,8 +26,8 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef POSIX_PLATFORM_CONFIG_FILE_HPP_
-#define POSIX_PLATFORM_CONFIG_FILE_HPP_
+#ifndef OT_POSIX_PLATFORM_CONFIG_FILE_HPP_
+#define OT_POSIX_PLATFORM_CONFIG_FILE_HPP_
 
 #include <assert.h>
 #include <stdint.h>
@@ -125,4 +125,4 @@
 } // namespace Posix
 } // namespace ot
 
-#endif // POSIX_PLATFORM_CONFIG_FILE_HPP_
+#endif // OT_POSIX_PLATFORM_CONFIG_FILE_HPP_
diff --git a/src/posix/platform/configuration.cpp b/src/posix/platform/configuration.cpp
index 07f1208..6311bdc 100644
--- a/src/posix/platform/configuration.cpp
+++ b/src/posix/platform/configuration.cpp
@@ -43,6 +43,8 @@
 namespace ot {
 namespace Posix {
 
+const char Configuration::kLogModuleName[] = "Config";
+
 #if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
 const char Configuration::kKeyCalibratedPower[] = "calibrated_power";
 #endif
@@ -74,12 +76,12 @@
 exit:
     if (error == OT_ERROR_NONE)
     {
-        otLogInfoPlat("Successfully set region \"%c%c\"", (aRegionCode >> 8) & 0xff, (aRegionCode & 0xff));
+        LogInfo("Successfully set region \"%c%c\"", (aRegionCode >> 8) & 0xff, (aRegionCode & 0xff));
     }
     else
     {
-        otLogCritPlat("Failed to set region \"%c%c\": %s", (aRegionCode >> 8) & 0xff, (aRegionCode & 0xff),
-                      otThreadErrorToString(error));
+        LogCrit("Failed to set region \"%c%c\": %s", (aRegionCode >> 8) & 0xff, (aRegionCode & 0xff),
+                otThreadErrorToString(error));
     }
 
     return error;
@@ -112,7 +114,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogCritPlat("Failed to get power domain: %s", otThreadErrorToString(error));
+        LogCrit("Failed to get power domain: %s", otThreadErrorToString(error));
     }
 
     return error;
@@ -165,7 +167,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogCritPlat("Failed to update channel mask: %s", otThreadErrorToString(error));
+        LogCrit("Failed to update channel mask: %s", otThreadErrorToString(error));
     }
 
     return error;
@@ -182,7 +184,7 @@
 
     while (GetNextTargetPower(aDomain, iterator, targetPower) == OT_ERROR_NONE)
     {
-        otLogInfoPlat("Update target power: %s\r\n", targetPower.ToString().AsCString());
+        LogInfo("Update target power: %s\r\n", targetPower.ToString().AsCString());
 
         for (uint8_t ch = targetPower.GetChannelStart(); ch <= targetPower.GetChannelEnd(); ch++)
         {
@@ -193,7 +195,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogCritPlat("Failed to update target power: %s", otThreadErrorToString(error));
+        LogCrit("Failed to update target power: %s", otThreadErrorToString(error));
     }
 
     return error;
@@ -222,7 +224,7 @@
     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());
+        LogInfo("Update calibrated power: %s\r\n", calibratedPower.ToString().AsCString());
 
         for (uint8_t ch = calibratedPower.GetChannelStart(); ch <= calibratedPower.GetChannelEnd(); ch++)
         {
@@ -235,7 +237,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogCritPlat("Failed to update calibrated power table: %s", otThreadErrorToString(error));
+        LogCrit("Failed to update calibrated power table: %s", otThreadErrorToString(error));
     }
 
     return error;
@@ -259,7 +261,7 @@
 
         if ((error = aTargetPower.FromString(psave)) != OT_ERROR_NONE)
         {
-            otLogCritPlat("Failed to read target power: %s", otThreadErrorToString(error));
+            LogCrit("Failed to read target power: %s", otThreadErrorToString(error));
         }
         break;
     }
diff --git a/src/posix/platform/configuration.hpp b/src/posix/platform/configuration.hpp
index 71c479d..e2a2942 100644
--- a/src/posix/platform/configuration.hpp
+++ b/src/posix/platform/configuration.hpp
@@ -26,8 +26,8 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef POSIX_PLATFORM_CONFIGURATION_HPP_
-#define POSIX_PLATFORM_CONFIGURATION_HPP_
+#ifndef OT_POSIX_PLATFORM_CONFIGURATION_HPP_
+#define OT_POSIX_PLATFORM_CONFIGURATION_HPP_
 
 #include "openthread-posix-config.h"
 
@@ -41,7 +41,9 @@
 #include <openthread/platform/radio.h>
 
 #include "config_file.hpp"
+#include "logger.hpp"
 #include "power.hpp"
+
 #include "common/code_utils.hpp"
 
 namespace ot {
@@ -51,9 +53,11 @@
  * Updates the target power table and calibrated power table to the RCP.
  *
  */
-class Configuration
+class Configuration : public Logger<Configuration>
 {
 public:
+    static const char kLogModuleName[]; ///< Module name used for logging.
+
     Configuration(void)
         : mFactoryConfigFile(OPENTHREAD_POSIX_CONFIG_FACTORY_CONFIG_FILE)
         , mProductConfigFile(OPENTHREAD_POSIX_CONFIG_PRODUCT_CONFIG_FILE)
@@ -150,4 +154,4 @@
 } // namespace ot
 
 #endif // OPENTHREAD_POSIX_CONFIG_CONFIGURATION_FILE_ENABLE
-#endif // POSIX_PLATFORM_CONFIGURATION_HPP_
+#endif // OT_POSIX_PLATFORM_CONFIGURATION_HPP_
diff --git a/src/posix/platform/daemon.cpp b/src/posix/platform/daemon.cpp
index 245ad48..dad11a7 100644
--- a/src/posix/platform/daemon.cpp
+++ b/src/posix/platform/daemon.cpp
@@ -28,7 +28,7 @@
 
 #include "posix/platform/daemon.hpp"
 
-#if defined(__ANDROID__) && !OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
 #include <cutils/sockets.h>
 #endif
 #include <fcntl.h>
@@ -75,6 +75,8 @@
 
 } // namespace
 
+const char Daemon::kLogModuleName[] = "Daemon";
+
 int Daemon::OutputFormat(const char *aFormat, ...)
 {
     int     ret;
@@ -97,7 +99,7 @@
                   "OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH is too short!");
 
     rval = vsnprintf(buf, sizeof(buf), aFormat, aArguments);
-    VerifyOrExit(rval >= 0, otLogWarnPlat("Failed to format CLI output: %s", strerror(errno)));
+    VerifyOrExit(rval >= 0, LogWarn("Failed to format CLI output: %s", strerror(errno)));
 
     if (rval >= static_cast<int>(sizeof(buf)))
     {
@@ -116,7 +118,7 @@
 
     if (rval < 0)
     {
-        otLogWarnPlat("Failed to write CLI output: %s", strerror(errno));
+        LogWarn("Failed to write CLI output: %s", strerror(errno));
         close(mSessionSocket);
         mSessionSocket = -1;
     }
@@ -160,7 +162,7 @@
 exit:
     if (rval == -1)
     {
-        otLogWarnPlat("Failed to initialize session socket: %s", strerror(errno));
+        LogWarn("Failed to initialize session socket: %s", strerror(errno));
         if (newSessionSocket != -1)
         {
             close(newSessionSocket);
@@ -168,11 +170,11 @@
     }
     else
     {
-        otLogInfoPlat("Session socket is ready");
+        LogInfo("Session socket is ready");
     }
 }
 
-#if defined(__ANDROID__) && !OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
 void Daemon::createListenSocketOrDie(void)
 {
     Filename socketFile;
@@ -184,6 +186,7 @@
     // This returns the init-managed stream socket which is already bind to
     // /dev/socket/ot-daemon/<interface-name>.sock
     mListenSocket = android_get_control_socket(socketFile);
+
     if (mListenSocket == -1)
     {
         DieNowWithMessage("android_get_control_socket", OT_EXIT_ERROR_ERRNO);
@@ -265,7 +268,7 @@
         DieNowWithMessage("bind", OT_EXIT_ERROR_ERRNO);
     }
 }
-#endif // defined(__ANDROID__) && !OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE
+#endif // OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
 
 void Daemon::SetUp(void)
 {
@@ -284,18 +287,13 @@
         DieNowWithMessage("listen", OT_EXIT_ERROR_ERRNO);
     }
 
+exit:
 #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);
+    otSysCliInitUsingDaemon(gInstance);
 #endif
 
     Mainloop::Manager::Get().Add(*this);
 
-exit:
     return;
 }
 
@@ -309,7 +307,7 @@
         mSessionSocket = -1;
     }
 
-#if !defined(__ANDROID__) || OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE
+#if !OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
     // The `mListenSocket` is managed by `init` on Android
     if (mListenSocket != -1)
     {
@@ -322,7 +320,7 @@
         Filename sockfile;
 
         GetFilename(sockfile, OPENTHREAD_POSIX_DAEMON_SOCKET_NAME);
-        otLogDebgPlat("Removing daemon socket: %s", sockfile);
+        LogDebg("Removing daemon socket: %s", sockfile);
         (void)unlink(sockfile);
     }
 
@@ -404,7 +402,7 @@
         {
             if (rval < 0)
             {
-                otLogWarnPlat("Daemon read: %s", strerror(errno));
+                LogWarn("Daemon read: %s", strerror(errno));
             }
             close(mSessionSocket);
             mSessionSocket = -1;
diff --git a/src/posix/platform/daemon.hpp b/src/posix/platform/daemon.hpp
index deba85b..be3738b 100644
--- a/src/posix/platform/daemon.hpp
+++ b/src/posix/platform/daemon.hpp
@@ -31,24 +31,28 @@
 #include "openthread-posix-config.h"
 
 #include "core/common/non_copyable.hpp"
-#include "posix/platform/mainloop.hpp"
+
+#include "logger.hpp"
+#include "mainloop.hpp"
 
 namespace ot {
 namespace Posix {
 
-class Daemon : public Mainloop::Source, private NonCopyable
+class Daemon : public Mainloop::Source, public Logger<Daemon>, private NonCopyable
 {
 public:
+    static const char kLogModuleName[];
+
     static Daemon &Get(void);
 
     void SetUp(void);
     void TearDown(void);
     void Update(otSysMainloopContext &aContext) override;
     void Process(const otSysMainloopContext &aContext) override;
+    int  OutputFormatV(const char *aFormat, va_list aArguments);
 
 private:
     int  OutputFormat(const char *aFormat, ...);
-    int  OutputFormatV(const char *aFormat, va_list aArguments);
     void createListenSocketOrDie(void);
     void InitializeSessionSocket(void);
 
diff --git a/src/posix/platform/firewall.cpp b/src/posix/platform/firewall.cpp
index 19ad8c5..7e9d47e 100644
--- a/src/posix/platform/firewall.cpp
+++ b/src/posix/platform/firewall.cpp
@@ -124,7 +124,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogWarnPlat("Failed to update ipsets: %s", otThreadErrorToString(error));
+        otLogWarnPlat("Firewall - failed to update ipsets: %s", otThreadErrorToString(error));
     }
 }
 
diff --git a/src/posix/platform/hdlc_interface.cpp b/src/posix/platform/hdlc_interface.cpp
index d2c45e4..12726fc 100644
--- a/src/posix/platform/hdlc_interface.cpp
+++ b/src/posix/platform/hdlc_interface.cpp
@@ -128,6 +128,8 @@
 namespace ot {
 namespace Posix {
 
+const char HdlcInterface::kLogModuleName[] = "HdlcIntface";
+
 HdlcInterface::HdlcInterface(const Url::Url &aRadioUrl)
     : mReceiveFrameCallback(nullptr)
     , mReceiveFrameContext(nullptr)
@@ -164,7 +166,7 @@
 #endif // OPENTHREAD_POSIX_CONFIG_RCP_PTY_ENABLE
     else
     {
-        otLogCritPlat("Radio file '%s' not supported", mRadioUrl.GetPath());
+        LogCrit("Radio file '%s' not supported", mRadioUrl.GetPath());
         ExitNow(error = OT_ERROR_FAILED);
     }
 
@@ -714,7 +716,7 @@
     {
         mInterfaceMetrics.mTransferredGarbageFrameCount++;
         mReceiveFrameBuffer->DiscardFrame();
-        otLogWarnPlat("Error decoding hdlc frame: %s", otThreadErrorToString(aError));
+        LogWarn("Error decoding hdlc frame: %s", otThreadErrorToString(aError));
     }
 
 exit:
@@ -742,7 +744,7 @@
             usleep(static_cast<useconds_t>(kOpenFileDelay) * US_PER_MS);
         } while (end > otPlatTimeGet());
 
-        otLogCritPlat("Failed to reopen UART connection after resetting the RCP device.");
+        LogCrit("Failed to reopen UART connection after resetting the RCP device.");
         error = OT_ERROR_FAILED;
     }
 
diff --git a/src/posix/platform/hdlc_interface.hpp b/src/posix/platform/hdlc_interface.hpp
index 6078135..c6e01c7 100644
--- a/src/posix/platform/hdlc_interface.hpp
+++ b/src/posix/platform/hdlc_interface.hpp
@@ -31,9 +31,10 @@
  *   This file includes definitions for the HDLC interface to radio (RCP).
  */
 
-#ifndef POSIX_PLATFORM_HDLC_INTERFACE_HPP_
-#define POSIX_PLATFORM_HDLC_INTERFACE_HPP_
+#ifndef OT_POSIX_PLATFORM_HDLC_INTERFACE_HPP_
+#define OT_POSIX_PLATFORM_HDLC_INTERFACE_HPP_
 
+#include "logger.hpp"
 #include "openthread-posix-config.h"
 #include "platform-posix.h"
 #include "lib/hdlc/hdlc.hpp"
@@ -48,9 +49,11 @@
  * Defines an HDLC interface to the Radio Co-processor (RCP)
  *
  */
-class HdlcInterface : public ot::Spinel::SpinelInterface
+class HdlcInterface : public ot::Spinel::SpinelInterface, public Logger<HdlcInterface>
 {
 public:
+    static const char kLogModuleName[]; ///< Module name used for logging.
+
     /**
      * Initializes the object.
      *
@@ -272,4 +275,5 @@
 
 } // namespace Posix
 } // namespace ot
-#endif // POSIX_PLATFORM_HDLC_INTERFACE_HPP_
+
+#endif // OT_POSIX_PLATFORM_HDLC_INTERFACE_HPP_
diff --git a/src/posix/platform/include/openthread/openthread-system.h b/src/posix/platform/include/openthread/openthread-system.h
index 61a0741..7f11349 100644
--- a/src/posix/platform/include/openthread/openthread-system.h
+++ b/src/posix/platform/include/openthread/openthread-system.h
@@ -254,6 +254,19 @@
  */
 bool otSysInfraIfIsRunning(void);
 
+/**
+ * Initializes the CLI module using the daemon.
+ *
+ * This function initializes the CLI module, and assigns the daemon to handle
+ * the CLI output. This function can be invoked multiple times. The typical use case
+ * is that, after OTBR/vendor_server's CLI output redirection, it uses this API to
+ * restore the original daemon's CLI output.
+ *
+ * @param[in] aInstance  The OpenThread instance structure.
+ *
+ */
+void otSysCliInitUsingDaemon(otInstance *aInstance);
+
 #ifdef __cplusplus
 } // end of extern "C"
 #endif
diff --git a/src/posix/platform/infra_if.cpp b/src/posix/platform/infra_if.cpp
index 6e16b3f..0c97852 100644
--- a/src/posix/platform/infra_if.cpp
+++ b/src/posix/platform/infra_if.cpp
@@ -128,6 +128,8 @@
 namespace ot {
 namespace Posix {
 
+const char InfraNetif::kLogModuleName[] = "InfraNetif";
+
 int InfraNetif::CreateIcmp6Socket(const char *aInfraIfName)
 {
     int                 sock;
@@ -174,7 +176,7 @@
 #ifdef __linux__
     rval = setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, aInfraIfName, strlen(aInfraIfName));
 #else  // __NetBSD__ || __FreeBSD__ || __APPLE__
-    rval = setsockopt(mInfraIfIcmp6Socket, IPPROTO_IPV6, IPV6_BOUND_IF, &mInfraIfIndex, sizeof(mInfraIfIndex));
+    rval = setsockopt(sock, IPPROTO_IPV6, IPV6_BOUND_IF, aInfraIfName, strlen(aInfraIfName));
 #endif // __linux__
     VerifyOrDie(rval == 0, OT_EXIT_ERROR_ERRNO);
 
@@ -190,6 +192,7 @@
 
 bool IsAddressGlobalUnicast(const in6_addr &aAddress) { return (aAddress.s6_addr[0] & 0xe0) == 0x20; }
 
+#ifdef __linux__
 // Create a net-link socket that subscribes to link & addresses events.
 int CreateNetLinkSocket(void)
 {
@@ -209,6 +212,7 @@
 
     return sock;
 }
+#endif // #ifdef __linux__
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 otError InfraNetif::SendIcmp6Nd(uint32_t            aInfraIfIndex,
@@ -269,15 +273,16 @@
     memcpy(CMSG_DATA(cmsgPointer), &hopLimit, sizeof(hopLimit));
 
     rval = sendmsg(mInfraIfIcmp6Socket, &msgHeader, 0);
+
     if (rval < 0)
     {
-        otLogWarnPlat("failed to send ICMPv6 message: %s", strerror(errno));
+        LogWarn("failed to send ICMPv6 message: %s", strerror(errno));
         ExitNow(error = OT_ERROR_FAILED);
     }
 
     if (static_cast<size_t>(rval) != iov.iov_len)
     {
-        otLogWarnPlat("failed to send ICMPv6 message: partially sent");
+        LogWarn("failed to send ICMPv6 message: partially sent");
         ExitNow(error = OT_ERROR_FAILED);
     }
 
@@ -309,7 +314,7 @@
     if (ioctl(sock, SIOCGIFFLAGS, &ifReq) == -1)
     {
 #if OPENTHREAD_POSIX_CONFIG_EXIT_ON_INFRA_NETIF_LOST_ENABLE
-        otLogCritPlat("The infra link %s may be lost. Exiting.", mInfraIfName);
+        LogCrit("The infra link %s may be lost. Exiting.", mInfraIfName);
         DieNow(OT_EXIT_ERROR_ERRNO);
 #endif
         ExitNow();
@@ -332,7 +337,7 @@
 
     if (getifaddrs(&ifAddrs) < 0)
     {
-        otLogWarnPlat("failed to get netif addresses: %s", strerror(errno));
+        LogWarn("failed to get netif addresses: %s", strerror(errno));
         ExitNow();
     }
 
@@ -379,7 +384,7 @@
 
     if (getifaddrs(&ifAddrs) < 0)
     {
-        otLogCritPlat("failed to get netif addresses: %s", strerror(errno));
+        LogCrit("failed to get netif addresses: %s", strerror(errno));
         DieNow(OT_EXIT_ERROR_ERRNO);
     }
 
@@ -405,7 +410,12 @@
     return hasLla;
 }
 
-void InfraNetif::Init(void) { mNetLinkSocket = CreateNetLinkSocket(); }
+void InfraNetif::Init(void)
+{
+#ifdef __linux__
+    mNetLinkSocket = CreateNetLinkSocket();
+#endif
+}
 
 void InfraNetif::SetInfraNetif(const char *aIfName, int aIcmp6Socket)
 {
@@ -414,7 +424,9 @@
     OT_UNUSED_VARIABLE(aIcmp6Socket);
 
     OT_ASSERT(gInstance != nullptr);
+#ifdef __linux__
     VerifyOrDie(mNetLinkSocket != -1, OT_EXIT_INVALID_STATE);
+#endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     SetInfraNetifIcmp6SocketForBorderRouting(aIcmp6Socket);
@@ -425,7 +437,7 @@
 
     if (aIfName == nullptr || aIfName[0] == '\0')
     {
-        otLogWarnPlat("Border Routing/Backbone Router feature is disabled: infra interface is missing");
+        LogWarn("Border Routing/Backbone Router feature is disabled: infra interface is missing");
         ExitNow();
     }
 
@@ -436,7 +448,7 @@
     ifIndex = if_nametoindex(aIfName);
     if (ifIndex == 0)
     {
-        otLogCritPlat("Failed to get the index for infra interface %s", aIfName);
+        LogCrit("Failed to get the index for infra interface %s", aIfName);
         DieNow(OT_EXIT_INVALID_ARGUMENTS);
     }
 
@@ -449,7 +461,9 @@
 void InfraNetif::SetUp(void)
 {
     OT_ASSERT(gInstance != nullptr);
+#ifdef __linux__
     VerifyOrExit(mNetLinkSocket != -1);
+#endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     SuccessOrDie(otBorderRoutingInit(gInstance, mInfraIfIndex, otSysInfraIfIsRunning()));
@@ -461,6 +475,9 @@
 #endif
 
     Mainloop::Manager::Get().Add(*this);
+
+    ExitNow(); // To silence unused `exit` label warning.
+
 exit:
     return;
 }
@@ -488,11 +505,13 @@
     }
 #endif
 
+#ifdef __linux__
     if (mNetLinkSocket != -1)
     {
         close(mNetLinkSocket);
         mNetLinkSocket = -1;
     }
+#endif
 
     mInfraIfName[0] = '\0';
     mInfraIfIndex   = 0;
@@ -500,7 +519,9 @@
 
 void InfraNetif::Update(otSysMainloopContext &aContext)
 {
+#ifdef __linux__
     VerifyOrExit(mNetLinkSocket != -1);
+#endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     VerifyOrExit(mInfraIfIcmp6Socket != -1);
@@ -509,13 +530,17 @@
     aContext.mMaxFd = OT_MAX(aContext.mMaxFd, mInfraIfIcmp6Socket);
 #endif
 
+#ifdef __linux__
     FD_SET(mNetLinkSocket, &aContext.mReadFdSet);
     aContext.mMaxFd = OT_MAX(aContext.mMaxFd, mNetLinkSocket);
+#endif
 
 exit:
     return;
 }
 
+#ifdef __linux__
+
 void InfraNetif::ReceiveNetLinkMessage(void)
 {
     const size_t kMaxNetLinkBufSize = 8192;
@@ -529,7 +554,7 @@
     len = recv(mNetLinkSocket, msgBuffer.mBuffer, sizeof(msgBuffer.mBuffer), 0);
     if (len < 0)
     {
-        otLogCritPlat("Failed to receive netlink message: %s", strerror(errno));
+        LogCrit("Failed to receive netlink message: %s", strerror(errno));
         ExitNow();
     }
 
@@ -554,7 +579,7 @@
             struct nlmsgerr *errMsg = reinterpret_cast<struct nlmsgerr *>(NLMSG_DATA(header));
 
             OT_UNUSED_VARIABLE(errMsg);
-            otLogWarnPlat("netlink NLMSG_ERROR response: seq=%u, error=%d", header->nlmsg_seq, errMsg->error);
+            LogWarn("netlink NLMSG_ERROR response: seq=%u, error=%d", header->nlmsg_seq, errMsg->error);
             break;
         }
         default:
@@ -566,6 +591,8 @@
     return;
 }
 
+#endif // #ifdef __linux__
+
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 void InfraNetif::ReceiveIcmp6Message(void)
 {
@@ -599,7 +626,7 @@
     rval = recvmsg(mInfraIfIcmp6Socket, &msg, 0);
     if (rval < 0)
     {
-        otLogWarnPlat("Failed to receive ICMPv6 message: %s", strerror(errno));
+        LogWarn("Failed to receive ICMPv6 message: %s", strerror(errno));
         ExitNow(error = OT_ERROR_DROP);
     }
 
@@ -635,7 +662,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogDebgPlat("Failed to handle ICMPv6 message: %s", otThreadErrorToString(error));
+        LogDebg("Failed to handle ICMPv6 message: %s", otThreadErrorToString(error));
     }
 }
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
@@ -655,7 +682,7 @@
 
     VerifyOrExit((char *)req->ar_name == kWellKnownIpv4OnlyName);
 
-    otLogInfoPlat("Handling host address response for %s", kWellKnownIpv4OnlyName);
+    LogInfo("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)
@@ -754,10 +781,10 @@
 
     if (status != 0)
     {
-        otLogNotePlat("getaddrinfo_a failed: %s", gai_strerror(status));
+        LogNote("getaddrinfo_a failed: %s", gai_strerror(status));
         ExitNow(error = OT_ERROR_FAILED);
     }
-    otLogInfoPlat("getaddrinfo_a requested for %s", kWellKnownIpv4OnlyName);
+    LogInfo("getaddrinfo_a requested for %s", kWellKnownIpv4OnlyName);
 exit:
     if (error != OT_ERROR_NONE)
     {
@@ -792,7 +819,10 @@
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     VerifyOrExit(mInfraIfIcmp6Socket != -1);
 #endif
+
+#ifdef __linux__
     VerifyOrExit(mNetLinkSocket != -1);
+#endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     if (FD_ISSET(mInfraIfIcmp6Socket, &aContext.mReadFdSet))
@@ -801,10 +831,12 @@
     }
 #endif
 
+#ifdef __linux__
     if (FD_ISSET(mNetLinkSocket, &aContext.mReadFdSet))
     {
         ReceiveNetLinkMessage();
     }
+#endif
 
 exit:
     return;
diff --git a/src/posix/platform/infra_if.hpp b/src/posix/platform/infra_if.hpp
index e8aedd2..3eee248 100644
--- a/src/posix/platform/infra_if.hpp
+++ b/src/posix/platform/infra_if.hpp
@@ -31,15 +31,20 @@
  *   This file implements the infrastructure interface for posix.
  */
 
+#ifndef OT_POSIX_PLATFORM_INFRA_IF_HPP_
+#define OT_POSIX_PLATFORM_INFRA_IF_HPP_
+
 #include "openthread-posix-config.h"
 
 #include <net/if.h>
 #include <openthread/nat64.h>
 #include <openthread/openthread-system.h>
 
-#include "multicast_routing.hpp"
 #include "core/common/non_copyable.hpp"
-#include "posix/platform/mainloop.hpp"
+
+#include "logger.hpp"
+#include "mainloop.hpp"
+#include "multicast_routing.hpp"
 
 #if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
 
@@ -50,9 +55,11 @@
  * Manages infrastructure network interface.
  *
  */
-class InfraNetif : public Mainloop::Source, private NonCopyable
+class InfraNetif : public Mainloop::Source, public Logger<InfraNetif>, private NonCopyable
 {
 public:
+    static const char kLogModuleName[]; ///< Module name used for logging.
+
     /**
      * Updates the fd_set and timeout for mainloop.
      *
@@ -220,8 +227,12 @@
     static const uint8_t      kValidNat64PrefixLength[];
 
     char     mInfraIfName[IFNAMSIZ];
-    uint32_t mInfraIfIndex  = 0;
-    int      mNetLinkSocket = -1;
+    uint32_t mInfraIfIndex = 0;
+
+#ifdef __linux__
+    int mNetLinkSocket = -1;
+#endif
+
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     int mInfraIfIcmp6Socket = -1;
 #endif
@@ -229,7 +240,10 @@
     MulticastRoutingManager mMulticastRoutingManager;
 #endif
 
-    void        ReceiveNetLinkMessage(void);
+#ifdef __linux__
+    void ReceiveNetLinkMessage(void);
+#endif
+
     bool        HasLinkLocalAddress(void) const;
     static void DiscoverNat64PrefixDone(union sigval sv);
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
@@ -241,3 +255,5 @@
 } // namespace Posix
 } // namespace ot
 #endif // OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
+
+#endif // OT_POSIX_PLATFORM_INFRA_IF_HPP_
diff --git a/src/posix/platform/ip6_utils.hpp b/src/posix/platform/ip6_utils.hpp
index 39c7bc1..841c317 100644
--- a/src/posix/platform/ip6_utils.hpp
+++ b/src/posix/platform/ip6_utils.hpp
@@ -26,16 +26,83 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
+#ifndef OT_POSIX_PLATFORM_IP6_UTILS_HPP_
+#define OT_POSIX_PLATFORM_IP6_UTILS_HPP_
+
 #include "openthread-posix-config.h"
 #include "platform-posix.h"
 
 #include <arpa/inet.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <openthread/ip6.h>
 
 namespace ot {
 namespace Posix {
 namespace Ip6Utils {
 
 /**
+ * Indicates whether or not the IPv6 address scope is Link-Local.
+ *
+ * @param[in] aAddress   The IPv6 address to check.
+ *
+ * @retval TRUE   If the IPv6 address scope is Link-Local.
+ * @retval FALSE  If the IPv6 address scope is not Link-Local.
+ *
+ */
+inline bool IsIp6AddressLinkLocal(const otIp6Address &aAddress)
+{
+    return (aAddress.mFields.m8[0] == 0xfe) && ((aAddress.mFields.m8[1] & 0xc0) == 0x80);
+}
+
+/**
+ * Indicates whether or not the IPv6 address is multicast.
+ *
+ * @param[in] aAddress   The IPv6 address to check.
+ *
+ * @retval TRUE   If the IPv6 address scope is multicast.
+ * @retval FALSE  If the IPv6 address scope is not multicast.
+ *
+ */
+inline bool IsIp6AddressMulticast(const otIp6Address &aAddress) { return (aAddress.mFields.m8[0] == 0xff); }
+
+/**
+ * Indicates whether or not the IPv6 address is unspecified.
+ *
+ * @param[in] aAddress   The IPv6 address to check.
+ *
+ * @retval TRUE   If the IPv6 address scope is unspecified.
+ * @retval FALSE  If the IPv6 address scope is not unspecified.
+ *
+ */
+inline bool IsIp6AddressUnspecified(const otIp6Address &aAddress) { return otIp6IsAddressUnspecified(&aAddress); }
+
+/**
+ * Copies the IPv6 address bytes into a given buffer.
+ *
+ * @param[in] aAddress  The IPv6 address to copy.
+ * @param[in] aBuffer   A pointer to buffer to copy the address to.
+ *
+ */
+inline void CopyIp6AddressTo(const otIp6Address &aAddress, void *aBuffer)
+{
+    memcpy(aBuffer, &aAddress, sizeof(otIp6Address));
+}
+
+/**
+ * Reads and set the the IPv6 address bytes from a given buffer.
+ *
+ * @param[in] aBuffer    A pointer to buffer to read from.
+ * @param[out] aAddress  A reference to populate with the read IPv6 address.
+ *
+ */
+inline void ReadIp6AddressFrom(const void *aBuffer, otIp6Address &aAddress)
+{
+    memcpy(&aAddress, aBuffer, sizeof(otIp6Address));
+}
+
+/**
  * This utility class converts binary IPv6 address to text format.
  *
  */
@@ -68,3 +135,5 @@
 } // namespace Ip6Utils
 } // namespace Posix
 } // namespace ot
+
+#endif // OT_POSIX_PLATFORM_IP6_UTILS_HPP_
diff --git a/src/posix/platform/logger.hpp b/src/posix/platform/logger.hpp
new file mode 100644
index 0000000..e907b87
--- /dev/null
+++ b/src/posix/platform/logger.hpp
@@ -0,0 +1,141 @@
+/*
+ *  Copyright (c) 2024, 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 `Logger` class for use by POSIX platform module.
+ */
+
+#ifndef OT_POSIX_PLATFORM_LOGGER_HPP_
+#define OT_POSIX_PLATFORM_LOGGER_HPP_
+
+#include "openthread-posix-config.h"
+
+#include <openthread/logging.h>
+
+namespace ot {
+namespace Posix {
+
+/**
+ * Provides logging methods for a specific POSIX module.
+ *
+ * The `Type` class MUST provide a `static const char kLogModuleName[]` which specifies the POSIX log module name to
+ * include in the platform logs (using `otLogPlatArgs()`).
+ *
+ * Users of this class should follow CRTP-style inheritance, i.e., the `Type` class itself should inherit from
+ * `Logger<Type>`.
+ *
+ */
+template <typename Type> class Logger
+{
+public:
+    /**
+     * Emits a log message at critical log level.
+     *
+     * @param[in]  aFormat  The format string.
+     * @param[in]  ...      Arguments for the format specification.
+     *
+     */
+    static void LogCrit(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2)
+    {
+        va_list args;
+
+        va_start(args, aFormat);
+        otLogPlatArgs(OT_LOG_LEVEL_CRIT, Type::kLogModuleName, aFormat, args);
+        va_end(args);
+    }
+
+    /**
+     * Emits a log message at warning log level.
+     *
+     * @param[in]  aFormat  The format string.
+     * @param[in]  ...      Arguments for the format specification.
+     *
+     */
+    static void LogWarn(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2)
+    {
+        va_list args;
+
+        va_start(args, aFormat);
+        otLogPlatArgs(OT_LOG_LEVEL_WARN, Type::kLogModuleName, aFormat, args);
+        va_end(args);
+    }
+
+    /**
+     * Emits a log message at note log level.
+     *
+     * @param[in]  aFormat  The format string.
+     * @param[in]  ...      Arguments for the format specification.
+     *
+     */
+    static void LogNote(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2)
+    {
+        va_list args;
+
+        va_start(args, aFormat);
+        otLogPlatArgs(OT_LOG_LEVEL_NOTE, Type::kLogModuleName, aFormat, args);
+        va_end(args);
+    }
+
+    /**
+     * Emits a log message at info log level.
+     *
+     * @param[in]  aFormat  The format string.
+     * @param[in]  ...      Arguments for the format specification.
+     *
+     */
+    static void LogInfo(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2)
+    {
+        va_list args;
+
+        va_start(args, aFormat);
+        otLogPlatArgs(OT_LOG_LEVEL_INFO, Type::kLogModuleName, aFormat, args);
+        va_end(args);
+    }
+
+    /**
+     * Emits a log message at debug log level.
+     *
+     * @param[in]  aFormat  The format string.
+     * @param[in]  ...      Arguments for the format specification.
+     *
+     */
+    static void LogDebg(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2)
+    {
+        va_list args;
+
+        va_start(args, aFormat);
+        otLogPlatArgs(OT_LOG_LEVEL_DEBG, Type::kLogModuleName, aFormat, args);
+        va_end(args);
+    }
+};
+
+} // namespace Posix
+} // namespace ot
+
+#endif // OT_POSIX_PLATFORM_LOGGER_HPP_
diff --git a/src/posix/platform/mainloop.hpp b/src/posix/platform/mainloop.hpp
index 6e11e56..a9ce981 100644
--- a/src/posix/platform/mainloop.hpp
+++ b/src/posix/platform/mainloop.hpp
@@ -28,7 +28,7 @@
 
 /**
  * @file
- *   This file includes definitions for the SPI interface to radio (RCP).
+ *   This file includes definitions for the mainloop events and manager.
  */
 
 #ifndef OT_POSIX_PLATFORM_MAINLOOP_HPP_
diff --git a/src/posix/platform/mdns_socket.cpp b/src/posix/platform/mdns_socket.cpp
new file mode 100644
index 0000000..2f96ee7
--- /dev/null
+++ b/src/posix/platform/mdns_socket.cpp
@@ -0,0 +1,710 @@
+/*
+ *  Copyright (c) 2024, 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 "mdns_socket.hpp"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "ip6_utils.hpp"
+#include "platform-posix.h"
+#include "common/code_utils.hpp"
+
+extern "C" otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    return ot::Posix::MdnsSocket::Get().SetListeningEnabled(aInstance, aEnable, aInfraIfIndex);
+}
+
+extern "C" void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    return ot::Posix::MdnsSocket::Get().SendMulticast(aMessage, aInfraIfIndex);
+}
+
+extern "C" void otPlatMdnsSendUnicast(otInstance *aInstance, otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    return ot::Posix::MdnsSocket::Get().SendUnicast(aMessage, aAddress);
+}
+
+namespace ot {
+namespace Posix {
+
+using namespace ot::Posix::Ip6Utils;
+
+const char MdnsSocket::kLogModuleName[] = "MdnsSocket";
+
+MdnsSocket &MdnsSocket::Get(void)
+{
+    static MdnsSocket sInstance;
+
+    return sInstance;
+}
+
+void MdnsSocket::Init(void)
+{
+    mEnabled      = false;
+    mInfraIfIndex = 0;
+    mFd6          = -1;
+    mFd4          = -1;
+    mPendingIp6Tx = 0;
+    mPendingIp4Tx = 0;
+
+    // mDNS multicast IPv6 address "ff02::fb"
+    memset(&mMulticastIp6Address, 0, sizeof(otIp6Address));
+    mMulticastIp6Address.mFields.m8[0]  = 0xff;
+    mMulticastIp6Address.mFields.m8[1]  = 0x02;
+    mMulticastIp6Address.mFields.m8[15] = 0xfb;
+
+    // mDNS multicast IPv4 address "224.0.0.251"
+    memset(&mMulticastIp4Address, 0, sizeof(otIp4Address));
+    mMulticastIp4Address.mFields.m8[0] = 224;
+    mMulticastIp4Address.mFields.m8[3] = 251;
+
+    memset(&mTxQueue, 0, sizeof(mTxQueue));
+}
+
+void MdnsSocket::SetUp(void)
+{
+    otMessageQueueInit(&mTxQueue);
+    Mainloop::Manager::Get().Add(*this);
+}
+
+void MdnsSocket::TearDown(void)
+{
+    Mainloop::Manager::Get().Remove(*this);
+
+    if (mEnabled)
+    {
+        ClearTxQueue();
+        mEnabled = false;
+    }
+}
+
+void MdnsSocket::Deinit(void)
+{
+    CloseIp4Socket();
+    CloseIp6Socket();
+}
+
+void MdnsSocket::Update(otSysMainloopContext &aContext)
+{
+    VerifyOrExit(mEnabled);
+
+    FD_SET(mFd6, &aContext.mReadFdSet);
+    FD_SET(mFd4, &aContext.mReadFdSet);
+
+    if (mPendingIp6Tx > 0)
+    {
+        FD_SET(mFd6, &aContext.mWriteFdSet);
+    }
+
+    if (mPendingIp4Tx > 0)
+    {
+        FD_SET(mFd4, &aContext.mWriteFdSet);
+    }
+
+    if (aContext.mMaxFd < mFd6)
+    {
+        aContext.mMaxFd = mFd6;
+    }
+
+    if (aContext.mMaxFd < mFd4)
+    {
+        aContext.mMaxFd = mFd4;
+    }
+
+exit:
+    return;
+}
+
+void MdnsSocket::Process(const otSysMainloopContext &aContext)
+{
+    VerifyOrExit(mEnabled);
+
+    if (FD_ISSET(mFd6, &aContext.mWriteFdSet))
+    {
+        SendQueuedMessages(kIp6Msg);
+    }
+
+    if (FD_ISSET(mFd4, &aContext.mWriteFdSet))
+    {
+        SendQueuedMessages(kIp4Msg);
+    }
+
+    if (FD_ISSET(mFd6, &aContext.mReadFdSet))
+    {
+        ReceiveMessage(kIp6Msg);
+    }
+
+    if (FD_ISSET(mFd4, &aContext.mReadFdSet))
+    {
+        ReceiveMessage(kIp4Msg);
+    }
+
+exit:
+    return;
+}
+
+otError MdnsSocket::SetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aEnable != mEnabled);
+    mInstance = aInstance;
+
+    if (aEnable)
+    {
+        error = Enable(aInfraIfIndex);
+    }
+    else
+    {
+        Disable(aInfraIfIndex);
+    }
+
+exit:
+    return error;
+}
+
+otError MdnsSocket::Enable(uint32_t aInfraIfIndex)
+{
+    otError error;
+
+    SuccessOrExit(error = OpenIp4Socket(aInfraIfIndex));
+    SuccessOrExit(error = JoinOrLeaveIp4MulticastGroup(/* aJoin */ true, aInfraIfIndex));
+
+    SuccessOrExit(error = OpenIp6Socket(aInfraIfIndex));
+    SuccessOrExit(error = JoinOrLeaveIp6MulticastGroup(/* aJoin */ true, aInfraIfIndex));
+
+    mEnabled      = true;
+    mInfraIfIndex = aInfraIfIndex;
+
+    LogInfo("Enabled");
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        CloseIp4Socket();
+        CloseIp6Socket();
+    }
+
+    return error;
+}
+
+void MdnsSocket::Disable(uint32_t aInfraIfIndex)
+{
+    ClearTxQueue();
+
+    IgnoreError(JoinOrLeaveIp4MulticastGroup(/* aJoin */ false, aInfraIfIndex));
+    IgnoreError(JoinOrLeaveIp6MulticastGroup(/* aJoin */ false, aInfraIfIndex));
+    CloseIp4Socket();
+    CloseIp6Socket();
+
+    mEnabled = false;
+
+    LogInfo("Disabled");
+}
+
+void MdnsSocket::SendMulticast(otMessage *aMessage, uint32_t aInfraIfIndex)
+{
+    Metadata metadata;
+    uint16_t length;
+
+    VerifyOrExit(mEnabled);
+    VerifyOrExit(aInfraIfIndex == mInfraIfIndex);
+
+    length = otMessageGetLength(aMessage);
+
+    if (length > kMaxMessageLength)
+    {
+        LogWarn("Multicast msg length %u is longer than max %u", length, kMaxMessageLength);
+        ExitNow();
+    }
+
+    metadata.mIp6Address = mMulticastIp6Address;
+    metadata.mIp6Port    = kMdnsPort;
+    metadata.mIp4Address = mMulticastIp4Address;
+    metadata.mIp4Port    = kMdnsPort;
+
+    SuccessOrExit(otMessageAppend(aMessage, &metadata, sizeof(Metadata)));
+
+    mPendingIp4Tx++;
+    mPendingIp6Tx++;
+
+    otMessageQueueEnqueue(&mTxQueue, aMessage);
+    aMessage = NULL;
+
+exit:
+    if (aMessage != NULL)
+    {
+        otMessageFree(aMessage);
+    }
+}
+
+void MdnsSocket::SendUnicast(otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress)
+{
+    bool     isIp4 = false;
+    Metadata metadata;
+    uint16_t length;
+
+    VerifyOrExit(mEnabled);
+    VerifyOrExit(aAddress->mInfraIfIndex == mInfraIfIndex);
+
+    length = otMessageGetLength(aMessage);
+
+    if (length > kMaxMessageLength)
+    {
+        LogWarn("Unicast msg length %u is longer than max %u", length, kMaxMessageLength);
+        ExitNow();
+    }
+
+    memset(&metadata, 0, sizeof(Metadata));
+
+    if (otIp4FromIp4MappedIp6Address(&aAddress->mAddress, &metadata.mIp4Address) == OT_ERROR_NONE)
+    {
+        isIp4             = true;
+        metadata.mIp4Port = aAddress->mPort;
+        metadata.mIp6Port = 0;
+    }
+    else
+    {
+        metadata.mIp6Address = aAddress->mAddress;
+        metadata.mIp4Port    = 0;
+        metadata.mIp6Port    = aAddress->mPort;
+    }
+
+    SuccessOrExit(otMessageAppend(aMessage, &metadata, sizeof(Metadata)));
+
+    if (isIp4)
+    {
+        mPendingIp4Tx++;
+    }
+    else
+    {
+        mPendingIp6Tx++;
+    }
+
+    otMessageQueueEnqueue(&mTxQueue, aMessage);
+    aMessage = NULL;
+
+exit:
+    if (aMessage != NULL)
+    {
+        otMessageFree(aMessage);
+    }
+}
+
+void MdnsSocket::ClearTxQueue(void)
+{
+    otMessage *message;
+
+    while ((message = otMessageQueueGetHead(&mTxQueue)) != NULL)
+    {
+        otMessageQueueDequeue(&mTxQueue, message);
+        otMessageFree(message);
+    }
+
+    mPendingIp4Tx = 0;
+    mPendingIp6Tx = 0;
+}
+
+void MdnsSocket::SendQueuedMessages(MsgType aMsgType)
+{
+    switch (aMsgType)
+    {
+    case kIp6Msg:
+        VerifyOrExit(mPendingIp6Tx > 0);
+        break;
+    case kIp4Msg:
+        VerifyOrExit(mPendingIp4Tx > 0);
+        break;
+    }
+
+    for (otMessage *message = otMessageQueueGetHead(&mTxQueue); message != NULL;
+         message            = otMessageQueueGetNext(&mTxQueue, message))
+    {
+        bool                isTxPending = false;
+        uint16_t            length;
+        uint16_t            offset;
+        int                 bytesSent;
+        Metadata            metadata;
+        uint8_t             buffer[kMaxMessageLength];
+        struct sockaddr_in6 addr6;
+        struct sockaddr_in  addr;
+
+        length = otMessageGetLength(message);
+
+        offset = length - sizeof(Metadata);
+        length -= sizeof(Metadata);
+
+        otMessageRead(message, offset, &metadata, sizeof(Metadata));
+
+        switch (aMsgType)
+        {
+        case kIp6Msg:
+            isTxPending = (metadata.mIp6Port != 0);
+            break;
+        case kIp4Msg:
+            isTxPending = (metadata.mIp4Port != 0);
+            break;
+        }
+
+        if (!isTxPending)
+        {
+            continue;
+        }
+
+        otMessageRead(message, 0, buffer, length);
+
+        switch (aMsgType)
+        {
+        case kIp6Msg:
+            memset(&addr6, 0, sizeof(addr6));
+            addr6.sin6_family = AF_INET6;
+            addr6.sin6_port   = htons(metadata.mIp6Port);
+            CopyIp6AddressTo(metadata.mIp6Address, &addr6.sin6_addr);
+            bytesSent = sendto(mFd6, buffer, length, 0, reinterpret_cast<struct sockaddr *>(&addr6), sizeof(addr6));
+            VerifyOrExit(bytesSent == length);
+            metadata.mIp6Port = 0;
+            mPendingIp6Tx--;
+            break;
+
+        case kIp4Msg:
+            memset(&addr, 0, sizeof(addr));
+            addr.sin_family = AF_INET;
+            addr.sin_port   = htons(metadata.mIp4Port);
+            memcpy(&addr.sin_addr.s_addr, &metadata.mIp4Address, sizeof(otIp4Address));
+            bytesSent = sendto(mFd4, buffer, length, 0, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
+            VerifyOrExit(bytesSent == length);
+            metadata.mIp4Port = 0;
+            mPendingIp4Tx--;
+            break;
+        }
+
+        if (metadata.CanFreeMessage())
+        {
+            otMessageQueueDequeue(&mTxQueue, message);
+            otMessageFree(message);
+        }
+        else
+        {
+            otMessageWrite(message, offset, &metadata, sizeof(Metadata));
+        }
+    }
+
+exit:
+    return;
+}
+
+void MdnsSocket::ReceiveMessage(MsgType aMsgType)
+{
+    otMessage            *message = nullptr;
+    uint8_t               buffer[kMaxMessageLength];
+    otPlatMdnsAddressInfo addrInfo;
+    uint16_t              length = 0;
+    struct sockaddr_in6   sockaddr6;
+    struct sockaddr_in    sockaddr;
+    socklen_t             len = sizeof(sockaddr6);
+    ssize_t               rval;
+
+    memset(&addrInfo, 0, sizeof(addrInfo));
+
+    switch (aMsgType)
+    {
+    case kIp6Msg:
+        len = sizeof(sockaddr6);
+        memset(&sockaddr6, 0, sizeof(sockaddr6));
+        rval = recvfrom(mFd6, reinterpret_cast<char *>(&buffer), sizeof(buffer), 0,
+                        reinterpret_cast<struct sockaddr *>(&sockaddr6), &len);
+        VerifyOrExit(rval >= 0, LogCrit("recvfrom() for IPv6 socket failed, errno: %s", strerror(errno)));
+        length = static_cast<uint16_t>(rval);
+        ReadIp6AddressFrom(&sockaddr6.sin6_addr, addrInfo.mAddress);
+        break;
+
+    case kIp4Msg:
+        len = sizeof(sockaddr);
+        memset(&sockaddr, 0, sizeof(sockaddr));
+        rval = recvfrom(mFd4, reinterpret_cast<char *>(&buffer), sizeof(buffer), 0,
+                        reinterpret_cast<struct sockaddr *>(&sockaddr), &len);
+        VerifyOrExit(rval >= 0, LogCrit("recvfrom() for IPv4 socket failed, errno: %s", strerror(errno)));
+        length = static_cast<uint16_t>(rval);
+        otIp4ToIp4MappedIp6Address((otIp4Address *)(&sockaddr.sin_addr.s_addr), &addrInfo.mAddress);
+        break;
+    }
+
+    VerifyOrExit(length > 0);
+
+    message = otIp6NewMessage(mInstance, nullptr);
+    VerifyOrExit(message != nullptr);
+    SuccessOrExit(otMessageAppend(message, buffer, length));
+
+    addrInfo.mPort         = kMdnsPort;
+    addrInfo.mInfraIfIndex = mInfraIfIndex;
+
+    otPlatMdnsHandleReceive(mInstance, message, /* aInUnicast */ false, &addrInfo);
+    message = nullptr;
+
+exit:
+    if (message != nullptr)
+    {
+        otMessageFree(message);
+    }
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Socket helpers
+
+otError MdnsSocket::OpenIp4Socket(uint32_t aInfraIfIndex)
+{
+    otError            error = OT_ERROR_FAILED;
+    struct sockaddr_in addr;
+    int                fd;
+
+    fd = socket(AF_INET, SOCK_DGRAM, 0);
+    VerifyOrExit(fd >= 0, LogCrit("Failed to create IPv4 socket"));
+
+#ifdef __linux__
+    {
+        char        nameBuffer[IF_NAMESIZE];
+        const char *ifname;
+
+        ifname = if_indextoname(aInfraIfIndex, nameBuffer);
+        VerifyOrExit(ifname != NULL, LogCrit("if_indextoname() failed"));
+
+        error = SetSocketOptionValue(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname), "SO_BINDTODEVICE");
+        SuccessOrExit(error);
+    }
+#else
+    {
+        int ifindex = static_cast<int>(aInfraIfIndex);
+
+        SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IP, IP_BOUND_IF, ifindex, "IP_BOUND_IF"));
+    }
+#endif
+
+    SuccessOrExit(error = SetSocketOption<uint8_t>(fd, IPPROTO_IP, IP_MULTICAST_TTL, 255, "IP_MULTICAST_TTL"));
+    SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IP, IP_TTL, 255, "IP_TTL"));
+    SuccessOrExit(error = SetSocketOption<uint8_t>(fd, IPPROTO_IP, IP_MULTICAST_LOOP, 1, "IP_MULTICAST_LOOP"));
+    SuccessOrExit(error = SetReuseAddrPortOptions(fd));
+
+    {
+        struct ip_mreqn mreqn;
+
+        memset(&mreqn, 0, sizeof(mreqn));
+        mreqn.imr_multiaddr.s_addr = inet_addr("224.0.0.251");
+        mreqn.imr_ifindex          = aInfraIfIndex;
+
+        SuccessOrExit(
+            error = SetSocketOptionValue(fd, IPPROTO_IP, IP_MULTICAST_IF, &mreqn, sizeof(mreqn), "IP_MULTICAST_IF"));
+    }
+
+    memset(&addr, 0, sizeof(addr));
+    addr.sin_family      = AF_INET;
+    addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    addr.sin_port        = htons(kMdnsPort);
+
+    if (bind(fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) < 0)
+    {
+        LogCrit("bind() to mDNS port for IPv4 socket failed, errno: %s", strerror(errno));
+        error = OT_ERROR_FAILED;
+        ExitNow();
+    }
+
+    mFd4 = fd;
+
+    LogInfo("Successfully opened IPv4 socket");
+
+exit:
+    return error;
+}
+
+otError MdnsSocket::JoinOrLeaveIp4MulticastGroup(bool aJoin, uint32_t aInfraIfIndex)
+{
+    struct ip_mreqn mreqn;
+
+    memset(&mreqn, 0, sizeof(mreqn));
+    memcpy(&mreqn.imr_multiaddr.s_addr, &mMulticastIp4Address, sizeof(otIp4Address));
+    mreqn.imr_ifindex = aInfraIfIndex;
+
+    if (aJoin)
+    {
+        // Suggested workaround for netif not dropping
+        // a previous multicast membership.
+        setsockopt(mFd4, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreqn, sizeof(mreqn));
+    }
+
+    return SetSocketOption(mFd4, IPPROTO_IP, aJoin ? IP_ADD_MEMBERSHIP : IP_DROP_MEMBERSHIP, mreqn,
+                           "IP_ADD/DROP_MEMBERSHIP");
+}
+
+void MdnsSocket::CloseIp4Socket(void)
+{
+    if (mFd4 >= 0)
+    {
+        close(mFd4);
+        mFd4 = -1;
+    }
+}
+
+otError MdnsSocket::OpenIp6Socket(uint32_t aInfraIfIndex)
+{
+    otError             error = OT_ERROR_FAILED;
+    struct sockaddr_in6 addr6;
+    int                 fd;
+    int                 ifindex = static_cast<int>(aInfraIfIndex);
+
+    fd = socket(AF_INET6, SOCK_DGRAM, 0);
+    VerifyOrExit(fd >= 0, LogCrit("Failed to create IPv4 socket"));
+
+#ifdef __linux__
+    {
+        char        nameBuffer[IF_NAMESIZE];
+        const char *ifname;
+
+        ifname = if_indextoname(aInfraIfIndex, nameBuffer);
+        VerifyOrExit(ifname != NULL, LogCrit("if_indextoname() failed"));
+
+        error = SetSocketOptionValue(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname), "SO_BINDTODEVICE");
+        SuccessOrExit(error);
+    }
+#else
+    {
+        SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IPV6, IPV6_BOUND_IF, ifindex, "IPV6_BOUND_IF"));
+    }
+#endif
+
+    SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, 255, "IPV6_MULTICAST_HOPS"));
+    SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, 255, "IPV6_UNICAST_HOPS"));
+    SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IPV6, IPV6_V6ONLY, 1, "IPV6_V6ONLY"));
+    SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, ifindex, "IPV6_MULTICAST_IF"));
+    SuccessOrExit(error = SetSocketOption<int>(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, 1, "IPV6_MULTICAST_LOOP"));
+    SuccessOrExit(error = SetReuseAddrPortOptions(fd));
+
+    memset(&addr6, 0, sizeof(addr6));
+    addr6.sin6_family = AF_INET6;
+    addr6.sin6_port   = htons(kMdnsPort);
+
+    if (bind(fd, reinterpret_cast<struct sockaddr *>(&addr6), sizeof(addr6)) < 0)
+    {
+        LogCrit("bind() to mDNS port for IPv6 socket failed, errno: %s", strerror(errno));
+        error = OT_ERROR_FAILED;
+        ExitNow();
+    }
+
+    mFd6 = fd;
+
+    LogInfo("Successfully opened IPv6 socket");
+
+exit:
+    return error;
+}
+
+#ifndef IPV6_ADD_MEMBERSHIP
+#ifdef IPV6_JOIN_GROUP
+#define IPV6_ADD_MEMBERSHIP IPV6_JOIN_GROUP
+#endif
+#endif
+
+#ifndef IPV6_DROP_MEMBERSHIP
+#ifdef IPV6_LEAVE_GROUP
+#define IPV6_DROP_MEMBERSHIP IPV6_LEAVE_GROUP
+#endif
+#endif
+
+otError MdnsSocket::JoinOrLeaveIp6MulticastGroup(bool aJoin, uint32_t aInfraIfIndex)
+{
+    struct ipv6_mreq mreq6;
+
+    memset(&mreq6, 0, sizeof(mreq6));
+    Ip6Utils::CopyIp6AddressTo(mMulticastIp6Address, &mreq6.ipv6mr_multiaddr);
+
+    mreq6.ipv6mr_interface = static_cast<int>(aInfraIfIndex);
+
+    if (aJoin)
+    {
+        // Suggested workaround for netif not dropping
+        // a previous multicast membership.
+        setsockopt(mFd6, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, &mreq6, sizeof(mreq6));
+    }
+
+    return SetSocketOptionValue(mFd6, IPPROTO_IPV6, aJoin ? IPV6_ADD_MEMBERSHIP : IPV6_DROP_MEMBERSHIP, &mreq6,
+                                sizeof(mreq6), "IP6_ADD/DROP_MEMBERSHIP");
+}
+
+void MdnsSocket::CloseIp6Socket(void)
+{
+    if (mFd6 >= 0)
+    {
+        close(mFd6);
+        mFd6 = -1;
+    }
+}
+
+otError MdnsSocket::SetReuseAddrPortOptions(int aFd)
+{
+    otError error;
+
+    SuccessOrExit(error = SetSocketOption<int>(aFd, SOL_SOCKET, SO_REUSEADDR, 1, "SO_REUSEADDR"));
+    SuccessOrExit(error = SetSocketOption<int>(aFd, SOL_SOCKET, SO_REUSEPORT, 1, "SO_REUSEPORT"));
+
+exit:
+    return error;
+}
+
+otError MdnsSocket::SetSocketOptionValue(int         aFd,
+                                         int         aLevel,
+                                         int         aOption,
+                                         const void *aValue,
+                                         uint32_t    aValueLength,
+                                         const char *aOptionName)
+{
+    otError error = OT_ERROR_NONE;
+
+    if (setsockopt(aFd, aLevel, aOption, aValue, aValueLength) != 0)
+    {
+        error = OT_ERROR_FAILED;
+        LogCrit("Failed to setsockopt(%s) - errno: %s", aOptionName, strerror(errno));
+    }
+
+    return error;
+}
+
+} // namespace Posix
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
diff --git a/src/posix/platform/mdns_socket.hpp b/src/posix/platform/mdns_socket.hpp
new file mode 100644
index 0000000..af60733
--- /dev/null
+++ b/src/posix/platform/mdns_socket.hpp
@@ -0,0 +1,181 @@
+/*
+ *  Copyright (c) 2024, 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 OT_POSIX_PLATFORM_MDNS_SOCKET_HPP_
+#define OT_POSIX_PLATFORM_MDNS_SOCKET_HPP_
+
+#include "openthread-posix-config.h"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+#include <openthread/nat64.h>
+#include <openthread/platform/mdns_socket.h>
+
+#include "logger.hpp"
+#include "mainloop.hpp"
+
+#include "core/common/non_copyable.hpp"
+
+namespace ot {
+namespace Posix {
+
+/**
+ * Implements platform mDNS socket APIs.
+ *
+ */
+class MdnsSocket : public Mainloop::Source, public Logger<MdnsSocket>, private NonCopyable
+{
+public:
+    static const char kLogModuleName[]; ///< Module name used for logging.
+
+    /**
+     * Gets the `MdnsSocket` singleton.
+     *
+     * @returns The singleton object.
+     *
+     */
+    static MdnsSocket &Get(void);
+
+    /**
+     * Initializes the `MdnsSocket`.
+     *
+     * Called before OpenThread instance is created.
+     *
+     */
+    void Init(void);
+
+    /**
+     * Sets up the `MdnsSocket`.
+     *
+     * Called after OpenThread instance is created.
+     *
+     */
+    void SetUp(void);
+
+    /**
+     * Tears down the `MdnsSocket`.
+     *
+     * Called before OpenThread instance is destructed.
+     *
+     */
+    void TearDown(void);
+
+    /**
+     * Deinitializes the `MdnsSocket`.
+     *
+     * Called after OpenThread instance is destructed.
+     *
+     */
+    void Deinit(void);
+
+    /**
+     * Updates the fd_set and timeout for mainloop.
+     *
+     * @param[in,out]   aContext    A reference to the mainloop context.
+     *
+     */
+    void Update(otSysMainloopContext &aContext) override;
+
+    /**
+     * Performs `MdnsSocket` processing.
+     *
+     * @param[in]   aContext   A reference to the mainloop context.
+     *
+     */
+    void Process(const otSysMainloopContext &aContext) override;
+
+    // otPlatMdns APIs
+    otError SetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex);
+    void    SendMulticast(otMessage *aMessage, uint32_t aInfraIfIndex);
+    void    SendUnicast(otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress);
+
+private:
+    static constexpr uint16_t kMaxMessageLength = 2000;
+    static constexpr uint16_t kMdnsPort         = 5353;
+
+    enum MsgType : uint8_t
+    {
+        kIp6Msg,
+        kIp4Msg,
+    };
+
+    struct Metadata
+    {
+        bool CanFreeMessage(void) const { return (mIp6Port == 0) && (mIp4Port == 0); }
+
+        otIp6Address mIp6Address;
+        uint16_t     mIp6Port;
+        otIp4Address mIp4Address;
+        uint16_t     mIp4Port;
+    };
+
+    bool           mEnabled;
+    uint32_t       mInfraIfIndex;
+    int            mFd4;
+    int            mFd6;
+    uint32_t       mPendingIp6Tx;
+    uint32_t       mPendingIp4Tx;
+    otMessageQueue mTxQueue;
+    otIp6Address   mMulticastIp6Address;
+    otIp4Address   mMulticastIp4Address;
+    otInstance    *mInstance;
+
+    otError Enable(uint32_t aInfraIfIndex);
+    void    Disable(uint32_t aInfraIfIndex);
+    void    ClearTxQueue(void);
+    void    SendQueuedMessages(MsgType aMsgType);
+    void    ReceiveMessage(MsgType aMsgType);
+
+    otError OpenIp4Socket(uint32_t aInfraIfIndex);
+    otError JoinOrLeaveIp4MulticastGroup(bool aJoin, uint32_t aInfraIfIndex);
+    void    CloseIp4Socket(void);
+    otError OpenIp6Socket(uint32_t aInfraIfIndex);
+    otError JoinOrLeaveIp6MulticastGroup(bool aJoin, uint32_t aInfraIfIndex);
+    void    CloseIp6Socket(void);
+
+    static otError SetReuseAddrPortOptions(int aFd);
+
+    template <typename ValueType>
+    static otError SetSocketOption(int aFd, int aLevel, int aOption, const ValueType &aValue, const char *aOptionName)
+    {
+        return SetSocketOptionValue(aFd, aLevel, aOption, &aValue, sizeof(ValueType), aOptionName);
+    }
+
+    static otError SetSocketOptionValue(int         aFd,
+                                        int         aLevel,
+                                        int         aOption,
+                                        const void *aValue,
+                                        uint32_t    aValueLength,
+                                        const char *aOptionName);
+};
+
+} // namespace Posix
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+#endif // OT_POSIX_PLATFORM_MDNS_SOCKET_HPP_
diff --git a/src/posix/platform/multicast_routing.cpp b/src/posix/platform/multicast_routing.cpp
index a224ae1..864f984 100644
--- a/src/posix/platform/multicast_routing.cpp
+++ b/src/posix/platform/multicast_routing.cpp
@@ -54,19 +54,21 @@
 namespace ot {
 namespace Posix {
 
-#define LogResult(aError, ...)                                                                                      \
-    do                                                                                                              \
-    {                                                                                                               \
-        otError _err = (aError);                                                                                    \
-                                                                                                                    \
-        if (_err == OT_ERROR_NONE)                                                                                  \
-        {                                                                                                           \
-            otLogInfoPlat(OT_FIRST_ARG(__VA_ARGS__) ": %s" OT_REST_ARGS(__VA_ARGS__), otThreadErrorToString(_err)); \
-        }                                                                                                           \
-        else                                                                                                        \
-        {                                                                                                           \
-            otLogWarnPlat(OT_FIRST_ARG(__VA_ARGS__) ": %s" OT_REST_ARGS(__VA_ARGS__), otThreadErrorToString(_err)); \
-        }                                                                                                           \
+const char MulticastRoutingManager::kLogModuleName[] = "McastRtMgr";
+
+#define LogResult(aError, ...)                                                                                \
+    do                                                                                                        \
+    {                                                                                                         \
+        otError _err = (aError);                                                                              \
+                                                                                                              \
+        if (_err == OT_ERROR_NONE)                                                                            \
+        {                                                                                                     \
+            LogInfo(OT_FIRST_ARG(__VA_ARGS__) ": %s" OT_REST_ARGS(__VA_ARGS__), otThreadErrorToString(_err)); \
+        }                                                                                                     \
+        else                                                                                                  \
+        {                                                                                                     \
+            LogWarn(OT_FIRST_ARG(__VA_ARGS__) ": %s" OT_REST_ARGS(__VA_ARGS__), otThreadErrorToString(_err)); \
+        }                                                                                                     \
     } while (false)
 
 void MulticastRoutingManager::SetUp(void)
@@ -114,7 +116,7 @@
 
     InitMulticastRouterSock();
 
-    LogResult(OT_ERROR_NONE, "MulticastRoutingManager: %s", __FUNCTION__);
+    LogResult(OT_ERROR_NONE, "%s", __FUNCTION__);
 exit:
     return;
 }
@@ -123,7 +125,7 @@
 {
     FinalizeMulticastRouterSock();
 
-    LogResult(OT_ERROR_NONE, "MulticastRoutingManager: %s", __FUNCTION__);
+    LogResult(OT_ERROR_NONE, "%s", __FUNCTION__);
 }
 
 void MulticastRoutingManager::Add(const Ip6::Address &aAddress)
@@ -133,7 +135,7 @@
     UnblockInboundMulticastForwardingCache(aAddress);
     UpdateMldReport(aAddress, true);
 
-    LogResult(OT_ERROR_NONE, "MulticastRoutingManager: %s: %s", __FUNCTION__, aAddress.ToString().AsCString());
+    LogResult(OT_ERROR_NONE, "%s: %s", __FUNCTION__, aAddress.ToString().AsCString());
 
 exit:
     return;
@@ -148,7 +150,7 @@
     RemoveInboundMulticastForwardingCache(aAddress);
     UpdateMldReport(aAddress, false);
 
-    LogResult(error, "MulticastRoutingManager: %s: %s", __FUNCTION__, aAddress.ToString().AsCString());
+    LogResult(error, "%s: %s", __FUNCTION__, aAddress.ToString().AsCString());
 
 exit:
     return;
@@ -166,8 +168,7 @@
                  ? OT_ERROR_FAILED
                  : OT_ERROR_NONE);
 
-    LogResult(error, "MulticastRoutingManager: %s: address %s %s", __FUNCTION__, aAddress.ToString().AsCString(),
-              (isAdd ? "Added" : "Removed"));
+    LogResult(error, "%s: address %s %s", __FUNCTION__, aAddress.ToString().AsCString(), (isAdd ? "Added" : "Removed"));
 }
 
 bool MulticastRoutingManager::HasMulticastListener(const Ip6::Address &aAddress) const
@@ -283,7 +284,7 @@
     error = AddMulticastForwardingCache(src, dst, static_cast<MifIndex>(mrt6msg->im6_mif));
 
 exit:
-    LogResult(error, "MulticastRoutingManager: %s", __FUNCTION__);
+    LogResult(error, "%s", __FUNCTION__);
 }
 
 otError MulticastRoutingManager::AddMulticastForwardingCache(const Ip6::Address &aSrcAddr,
@@ -340,9 +341,8 @@
 
     SaveMulticastForwardingCache(aSrcAddr, aGroupAddr, aIif, forwardMif);
 exit:
-    LogResult(error, "MulticastRoutingManager: %s: add dynamic route: %s %s => %s %s", __FUNCTION__,
-              MifIndexToString(aIif), aSrcAddr.ToString().AsCString(), aGroupAddr.ToString().AsCString(),
-              MifIndexToString(forwardMif));
+    LogResult(error, "%s: add dynamic route: %s %s => %s %s", __FUNCTION__, MifIndexToString(aIif),
+              aSrcAddr.ToString().AsCString(), aGroupAddr.ToString().AsCString(), MifIndexToString(forwardMif));
 
     return error;
 }
@@ -377,7 +377,7 @@
 
         mfc.Set(kMifIndexBackbone, kMifIndexThread);
 
-        LogResult(error, "MulticastRoutingManager: %s: %s %s => %s %s", __FUNCTION__, MifIndexToString(mfc.mIif),
+        LogResult(error, "%s: %s %s => %s %s", __FUNCTION__, MifIndexToString(mfc.mIif),
                   mfc.mSrcAddr.ToString().AsCString(), mfc.mGroupAddr.ToString().AsCString(),
                   MifIndexToString(kMifIndexThread));
     }
@@ -439,9 +439,9 @@
     {
         unsigned long validPktCnt;
 
-        otLogDebgPlat("MulticastRoutingManager: %s: SIOCGETSGCNT_IN6 %s => %s: bytecnt=%lu, pktcnt=%lu, wrong_if=%lu",
-                      __FUNCTION__, aMfc.mSrcAddr.ToString().AsCString(), aMfc.mGroupAddr.ToString().AsCString(),
-                      sioc_sg_req6.bytecnt, sioc_sg_req6.pktcnt, sioc_sg_req6.wrong_if);
+        LogDebg("%s: SIOCGETSGCNT_IN6 %s => %s: bytecnt=%lu, pktcnt=%lu, wrong_if=%lu", __FUNCTION__,
+                aMfc.mSrcAddr.ToString().AsCString(), aMfc.mGroupAddr.ToString().AsCString(), sioc_sg_req6.bytecnt,
+                sioc_sg_req6.pktcnt, sioc_sg_req6.wrong_if);
 
         validPktCnt = sioc_sg_req6.pktcnt - sioc_sg_req6.wrong_if;
         if (validPktCnt != aMfc.mValidPktCnt)
@@ -453,8 +453,8 @@
     }
     else
     {
-        otLogDebgPlat("MulticastRoutingManager: %s: SIOCGETSGCNT_IN6 %s => %s failed: %s", __FUNCTION__,
-                      aMfc.mSrcAddr.ToString().AsCString(), aMfc.mGroupAddr.ToString().AsCString(), strerror(errno));
+        LogDebg("%s: SIOCGETSGCNT_IN6 %s => %s failed: %s", __FUNCTION__, aMfc.mSrcAddr.ToString().AsCString(),
+                aMfc.mGroupAddr.ToString().AsCString(), strerror(errno));
     }
 
     return updated;
@@ -483,19 +483,18 @@
 void MulticastRoutingManager::DumpMulticastForwardingCache(void) const
 {
 #if OPENTHREAD_CONFIG_LOG_PLATFORM && (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_DEBG)
-    otLogDebgPlat("MulticastRoutingManager: ==================== MFC ENTRIES ====================");
+    LogDebg("==================== MFC ENTRIES ====================");
 
     for (const MulticastForwardingCache &mfc : mMulticastForwardingCacheTable)
     {
         if (mfc.IsValid())
         {
-            otLogDebgPlat("MulticastRoutingManager: %s %s => %s %s", MifIndexToString(mfc.mIif),
-                          mfc.mSrcAddr.ToString().AsCString(), mfc.mGroupAddr.ToString().AsCString(),
-                          MifIndexToString(mfc.mOif));
+            LogDebg("%s %s => %s %s", MifIndexToString(mfc.mIif), mfc.mSrcAddr.ToString().AsCString(),
+                    mfc.mGroupAddr.ToString().AsCString(), MifIndexToString(mfc.mOif));
         }
     }
 
-    otLogDebgPlat("MulticastRoutingManager: =====================================================");
+    LogDebg("=====================================================");
 #endif
 }
 
@@ -605,7 +604,7 @@
                 ? OT_ERROR_NONE
                 : OT_ERROR_FAILED;
 
-    LogResult(error, "MulticastRoutingManager: %s: %s %s => %s %s", __FUNCTION__, MifIndexToString(aMfc.mIif),
+    LogResult(error, "%s: %s %s => %s %s", __FUNCTION__, MifIndexToString(aMfc.mIif),
               aMfc.mSrcAddr.ToString().AsCString(), aMfc.mGroupAddr.ToString().AsCString(),
               MifIndexToString(aMfc.mOif));
 
diff --git a/src/posix/platform/multicast_routing.hpp b/src/posix/platform/multicast_routing.hpp
index c4d7e32..9d4e049 100644
--- a/src/posix/platform/multicast_routing.hpp
+++ b/src/posix/platform/multicast_routing.hpp
@@ -39,18 +39,21 @@
 #include <openthread/backbone_router_ftd.h>
 #include <openthread/openthread-system.h>
 
+#include "logger.hpp"
+#include "mainloop.hpp"
 #include "platform-posix.h"
 #include "core/common/non_copyable.hpp"
 #include "core/net/ip6_address.hpp"
 #include "lib/url/url.hpp"
-#include "posix/platform/mainloop.hpp"
 
 namespace ot {
 namespace Posix {
 
-class MulticastRoutingManager : public Mainloop::Source, private NonCopyable
+class MulticastRoutingManager : public Mainloop::Source, public Logger<MulticastRoutingManager>, private NonCopyable
 {
 public:
+    static const char kLogModuleName[];
+
     explicit MulticastRoutingManager()
 
         : mLastExpireTime(0)
diff --git a/src/posix/platform/netif.cpp b/src/posix/platform/netif.cpp
index d3b3024..e0a4c49 100644
--- a/src/posix/platform/netif.cpp
+++ b/src/posix/platform/netif.cpp
@@ -147,14 +147,14 @@
 #include <openthread/message.h>
 #include <openthread/nat64.h>
 #include <openthread/netdata.h>
+#include <openthread/thread.h>
 #include <openthread/platform/border_routing.h>
 #include <openthread/platform/misc.h>
 
-#include "common/code_utils.hpp"
-#include "common/debug.hpp"
-#include "net/ip6_address.hpp"
-
+#include "ip6_utils.hpp"
+#include "logger.hpp"
 #include "resolver.hpp"
+#include "common/code_utils.hpp"
 
 unsigned int gNetifIndex = 0;
 char         gNetifName[IFNAMSIZ];
@@ -167,10 +167,10 @@
 unsigned int otSysGetThreadNetifIndex(void) { return gNetifIndex; }
 
 #if OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
+
 #if OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE
 #include "firewall.hpp"
 #endif
-#include "posix/platform/ip6_utils.hpp"
 
 using namespace ot::Posix::Ip6Utils;
 
@@ -191,7 +191,7 @@
 #define OPENTHREAD_POSIX_TUN_DEVICE "/dev/net/tun"
 #endif
 
-#endif // OPENTHREAD_TUN_DEVICE
+#endif // OPENTHREAD_POSIX_TUN_DEVICE
 
 #ifdef __linux__
 static uint32_t sNetlinkSequence = 0; ///< Netlink message sequence.
@@ -291,33 +291,104 @@
 
 #define OPENTHREAD_POSIX_LOG_TUN_PACKETS 0
 
+static const char kLogModuleName[] = "Netif";
+
+static void LogCrit(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_CRIT, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogWarn(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_WARN, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogNote(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_NOTE, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogInfo(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_INFO, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogDebg(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_DEBG, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
 #if defined(__APPLE__) || defined(__NetBSD__) || defined(__FreeBSD__)
-static const uint8_t allOnes[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
-                                  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+
+static const uint8_t kAllOnes[] = {
+    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+};
 
 #define BITS_PER_BYTE 8
 #define MAX_PREFIX_LENGTH (OT_IP6_ADDRESS_SIZE * BITS_PER_BYTE)
 
+static void CopyBits(uint8_t *aDst, const uint8_t *aSrc, uint8_t aNumBits)
+{
+    // Copies `aNumBits` from `aSrc` into `aDst` handling
+    // the case where `aNumBits` may not be a multiple of 8.
+    // Leaves the remaining bits beyond `aNumBits` in `aDst`
+    // unchanged.
+
+    uint8_t numBytes  = aNumBits / BITS_PER_BYTE;
+    uint8_t extraBits = aNumBits % BITS_PER_BYTE;
+
+    memcpy(aDst, aSrc, numBytes);
+
+    if (extraBits > 0)
+    {
+        uint8_t mask = ((0x80 >> (extraBits - 1)) - 1);
+
+        aDst[numBytes] &= mask;
+        aDst[numBytes] |= (aSrc[numBytes] & ~mask);
+    }
+}
+
 static void InitNetaskWithPrefixLength(struct in6_addr *address, uint8_t prefixLen)
 {
-    ot::Ip6::Address addr;
+    otIp6Address addr;
 
     if (prefixLen > MAX_PREFIX_LENGTH)
     {
         prefixLen = MAX_PREFIX_LENGTH;
     }
 
-    addr.Clear();
-    addr.SetPrefix(allOnes, prefixLen);
-    memcpy(address, addr.mFields.m8, sizeof(addr.mFields.m8));
+    memset(&addr, 0, sizeof(otIp6Address));
+    CopyBits(addr.mFields.m8, kAllOnes, prefixLen);
+    CopyIp6AddressTo(addr, address);
 }
 
 static uint8_t NetmaskToPrefixLength(const struct sockaddr_in6 *netmask)
 {
     return otIp6PrefixMatch(reinterpret_cast<const otIp6Address *>(netmask->sin6_addr.s6_addr),
-                            reinterpret_cast<const otIp6Address *>(allOnes));
+                            reinterpret_cast<const otIp6Address *>(kAllOnes));
 }
-#endif
+
+#endif // defined(__APPLE__) || defined(__NetBSD__) || defined(__FreeBSD__)
 
 #ifdef __linux__
 #pragma GCC diagnostic push
@@ -412,7 +483,9 @@
 #endif
     {
 #if OPENTHREAD_POSIX_CONFIG_NETIF_PREFIX_ROUTE_METRIC > 0
-        if (aAddressInfo.mScope > ot::Ip6::Address::kLinkLocalScope)
+        static constexpr kLinkLocalScope = 2;
+
+        if (aAddressInfo.mScope > kLinkLocalScope)
         {
             AddRtAttrUint32(&req.nh, sizeof(req), IFA_RT_PRIORITY, OPENTHREAD_POSIX_CONFIG_NETIF_PREFIX_ROUTE_METRIC);
         }
@@ -421,13 +494,13 @@
 
     if (send(sNetlinkFd, &req, req.nh.nlmsg_len, 0) != -1)
     {
-        otLogInfoPlat("[netif] Sent request#%u to %s %s/%u", sNetlinkSequence, (aIsAdded ? "add" : "remove"),
-                      Ip6AddressString(aAddressInfo.mAddress).AsCString(), aAddressInfo.mPrefixLength);
+        LogInfo("Sent request#%u to %s %s/%u", sNetlinkSequence, (aIsAdded ? "add" : "remove"),
+                Ip6AddressString(aAddressInfo.mAddress).AsCString(), aAddressInfo.mPrefixLength);
     }
     else
     {
-        otLogWarnPlat("[netif] Failed to send request#%u to %s %s/%u", sNetlinkSequence, (aIsAdded ? "add" : "remove"),
-                      Ip6AddressString(aAddressInfo.mAddress).AsCString(), aAddressInfo.mPrefixLength);
+        LogWarn("Failed to send request#%u to %s %s/%u", sNetlinkSequence, (aIsAdded ? "add" : "remove"),
+                Ip6AddressString(aAddressInfo.mAddress).AsCString(), aAddressInfo.mPrefixLength);
     }
 }
 
@@ -468,14 +541,13 @@
         rval = ioctl(sIpFd, aIsAdded ? SIOCAIFADDR_IN6 : SIOCDIFADDR_IN6, &ifr6);
         if (rval == 0)
         {
-            otLogInfoPlat("[netif] %s %s/%u", (aIsAdded ? "Added" : "Removed"),
-                          Ip6AddressString(aAddressInfo.mAddress).AsCString(), aAddressInfo.mPrefixLength);
+            LogInfo("%s %s/%u", (aIsAdded ? "Added" : "Removed"), Ip6AddressString(aAddressInfo.mAddress).AsCString(),
+                    aAddressInfo.mPrefixLength);
         }
         else if (errno != EALREADY)
         {
-            otLogWarnPlat("[netif] Failed to %s %s/%u: %s", (aIsAdded ? "add" : "remove"),
-                          Ip6AddressString(aAddressInfo.mAddress).AsCString(), aAddressInfo.mPrefixLength,
-                          strerror(errno));
+            LogWarn("Failed to %s %s/%u: %s", (aIsAdded ? "add" : "remove"),
+                    Ip6AddressString(aAddressInfo.mAddress).AsCString(), aAddressInfo.mPrefixLength, strerror(errno));
         }
     }
 #endif
@@ -507,21 +579,20 @@
         char addressString[INET6_ADDRSTRLEN + 1];
 
         inet_ntop(AF_INET6, mreq.ipv6mr_multiaddr.s6_addr, addressString, sizeof(addressString));
-        otLogWarnPlat("[netif] Ignoring %s failure (EINVAL) for MC LINKLOCAL address (%s)",
-                      aIsAdded ? "IPV6_JOIN_GROUP" : "IPV6_LEAVE_GROUP", addressString);
+        LogWarn("Ignoring %s failure (EINVAL) for MC LINKLOCAL address (%s)",
+                aIsAdded ? "IPV6_JOIN_GROUP" : "IPV6_LEAVE_GROUP", addressString);
         err = 0;
     }
 #endif
 
     if (err != 0)
     {
-        otLogWarnPlat("[netif] %s failure (%d)", aIsAdded ? "IPV6_JOIN_GROUP" : "IPV6_LEAVE_GROUP", errno);
+        LogWarn("%s failure (%d)", aIsAdded ? "IPV6_JOIN_GROUP" : "IPV6_LEAVE_GROUP", errno);
         error = OT_ERROR_FAILED;
         ExitNow();
     }
 
-    otLogInfoPlat("[netif] %s multicast address %s", aIsAdded ? "Added" : "Removed",
-                  Ip6AddressString(&aAddress).AsCString());
+    LogInfo("%s multicast address %s", aIsAdded ? "Added" : "Removed", Ip6AddressString(&aAddress).AsCString());
 
 exit:
     SuccessOrDie(error);
@@ -544,8 +615,8 @@
 
     ifState = ((ifr.ifr_flags & IFF_UP) == IFF_UP) ? true : false;
 
-    otLogNotePlat("[netif] Changing interface state to %s%s.", aState ? "up" : "down",
-                  (ifState == aState) ? " (already done, ignoring)" : "");
+    LogNote("Changing interface state to %s%s.", aState ? "up" : "down",
+            (ifState == aState) ? " (already done, ignoring)" : "");
 
     if (ifState != aState)
     {
@@ -560,7 +631,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogWarnPlat("[netif] Failed to update state %s", otThreadErrorToString(error));
+        LogWarn("Failed to update state %s", otThreadErrorToString(error));
     }
 }
 
@@ -723,15 +794,14 @@
         otIp6PrefixToString(&sAddedOmrRoutes[i], prefixString, sizeof(prefixString));
         if ((error = DeleteRoute(sAddedOmrRoutes[i])) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] Failed to delete an OMR route %s in kernel: %s", prefixString,
-                          otThreadErrorToString(error));
+            LogWarn("Failed to delete an OMR route %s in kernel: %s", prefixString, otThreadErrorToString(error));
         }
         else
         {
             sAddedOmrRoutes[i] = sAddedOmrRoutes[sAddedOmrRoutesNum - 1];
             --sAddedOmrRoutesNum;
             --i;
-            otLogInfoPlat("[netif] Successfully deleted an OMR route %s in kernel", prefixString);
+            LogInfo("Successfully deleted an OMR route %s in kernel", prefixString);
         }
     }
 
@@ -746,13 +816,12 @@
         otIp6PrefixToString(&config.mPrefix, prefixString, sizeof(prefixString));
         if ((error = AddOmrRoute(config.mPrefix)) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] Failed to add an OMR route %s in kernel: %s", prefixString,
-                          otThreadErrorToString(error));
+            LogWarn("Failed to add an OMR route %s in kernel: %s", prefixString, otThreadErrorToString(error));
         }
         else
         {
             sAddedOmrRoutes[sAddedOmrRoutesNum++] = config.mPrefix;
-            otLogInfoPlat("[netif] Successfully added an OMR route %s in kernel", prefixString);
+            LogInfo("Successfully added an OMR route %s in kernel", prefixString);
         }
     }
 }
@@ -819,15 +888,14 @@
         otIp6PrefixToString(&sAddedExternalRoutes[i], prefixString, sizeof(prefixString));
         if ((error = DeleteRoute(sAddedExternalRoutes[i])) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] Failed to delete an external route %s in kernel: %s", prefixString,
-                          otThreadErrorToString(error));
+            LogWarn("Failed to delete an external route %s in kernel: %s", prefixString, otThreadErrorToString(error));
         }
         else
         {
             sAddedExternalRoutes[i] = sAddedExternalRoutes[sAddedExternalRoutesNum - 1];
             --sAddedExternalRoutesNum;
             --i;
-            otLogWarnPlat("[netif] Successfully deleted an external route %s in kernel", prefixString);
+            LogWarn("Successfully deleted an external route %s in kernel", prefixString);
         }
     }
 
@@ -838,18 +906,17 @@
             continue;
         }
         VerifyOrExit(sAddedExternalRoutesNum < kMaxExternalRoutesNum,
-                     otLogWarnPlat("[netif] No buffer to add more external routes in kernel"));
+                     LogWarn("No buffer to add more external routes in kernel"));
 
         otIp6PrefixToString(&config.mPrefix, prefixString, sizeof(prefixString));
         if ((error = AddExternalRoute(config.mPrefix)) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] Failed to add an external route %s in kernel: %s", prefixString,
-                          otThreadErrorToString(error));
+            LogWarn("Failed to add an external route %s in kernel: %s", prefixString, otThreadErrorToString(error));
         }
         else
         {
             sAddedExternalRoutes[sAddedExternalRoutesNum++] = config.mPrefix;
-            otLogWarnPlat("[netif] Successfully added an external route %s in kernel", prefixString);
+            LogWarn("Successfully added an external route %s in kernel", prefixString);
         }
     }
 exit:
@@ -914,30 +981,30 @@
         {
             if ((error = DeleteIp4Route(sActiveNat64Cidr)) != OT_ERROR_NONE)
             {
-                otLogWarnPlat("[netif] failed to delete route for NAT64: %s", otThreadErrorToString(error));
+                LogWarn("failed to delete route for NAT64: %s", otThreadErrorToString(error));
             }
         }
         sActiveNat64Cidr = translatorCidr;
 
         otIp4CidrToString(&translatorCidr, cidrString, sizeof(cidrString));
-        otLogInfoPlat("[netif] NAT64 CIDR updated to %s.", cidrString);
+        LogInfo("NAT64 CIDR updated to %s.", cidrString);
     }
 
     if (otNat64GetTranslatorState(gInstance) == OT_NAT64_STATE_ACTIVE)
     {
         if ((error = AddIp4Route(sActiveNat64Cidr, kNat64RoutePriority)) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] failed to add route for NAT64: %s", otThreadErrorToString(error));
+            LogWarn("failed to add route for NAT64: %s", otThreadErrorToString(error));
         }
-        otLogInfoPlat("[netif] Adding route for NAT64");
+        LogInfo("Adding route for NAT64");
     }
     else if (sActiveNat64Cidr.mLength > 0) // Translator is not active.
     {
         if ((error = DeleteIp4Route(sActiveNat64Cidr)) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] failed to delete route for NAT64: %s", otThreadErrorToString(error));
+            LogWarn("failed to delete route for NAT64: %s", otThreadErrorToString(error));
         }
-        otLogInfoPlat("[netif] Deleting route for NAT64");
+        LogInfo("Deleting route for NAT64");
     }
 
 exit:
@@ -993,7 +1060,7 @@
     VerifyOrExit(otMessageRead(aMessage, 0, &packet[offset], maxLength) == length, error = OT_ERROR_NO_BUFS);
 
 #if OPENTHREAD_POSIX_LOG_TUN_PACKETS
-    otLogInfoPlat("[netif] Packet from NCP (%u bytes)", static_cast<uint16_t>(length));
+    LogInfo("Packet from NCP (%u bytes)", static_cast<uint16_t>(length));
     otDumpInfoPlat("", &packet[offset], length);
 #endif
 
@@ -1012,7 +1079,7 @@
 
     if (error != OT_ERROR_NONE)
     {
-        otLogWarnPlat("[netif] Failed to receive, error:%s", otThreadErrorToString(error));
+        LogWarn("Failed to receive, error:%s", otThreadErrorToString(error));
     }
 }
 
@@ -1069,7 +1136,7 @@
     VerifyOrExit(ra != nullptr, error = OT_ERROR_INVALID_ARGS);
 
 #if OPENTHREAD_POSIX_LOG_TUN_PACKETS
-    otLogInfoPlat("[netif] RA to BorderRouting (%hu bytes)", static_cast<uint16_t>(length));
+    LogInfo("RA to BorderRouting (%hu bytes)", static_cast<uint16_t>(length));
     otDumpInfoPlat("", data, static_cast<size_t>(length));
 #endif
 
@@ -1169,7 +1236,7 @@
     }
 
 #if OPENTHREAD_POSIX_LOG_TUN_PACKETS
-    otLogInfoPlat("[netif] Packet to NCP (%hu bytes)", static_cast<uint16_t>(rval));
+    LogInfo("Packet to NCP (%hu bytes)", static_cast<uint16_t>(rval));
     otDumpInfoPlat("", &packet[offset], static_cast<size_t>(rval));
 #endif
 
@@ -1192,33 +1259,33 @@
     {
         if (error == OT_ERROR_DROP)
         {
-            otLogInfoPlat("[netif] Message dropped by Thread");
+            LogInfo("Message dropped by Thread");
         }
         else
         {
-            otLogWarnPlat("[netif] Failed to transmit, error:%s", otThreadErrorToString(error));
+            LogWarn("Failed to transmit, error:%s", otThreadErrorToString(error));
         }
     }
 }
 
-static void logAddrEvent(bool isAdd, const ot::Ip6::Address &aAddress, otError error)
+static void logAddrEvent(bool isAdd, const otIp6Address &aAddress, otError error)
 {
     OT_UNUSED_VARIABLE(aAddress);
 
     if ((error == OT_ERROR_NONE) || ((isAdd) && (error == OT_ERROR_ALREADY || error == OT_ERROR_REJECTED)) ||
         ((!isAdd) && (error == OT_ERROR_NOT_FOUND || error == OT_ERROR_REJECTED)))
     {
-        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)"
-                                                    : "");
+        LogInfo("%s [%s] %s%s", isAdd ? "ADD" : "DEL", IsIp6AddressMulticast(aAddress) ? "M" : "U",
+                Ip6AddressString(&aAddress).AsCString(),
+                error == OT_ERROR_ALREADY     ? " (already subscribed, ignored)"
+                : error == OT_ERROR_REJECTED  ? " (rejected)"
+                : error == OT_ERROR_NOT_FOUND ? " (not found, ignored)"
+                                              : "");
     }
     else
     {
-        otLogWarnPlat("[netif] %s [%s] %s failed (%s)", isAdd ? "ADD" : "DEL", aAddress.IsMulticast() ? "M" : "U",
-                      aAddress.ToString().AsCString(), otThreadErrorToString(error));
+        LogWarn("%s [%s] %s failed (%s)", isAdd ? "ADD" : "DEL", IsIp6AddressMulticast(aAddress) ? "M" : "U",
+                Ip6AddressString(&aAddress).AsCString(), otThreadErrorToString(error));
     }
 }
 
@@ -1246,8 +1313,9 @@
         case IFA_ANYCAST:
         case IFA_MULTICAST:
         {
-            ot::Ip6::Address addr;
-            memcpy(&addr, RTA_DATA(rta), sizeof(addr));
+            otIp6Address addr;
+
+            ReadIp6AddressFrom(RTA_DATA(rta), addr);
 
             memset(&addr6, 0, sizeof(addr6));
             addr6.sin6_family = AF_INET6;
@@ -1257,14 +1325,14 @@
             // which blocks openthread deriving an address by SLAAC and will cause routing issues.
             // Ignore the required anycast addresses here to allow OpenThread stack generate one when necessary,
             // and Linux will prefer the non-required anycast address on the interface.
-            if (isRequiredAnycast(addr.GetBytes(), ifaddr->ifa_prefixlen))
+            if (isRequiredAnycast(addr.mFields.m8, ifaddr->ifa_prefixlen))
             {
                 continue;
             }
 
             if (aNetlinkMessage->nlmsg_type == RTM_NEWADDR)
             {
-                if (!addr.IsMulticast())
+                if (!IsIp6AddressMulticast(addr))
                 {
                     otNetifAddress netAddr;
 
@@ -1283,6 +1351,7 @@
                 }
 
                 logAddrEvent(/* isAdd */ true, addr, error);
+
                 if (error == OT_ERROR_ALREADY || error == OT_ERROR_REJECTED)
                 {
                     error = OT_ERROR_NONE;
@@ -1292,7 +1361,7 @@
             }
             else if (aNetlinkMessage->nlmsg_type == RTM_DELADDR)
             {
-                if (!addr.IsMulticast())
+                if (!IsIp6AddressMulticast(addr))
                 {
                     error = otIp6RemoveUnicastAddress(aInstance, &addr);
                 }
@@ -1302,6 +1371,7 @@
                 }
 
                 logAddrEvent(/* isAdd */ false, addr, error);
+
                 if (error == OT_ERROR_NOT_FOUND || error == OT_ERROR_REJECTED)
                 {
                     error = OT_ERROR_NONE;
@@ -1317,7 +1387,7 @@
         }
 
         default:
-            otLogDebgPlat("[netif] Unexpected address type (%d).", (int)rta->rta_type);
+            LogDebg("Unexpected address type (%d).", (int)rta->rta_type);
             break;
         }
     }
@@ -1325,7 +1395,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogWarnPlat("[netif] Failed to process event, error:%s", otThreadErrorToString(error));
+        LogWarn("Failed to process event, error:%s", otThreadErrorToString(error));
     }
 }
 
@@ -1339,13 +1409,13 @@
 
     isUp = ((ifinfo->ifi_flags & IFF_UP) != 0);
 
-    otLogInfoPlat("[netif] Host netif is %s", isUp ? "up" : "down");
+    LogInfo("Host netif is %s", isUp ? "up" : "down");
 
 #if defined(RTM_NEWLINK) && defined(RTM_DELLINK)
     if (sIsSyncingState)
     {
         VerifyOrExit(isUp == otIp6IsEnabled(aInstance),
-                     otLogWarnPlat("[netif] Host netif state notification is unexpected (ignore)"));
+                     LogWarn("Host netif state notification is unexpected (ignore)"));
         sIsSyncingState = false;
     }
     else
@@ -1353,7 +1423,7 @@
         if (isUp != otIp6IsEnabled(aInstance))
     {
         SuccessOrExit(error = otIp6SetEnabled(aInstance, isUp));
-        otLogInfoPlat("[netif] Succeeded to sync netif state with host");
+        LogInfo("Succeeded to sync netif state with host");
     }
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
@@ -1362,7 +1432,7 @@
         // Recover NAT64 route.
         if ((error = AddIp4Route(sActiveNat64Cidr, kNat64RoutePriority)) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] failed to add route for NAT64: %s", otThreadErrorToString(error));
+            LogWarn("failed to add route for NAT64: %s", otThreadErrorToString(error));
         }
     }
 #endif
@@ -1370,7 +1440,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogWarnPlat("[netif] Failed to sync netif state with host: %s", otThreadErrorToString(error));
+        LogWarn("Failed to sync netif state with host: %s", otThreadErrorToString(error));
     }
 }
 #endif // __linux__
@@ -1455,7 +1525,10 @@
 
     if (addr6.sin6_family == AF_INET6)
     {
+        otIp6Address addr;
+
         is_link_local = false;
+
         if (IN6_IS_ADDR_LINKLOCAL(&addr6.sin6_addr))
         {
             is_link_local = true;
@@ -1467,8 +1540,7 @@
             addr6.sin6_addr.s6_addr[3] = 0;
         }
 
-        ot::Ip6::Address addr;
-        memcpy(&addr, &addr6.sin6_addr, sizeof(addr));
+        ReadIp6AddressFrom(&addr6.sin6_addr, addr);
 
         if (rtm->rtm_type == RTM_NEWADDR
 #ifdef RTM_NEWMADDR
@@ -1476,7 +1548,7 @@
 #endif
         )
         {
-            if (!addr.IsMulticast())
+            if (!IsIp6AddressMulticast(addr))
             {
                 otNetifAddress netAddr;
 
@@ -1519,16 +1591,14 @@
                         err = ioctl(sIpFd, SIOCDIFADDR_IN6, &ifr6);
                         if (err != 0)
                         {
-                            otLogWarnPlat(
-                                "[netif] Error (%d) removing stack-addded link-local address %s", errno,
-                                inet_ntop(AF_INET6, addr6.sin6_addr.s6_addr, addressString, sizeof(addressString)));
+                            LogWarn("Error (%d) removing stack-addded link-local address %s", errno,
+                                    inet_ntop(AF_INET6, addr6.sin6_addr.s6_addr, addressString, sizeof(addressString)));
                             error = OT_ERROR_FAILED;
                         }
                         else
                         {
-                            otLogNotePlat(
-                                "[netif]        %s (removed stack-added link-local)",
-                                inet_ntop(AF_INET6, addr6.sin6_addr.s6_addr, addressString, sizeof(addressString)));
+                            LogNote("       %s (removed stack-added link-local)",
+                                    inet_ntop(AF_INET6, addr6.sin6_addr.s6_addr, addressString, sizeof(addressString)));
                             error = OT_ERROR_NONE;
                         }
                     }
@@ -1536,6 +1606,7 @@
                     {
                         error = otIp6AddUnicastAddress(aInstance, &netAddr);
                         logAddrEvent(/* isAdd */ true, addr, error);
+
                         if (error == OT_ERROR_ALREADY)
                         {
                             error = OT_ERROR_NONE;
@@ -1551,6 +1622,7 @@
 
                 error = otIp6SubscribeMulticastAddress(aInstance, &addr);
                 logAddrEvent(/* isAdd */ true, addr, error);
+
                 if (error == OT_ERROR_ALREADY || error == OT_ERROR_REJECTED)
                 {
                     error = OT_ERROR_NONE;
@@ -1564,10 +1636,11 @@
 #endif
         )
         {
-            if (!addr.IsMulticast())
+            if (!IsIp6AddressMulticast(addr))
             {
                 error = otIp6RemoveUnicastAddress(aInstance, &addr);
                 logAddrEvent(/* isAdd */ false, addr, error);
+
                 if (error == OT_ERROR_NOT_FOUND)
                 {
                     error = OT_ERROR_NONE;
@@ -1577,11 +1650,13 @@
             {
                 error = otIp6UnsubscribeMulticastAddress(aInstance, &addr);
                 logAddrEvent(/* isAdd */ false, addr, error);
+
                 if (error == OT_ERROR_NOT_FOUND)
                 {
                     error = OT_ERROR_NONE;
                 }
             }
+
             SuccessOrExit(error);
         }
     }
@@ -1601,7 +1676,7 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogWarnPlat("[netif] Failed to process info event: %s", otThreadErrorToString(error));
+        LogWarn("Failed to process info event: %s", otThreadErrorToString(error));
     }
 }
 
@@ -1636,7 +1711,7 @@
 
     if (msg->nlmsg_len < NLMSG_LENGTH(sizeof(struct nlmsgerr)))
     {
-        otLogWarnPlat("[netif] Truncated netlink reply of request#%u", requestSeq);
+        LogWarn("Truncated netlink reply of request#%u", requestSeq);
         ExitNow();
     }
 
@@ -1645,7 +1720,7 @@
 
     if (err->error == 0)
     {
-        otLogInfoPlat("[netif] Succeeded to process request#%u", requestSeq);
+        LogInfo("Succeeded to process request#%u", requestSeq);
         ExitNow();
     }
 
@@ -1671,11 +1746,11 @@
         }
         else
         {
-            otLogDebgPlat("[netif] Ignoring netlink response attribute %d (request#%u)", rta->rta_type, requestSeq);
+            LogDebg("Ignoring netlink response attribute %d (request#%u)", rta->rta_type, requestSeq);
         }
     }
 
-    otLogWarnPlat("[netif] Failed to process request#%u: %s", requestSeq, errorMsg);
+    LogWarn("Failed to process request#%u: %s", requestSeq, errorMsg);
 
 exit:
     return;
@@ -1709,7 +1784,7 @@
     // Ensures full netlink header is received
     if (length < static_cast<ssize_t>(HEADER_SIZE))
     {
-        otLogWarnPlat("[netif] Unexpected netlink recv() result: %ld", static_cast<long>(length));
+        LogWarn("Unexpected netlink recv() result: %ld", static_cast<long>(length));
         ExitNow();
     }
 
@@ -1767,7 +1842,7 @@
 
 #if defined(ROUTE_FILTER) || defined(RO_MSGFILTER) || defined(__linux__)
         default:
-            otLogWarnPlat("[netif] Unhandled/Unexpected netlink/route message (%d).", (int)msg->nlmsg_type);
+            LogWarn("Unhandled/Unexpected netlink/route message (%d).", (int)msg->nlmsg_type);
             break;
 #else
             // this platform doesn't support filtering, so we expect messages of other types...we just ignore them
@@ -1848,11 +1923,13 @@
         {
             MLDv2Record *record = reinterpret_cast<MLDv2Record *>(&buffer[offset]);
 
-            otError          err;
-            ot::Ip6::Address address;
+            otError      err;
+            otIp6Address address;
 
-            memcpy(&address.mFields.m8, &record->mMulticastAddress, sizeof(address.mFields.m8));
+            ReadIp6AddressFrom(&record->mMulticastAddress, address);
+
             inet_ntop(AF_INET6, &record->mMulticastAddress, addressString, sizeof(addressString));
+
             if (record->mRecordType == kICMPv6MLDv2RecordChangeToIncludeType)
             {
                 err = otIp6SubscribeMulticastAddress(aInstance, &address);
@@ -1912,11 +1989,11 @@
 
     if (send(sNetlinkFd, &req, req.nh.nlmsg_len, 0) != -1)
     {
-        otLogInfoPlat("[netif] Sent request#%u to set addr_gen_mode to %d", sNetlinkSequence, mode);
+        LogInfo("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);
+        LogWarn("Failed to send request#%u to set addr_gen_mode to %d", sNetlinkSequence, mode);
     }
 }
 
@@ -1997,7 +2074,7 @@
     err        = getsockopt(sTunFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, gNetifName, &devNameLen);
     VerifyOrDie(err == 0, OT_EXIT_ERROR_ERRNO);
 
-    otLogInfoPlat("[netif] Tunnel device name = '%s'", gNetifName);
+    LogInfo("Tunnel device name = '%s'", gNetifName);
 }
 #endif
 
@@ -2069,13 +2146,13 @@
 #if defined(NETLINK_EXT_ACK)
         if (setsockopt(sNetlinkFd, SOL_NETLINK, NETLINK_EXT_ACK, &enable, sizeof(enable)) != 0)
         {
-            otLogWarnPlat("[netif] Failed to enable NETLINK_EXT_ACK: %s", strerror(errno));
+            LogWarn("Failed to enable NETLINK_EXT_ACK: %s", strerror(errno));
         }
 #endif
 #if defined(NETLINK_CAP_ACK)
         if (setsockopt(sNetlinkFd, SOL_NETLINK, NETLINK_CAP_ACK, &enable, sizeof(enable)) != 0)
         {
-            otLogWarnPlat("[netif] Failed to enable NETLINK_CAP_ACK: %s", strerror(errno));
+            LogWarn("Failed to enable NETLINK_CAP_ACK: %s", strerror(errno));
         }
 #endif
     }
@@ -2120,6 +2197,13 @@
 
 void platformNetifInit(otPlatformConfig *aPlatformConfig)
 {
+    // To silence "unused function" warning.
+    (void)LogCrit;
+    (void)LogWarn;
+    (void)LogInfo;
+    (void)LogNote;
+    (void)LogDebg;
+
     sIpFd = SocketWithCloseExec(AF_INET6, SOCK_DGRAM, IPPROTO_IP, kSocketNonBlock);
     VerifyOrDie(sIpFd >= 0, OT_EXIT_ERROR_ERRNO);
 
@@ -2148,19 +2232,19 @@
     {
         if ((error = otNat64SetIp4Cidr(gInstance, &cidr)) != OT_ERROR_NONE)
         {
-            otLogWarnPlat("[netif] failed to set CIDR for NAT64: %s", otThreadErrorToString(error));
+            LogWarn("failed to set CIDR for NAT64: %s", otThreadErrorToString(error));
         }
     }
     else
     {
-        otLogInfoPlat("[netif] No default NAT64 CIDR provided.");
+        LogInfo("No default NAT64 CIDR provided.");
     }
 }
 #endif
 
 void platformNetifSetUp(void)
 {
-    OT_ASSERT(gInstance != nullptr);
+    assert(gInstance != nullptr);
 
     otIp6SetReceiveFilterEnabled(gInstance, true);
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
diff --git a/src/posix/platform/openthread-posix-config.h b/src/posix/platform/openthread-posix-config.h
index f39c6f2..ade8ab2 100644
--- a/src/posix/platform/openthread-posix-config.h
+++ b/src/posix/platform/openthread-posix-config.h
@@ -26,8 +26,8 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef OPENTHREAD_PLATFORM_CONFIG_H_
-#define OPENTHREAD_PLATFORM_CONFIG_H_
+#ifndef OPENTHREAD_PLATFORM_POSIX_CONFIG_H_
+#define OPENTHREAD_PLATFORM_POSIX_CONFIG_H_
 
 #include "openthread-core-config.h"
 
@@ -429,4 +429,4 @@
 #define OPENTHREAD_POSIX_CONFIG_TREL_TX_PACKET_POOL_SIZE 5
 #endif
 
-#endif // OPENTHREAD_PLATFORM_CONFIG_H_
+#endif // OPENTHREAD_PLATFORM_POSIX_CONFIG_H_
diff --git a/src/posix/platform/platform-posix.h b/src/posix/platform/platform-posix.h
index 912ceb5..a5d3a30 100644
--- a/src/posix/platform/platform-posix.h
+++ b/src/posix/platform/platform-posix.h
@@ -32,8 +32,8 @@
  *   This file includes the platform-specific initializers.
  */
 
-#ifndef PLATFORM_POSIX_H_
-#define PLATFORM_POSIX_H_
+#ifndef OT_PLATFORM_POSIX_H_
+#define OT_PLATFORM_POSIX_H_
 
 #include "openthread-posix-config.h"
 
@@ -424,4 +424,4 @@
 #ifdef __cplusplus
 }
 #endif
-#endif // PLATFORM_POSIX_H_
+#endif // OT_PLATFORM_POSIX_H_
diff --git a/src/posix/platform/power.hpp b/src/posix/platform/power.hpp
index 4edbcb2..f4481f1 100644
--- a/src/posix/platform/power.hpp
+++ b/src/posix/platform/power.hpp
@@ -26,8 +26,8 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef POSIX_PLATFORM_POWER_H
-#define POSIX_PLATFORM_POWER_H
+#ifndef OT_POSIX_PLATFORM_POWER_HPP_
+#define OT_POSIX_PLATFORM_POWER_HPP_
 
 #include <assert.h>
 #include <stdio.h>
@@ -286,4 +286,4 @@
 };
 } // namespace Power
 } // namespace ot
-#endif // POSIX_PLATFORM_POWER_H
+#endif // OT_POSIX_PLATFORM_POWER_HPP_
diff --git a/src/posix/platform/radio.cpp b/src/posix/platform/radio.cpp
index 8828f9b..f5eb17c 100644
--- a/src/posix/platform/radio.cpp
+++ b/src/posix/platform/radio.cpp
@@ -57,6 +57,8 @@
 extern "C" void platformRadioInit(const char *aUrl) { sRadio.Init(aUrl); }
 } // namespace
 
+const char Radio::kLogModuleName[] = "Radio";
+
 Radio::Radio(void)
     : mRadioUrl(nullptr)
     , mRadioSpinel()
@@ -98,7 +100,7 @@
 
     mRadioSpinel.SetCallbacks(callbacks);
     mRadioSpinel.Init(*mSpinelInterface, resetRadio, skipCompatibilityCheck, iidList, OT_ARRAY_LENGTH(iidList));
-    otLogDebgPlat("instance init:%p - iid = %d", (void *)&mRadioSpinel, iidList[0]);
+    LogDebg("instance init:%p - iid = %d", (void *)&mRadioSpinel, iidList[0]);
 
     ProcessRadioUrl(mRadioUrl);
 }
@@ -145,7 +147,7 @@
 #endif
     else
     {
-        otLogCritPlat("The Spinel interface name \"%s\" is not supported!", aInterfaceName);
+        LogCrit("The Spinel interface name \"%s\" is not supported!", aInterfaceName);
         DieNow(OT_ERROR_FAILED);
     }
 
@@ -159,7 +161,7 @@
 
     if (aRadioUrl.HasParam("ncp-dataset"))
     {
-        otLogCritPlat("The argument \"ncp-dataset\" is no longer supported");
+        LogCrit("The argument \"ncp-dataset\" is no longer supported");
         DieNow(OT_ERROR_FAILED);
     }
 
@@ -220,7 +222,7 @@
         VerifyOrDie((error == OT_ERROR_NONE) || (error == OT_ERROR_NOT_IMPLEMENTED), OT_EXIT_FAILURE);
         if (error == OT_ERROR_NOT_IMPLEMENTED)
         {
-            otLogWarnPlat("The RCP doesn't support setting the max transmit power");
+            LogWarn("The RCP doesn't support setting the max transmit power");
         }
 
         ++channel;
@@ -233,7 +235,7 @@
         VerifyOrDie((error == OT_ERROR_NONE) || (error == OT_ERROR_NOT_IMPLEMENTED), OT_ERROR_FAILED);
         if (error == OT_ERROR_NOT_IMPLEMENTED)
         {
-            otLogWarnPlat("The RCP doesn't support setting the max transmit power");
+            LogWarn("The RCP doesn't support setting the max transmit power");
         }
 
         ++channel;
diff --git a/src/posix/platform/radio.hpp b/src/posix/platform/radio.hpp
index 49cdfdf..e4a8b1a 100644
--- a/src/posix/platform/radio.hpp
+++ b/src/posix/platform/radio.hpp
@@ -26,15 +26,16 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef POSIX_PLATFORM_RADIO_HPP_
-#define POSIX_PLATFORM_RADIO_HPP_
+#ifndef OT_POSIX_PLATFORM_RADIO_HPP_
+#define OT_POSIX_PLATFORM_RADIO_HPP_
 
+#include "hdlc_interface.hpp"
+#include "logger.hpp"
+#include "radio_url.hpp"
+#include "spi_interface.hpp"
+#include "vendor_interface.hpp"
 #include "common/code_utils.hpp"
 #include "lib/spinel/radio_spinel.hpp"
-#include "posix/platform/hdlc_interface.hpp"
-#include "posix/platform/radio_url.hpp"
-#include "posix/platform/spi_interface.hpp"
-#include "posix/platform/vendor_interface.hpp"
 #if OPENTHREAD_SPINEL_CONFIG_VENDOR_HOOK_ENABLE
 #ifdef OPENTHREAD_SPINEL_CONFIG_VENDOR_HOOK_HEADER
 #include OPENTHREAD_SPINEL_CONFIG_VENDOR_HOOK_HEADER
@@ -48,9 +49,11 @@
  * Manages Thread radio.
  *
  */
-class Radio
+class Radio : public Logger<Radio>
 {
 public:
+    static const char kLogModuleName[]; ///< Module name used for logging.
+
     /**
      * Creates the radio manager.
      *
@@ -123,4 +126,4 @@
 } // namespace Posix
 } // namespace ot
 
-#endif // POSIX_PLATFORM_RADIO_HPP_
+#endif // OT_POSIX_PLATFORM_RADIO_HPP_
diff --git a/src/posix/platform/radio_url.hpp b/src/posix/platform/radio_url.hpp
index 0246070..0e088cb 100644
--- a/src/posix/platform/radio_url.hpp
+++ b/src/posix/platform/radio_url.hpp
@@ -26,8 +26,8 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef POSIX_PLATFORM_RADIO_URL_HPP_
-#define POSIX_PLATFORM_RADIO_URL_HPP_
+#ifndef OT_POSIX_PLATFORM_RADIO_URL_HPP_
+#define OT_POSIX_PLATFORM_RADIO_URL_HPP_
 
 #include <stdio.h>
 #include <stdlib.h>
@@ -78,4 +78,4 @@
 } // namespace Posix
 } // namespace ot
 
-#endif // POSIX_PLATFORM_RADIO_URL_HPP_
+#endif // OT_POSIX_PLATFORM_RADIO_URL_HPP_
diff --git a/src/posix/platform/resolver.cpp b/src/posix/platform/resolver.cpp
index c8d80e2..642d771 100644
--- a/src/posix/platform/resolver.cpp
+++ b/src/posix/platform/resolver.cpp
@@ -61,6 +61,8 @@
 namespace ot {
 namespace Posix {
 
+const char Resolver::kLogModuleName[] = "Resolver";
+
 void Resolver::Init(void)
 {
     memset(mUpstreamTransaction, 0, sizeof(mUpstreamTransaction));
@@ -95,8 +97,7 @@
 
             if (inet_pton(AF_INET, &line.c_str()[sizeof(kNameserverItem)], &addr) == 1)
             {
-                otLogInfoPlat("Got nameserver #%d: %s", mUpstreamDnsServerCount,
-                              &line.c_str()[sizeof(kNameserverItem)]);
+                LogInfo("Got nameserver #%d: %s", mUpstreamDnsServerCount, &line.c_str()[sizeof(kNameserverItem)]);
                 mUpstreamDnsServerList[mUpstreamDnsServerCount] = addr;
                 mUpstreamDnsServerCount++;
             }
@@ -105,7 +106,7 @@
 
     if (mUpstreamDnsServerCount == 0)
     {
-        otLogCritPlat("No domain name servers found in %s, default to 127.0.0.1", kResolvConfFullPath);
+        LogCrit("No domain name servers found in %s, default to 127.0.0.1", kResolvConfFullPath);
     }
 
     mUpstreamDnsServerListFreshness = otPlatTimeGet();
@@ -137,12 +138,12 @@
             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);
+    LogInfo("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);
+        LogCrit("Failed to forward DNS query %p to server: %d", static_cast<void *>(aTxn), error);
     }
     return;
 }
@@ -171,7 +172,7 @@
             fdOrError = socket(AF_INET, SOCK_DGRAM, 0);
             if (fdOrError < 0)
             {
-                otLogInfoPlat("Failed to create socket for upstream resolver: %d", fdOrError);
+                LogInfo("Failed to create socket for upstream resolver: %d", fdOrError);
                 break;
             }
             ret             = &txn;
@@ -203,11 +204,11 @@
 exit:
     if (readSize < 0)
     {
-        otLogInfoPlat("Failed to read response from upstream resolver socket: %d", errno);
+        LogInfo("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));
+        LogInfo("Failed to forward upstream DNS response: %s", otThreadErrorToString(error));
     }
     if (message != nullptr)
     {
diff --git a/src/posix/platform/resolver.hpp b/src/posix/platform/resolver.hpp
index 8446a47..fae0cf9 100644
--- a/src/posix/platform/resolver.hpp
+++ b/src/posix/platform/resolver.hpp
@@ -26,8 +26,8 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef POSIX_PLATFORM_RESOLVER_HPP_
-#define POSIX_PLATFORM_RESOLVER_HPP_
+#ifndef OT_POSIX_PLATFORM_RESOLVER_HPP_
+#define OT_POSIX_PLATFORM_RESOLVER_HPP_
 
 #include <openthread/openthread-system.h>
 #include <openthread/platform/dns.h>
@@ -35,14 +35,18 @@
 #include <arpa/inet.h>
 #include <sys/select.h>
 
+#include "logger.hpp"
+
 #if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
 
 namespace ot {
 namespace Posix {
 
-class Resolver
+class Resolver : public Logger<Resolver>
 {
 public:
+    static const char kLogModuleName[]; ///< Module name used for logging.
+
     constexpr static ssize_t kMaxDnsMessageSize           = 512;
     constexpr static ssize_t kMaxUpstreamTransactionCount = 16;
     constexpr static ssize_t kMaxUpstreamServerCount      = 3;
@@ -73,10 +77,7 @@
     /**
      * 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.
+     * @param[in,out]  aContext  The mainloop context.
      *
      */
     void UpdateFdSet(otSysMainloopContext &aContext);
@@ -84,8 +85,7 @@
     /**
      * 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.
+     * @param[in]  aContext  The mainloop context.
      *
      */
     void Process(const otSysMainloopContext &aContext);
@@ -122,4 +122,4 @@
 
 #endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
 
-#endif // POSIX_PLATFORM_RESOLVER_HPP_
+#endif // OT_POSIX_PLATFORM_RESOLVER_HPP_
diff --git a/src/posix/platform/settings.hpp b/src/posix/platform/settings.hpp
index d2009aa..bf2cabd 100644
--- a/src/posix/platform/settings.hpp
+++ b/src/posix/platform/settings.hpp
@@ -26,8 +26,8 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef POSIX_PLATFORM_SETTINGS_HPP_
-#define POSIX_PLATFORM_SETTINGS_HPP_
+#ifndef OT_POSIX_PLATFORM_SETTINGS_HPP_
+#define OT_POSIX_PLATFORM_SETTINGS_HPP_
 
 namespace ot {
 namespace Posix {
@@ -100,4 +100,4 @@
 } // namespace Posix
 } // namespace ot
 
-#endif // POSIX_PLATFORM_SETTINGS_HPP_
+#endif // OT_POSIX_PLATFORM_SETTINGS_HPP_
diff --git a/src/posix/platform/spi_interface.cpp b/src/posix/platform/spi_interface.cpp
index 167f4b9..9d7860b 100644
--- a/src/posix/platform/spi_interface.cpp
+++ b/src/posix/platform/spi_interface.cpp
@@ -62,6 +62,8 @@
 namespace ot {
 namespace Posix {
 
+const char SpiInterface::kLogModuleName[] = "SpiIntface";
+
 SpiInterface::SpiInterface(const Url::Url &aRadioUrl)
     : mReceiveFrameCallback(nullptr)
     , mReceiveFrameContext(nullptr)
@@ -153,7 +155,7 @@
     }
     else
     {
-        otLogNotePlat("SPI interface enters polling mode.");
+        LogNote("SPI interface enters polling mode.");
     }
 
     InitResetPin(spiGpioResetDevice, spiGpioResetLine);
@@ -254,7 +256,7 @@
     char label[] = "SOC_THREAD_RESET";
     int  fd;
 
-    otLogDebgPlat("InitResetPin: charDev=%s, line=%" PRIu8, aCharDev, aLine);
+    LogDebg("InitResetPin: charDev=%s, line=%" PRIu8, aCharDev, aLine);
 
     VerifyOrDie(aCharDev != nullptr, OT_EXIT_INVALID_ARGUMENTS);
     VerifyOrDie((fd = open(aCharDev, O_RDWR)) != -1, OT_EXIT_ERROR_ERRNO);
@@ -268,7 +270,7 @@
     char label[] = "THREAD_SOC_INT";
     int  fd;
 
-    otLogDebgPlat("InitIntPin: charDev=%s, line=%" PRIu8, aCharDev, aLine);
+    LogDebg("InitIntPin: charDev=%s, line=%" PRIu8, aCharDev, aLine);
 
     VerifyOrDie(aCharDev != nullptr, OT_EXIT_INVALID_ARGUMENTS);
     VerifyOrDie((fd = open(aCharDev, O_RDWR)) != -1, OT_EXIT_ERROR_ERRNO);
@@ -283,7 +285,7 @@
     const uint8_t wordBits = kSpiBitsPerWord;
     int           fd;
 
-    otLogDebgPlat("InitSpiDev: path=%s, mode=%" PRIu8 ", speed=%" PRIu32, aPath, aMode, aSpeed);
+    LogDebg("InitSpiDev: path=%s, mode=%" PRIu8 ", speed=%" PRIu32, aPath, aMode, aSpeed);
 
     VerifyOrDie((aPath != nullptr) && (aMode <= kSpiModeMax), OT_EXIT_INVALID_ARGUMENTS);
     VerifyOrDie((fd = open(aPath, O_RDWR | O_CLOEXEC)) != -1, OT_EXIT_ERROR_ERRNO);
@@ -314,7 +316,7 @@
     // Set Reset pin to high level.
     SetGpioValue(mResetGpioValueFd, 1);
 
-    otLogNotePlat("Triggered hardware reset");
+    LogNote("Triggered hardware reset");
 }
 
 uint8_t *SpiInterface::GetRealRxFrameStart(uint8_t *aSpiRxFrameBuffer, uint8_t aAlignAllowance, uint16_t &aSkipLength)
@@ -453,12 +455,12 @@
 
     if (error != OT_ERROR_NONE)
     {
-        otLogCritPlat("PushPullSpi:DoSpiTransfer: errno=%s", strerror(errno));
+        LogCrit("PushPullSpi:DoSpiTransfer: errno=%s", strerror(errno));
 
         // Print out a helpful error message for a common error.
         if ((mSpiCsDelayUs != 0) && (errno == EINVAL))
         {
-            otLogWarnPlat("SPI ioctl failed with EINVAL. Try adding `--spi-cs-delay=0` to command line arguments.");
+            LogWarn("SPI ioctl failed with EINVAL. Try adding `--spi-cs-delay=0` to command line arguments.");
         }
 
         LogStats();
@@ -471,10 +473,10 @@
     {
         Spinel::SpiFrame rxFrame(spiRxFrame);
 
-        otLogDebgPlat("spi_transfer TX: H:%02X ACCEPT:%" PRIu16 " DATA:%" PRIu16, txFrame.GetHeaderFlagByte(),
-                      txFrame.GetHeaderAcceptLen(), txFrame.GetHeaderDataLen());
-        otLogDebgPlat("spi_transfer RX: H:%02X ACCEPT:%" PRIu16 " DATA:%" PRIu16, rxFrame.GetHeaderFlagByte(),
-                      rxFrame.GetHeaderAcceptLen(), rxFrame.GetHeaderDataLen());
+        LogDebg("spi_transfer TX: H:%02X ACCEPT:%" PRIu16 " DATA:%" PRIu16, txFrame.GetHeaderFlagByte(),
+                txFrame.GetHeaderAcceptLen(), txFrame.GetHeaderDataLen());
+        LogDebg("spi_transfer RX: H:%02X ACCEPT:%" PRIu16 " DATA:%" PRIu16, rxFrame.GetHeaderFlagByte(),
+                rxFrame.GetHeaderAcceptLen(), rxFrame.GetHeaderDataLen());
 
         slaveHeader = rxFrame.GetHeaderFlagByte();
         if ((slaveHeader == 0xFF) || (slaveHeader == 0x00))
@@ -485,11 +487,11 @@
                 // Device is off or in a bad state. In some cases may be induced by flow control.
                 if (mSpiSlaveDataLen == 0)
                 {
-                    otLogDebgPlat("Slave did not respond to frame. (Header was all 0x%02X)", slaveHeader);
+                    LogDebg("Slave did not respond to frame. (Header was all 0x%02X)", slaveHeader);
                 }
                 else
                 {
-                    otLogWarnPlat("Slave did not respond to frame. (Header was all 0x%02X)", slaveHeader);
+                    LogWarn("Slave did not respond to frame. (Header was all 0x%02X)", slaveHeader);
                 }
 
                 mSpiUnresponsiveFrameCount++;
@@ -499,8 +501,8 @@
                 // Header is full of garbage
                 mInterfaceMetrics.mTransferredGarbageFrameCount++;
 
-                otLogWarnPlat("Garbage in header : %02X %02X %02X %02X %02X", spiRxFrame[0], spiRxFrame[1],
-                              spiRxFrame[2], spiRxFrame[3], spiRxFrame[4]);
+                LogWarn("Garbage in header : %02X %02X %02X %02X %02X", spiRxFrame[0], spiRxFrame[1], spiRxFrame[2],
+                        spiRxFrame[3], spiRxFrame[4]);
                 otDumpDebgPlat("SPI-TX", mSpiTxFrameBuffer, spiTransferBytes);
                 otDumpDebgPlat("SPI-RX", spiRxFrameBuffer, spiTransferBytes);
             }
@@ -518,8 +520,8 @@
             mSpiTxRefusedCount++;
             mSpiSlaveDataLen = 0;
 
-            otLogWarnPlat("Garbage in header : %02X %02X %02X %02X %02X", spiRxFrame[0], spiRxFrame[1], spiRxFrame[2],
-                          spiRxFrame[3], spiRxFrame[4]);
+            LogWarn("Garbage in header : %02X %02X %02X %02X %02X", spiRxFrame[0], spiRxFrame[1], spiRxFrame[2],
+                    spiRxFrame[3], spiRxFrame[4]);
             otDumpDebgPlat("SPI-TX", mSpiTxFrameBuffer, spiTransferBytes);
             otDumpDebgPlat("SPI-RX", spiRxFrameBuffer, spiTransferBytes);
 
@@ -532,7 +534,7 @@
         {
             mSlaveResetCount++;
 
-            otLogNotePlat("Slave did reset (%" PRIu64 " resets so far)", mSlaveResetCount);
+            LogNote("Slave did reset (%" PRIu64 " resets so far)", mSlaveResetCount);
             LogStats();
         }
 
@@ -634,7 +636,7 @@
             // Interrupt pin is asserted, set the timeout to be 0.
             timeout.tv_sec  = 0;
             timeout.tv_usec = 0;
-            otLogDebgPlat("UpdateFdSet(): Interrupt.");
+            LogDebg("UpdateFdSet(): Interrupt.");
         }
         else
         {
@@ -677,7 +679,7 @@
         {
             // To avoid printing out this message over and over, we only print it out once the refused count is at two
             // or higher when we actually have something to send the slave. And then, we only print it once.
-            otLogInfoPlat("Slave is rate limiting transactions");
+            LogInfo("Slave is rate limiting transactions");
 
             mDidPrintRateLimitLog = true;
         }
@@ -686,7 +688,7 @@
         {
             // Ua-oh. The slave hasn't given us a chance to send it anything for over thirty frames. If this ever
             // happens, print out a warning to the logs.
-            otLogWarnPlat("Slave seems stuck.");
+            LogWarn("Slave seems stuck.");
         }
         else if (mSpiTxRefusedCount == kSpiTxRefuseExitCount)
         {
@@ -716,7 +718,7 @@
     {
         struct gpioevent_data event;
 
-        otLogDebgPlat("Process(): Interrupt.");
+        LogDebg("Process(): Interrupt.");
 
         // Read event data to clear interrupt.
         VerifyOrDie(read(mIntGpioValueFd, &event, sizeof(event)) != -1, OT_EXIT_ERROR_ERRNO);
@@ -804,21 +806,21 @@
 void SpiInterface::LogError(const char *aString)
 {
     OT_UNUSED_VARIABLE(aString);
-    otLogWarnPlat("%s: %s", aString, strerror(errno));
+    LogWarn("%s: %s", aString, strerror(errno));
 }
 
 void SpiInterface::LogStats(void)
 {
-    otLogInfoPlat("INFO: SlaveResetCount=%" PRIu64, mSlaveResetCount);
-    otLogInfoPlat("INFO: SpiDuplexFrameCount=%" PRIu64, mSpiDuplexFrameCount);
-    otLogInfoPlat("INFO: SpiUnresponsiveFrameCount=%" PRIu64, mSpiUnresponsiveFrameCount);
-    otLogInfoPlat("INFO: TransferredFrameCount=%" PRIu64, mInterfaceMetrics.mTransferredFrameCount);
-    otLogInfoPlat("INFO: TransferredValidFrameCount=%" PRIu64, mInterfaceMetrics.mTransferredValidFrameCount);
-    otLogInfoPlat("INFO: TransferredGarbageFrameCount=%" PRIu64, mInterfaceMetrics.mTransferredGarbageFrameCount);
-    otLogInfoPlat("INFO: RxFrameCount=%" PRIu64, mInterfaceMetrics.mRxFrameCount);
-    otLogInfoPlat("INFO: RxFrameByteCount=%" PRIu64, mInterfaceMetrics.mRxFrameByteCount);
-    otLogInfoPlat("INFO: TxFrameCount=%" PRIu64, mInterfaceMetrics.mTxFrameCount);
-    otLogInfoPlat("INFO: TxFrameByteCount=%" PRIu64, mInterfaceMetrics.mTxFrameByteCount);
+    LogInfo("INFO: SlaveResetCount=%" PRIu64, mSlaveResetCount);
+    LogInfo("INFO: SpiDuplexFrameCount=%" PRIu64, mSpiDuplexFrameCount);
+    LogInfo("INFO: SpiUnresponsiveFrameCount=%" PRIu64, mSpiUnresponsiveFrameCount);
+    LogInfo("INFO: TransferredFrameCount=%" PRIu64, mInterfaceMetrics.mTransferredFrameCount);
+    LogInfo("INFO: TransferredValidFrameCount=%" PRIu64, mInterfaceMetrics.mTransferredValidFrameCount);
+    LogInfo("INFO: TransferredGarbageFrameCount=%" PRIu64, mInterfaceMetrics.mTransferredGarbageFrameCount);
+    LogInfo("INFO: RxFrameCount=%" PRIu64, mInterfaceMetrics.mRxFrameCount);
+    LogInfo("INFO: RxFrameByteCount=%" PRIu64, mInterfaceMetrics.mRxFrameByteCount);
+    LogInfo("INFO: TxFrameCount=%" PRIu64, mInterfaceMetrics.mTxFrameCount);
+    LogInfo("INFO: TxFrameByteCount=%" PRIu64, mInterfaceMetrics.mTxFrameByteCount);
 }
 } // namespace Posix
 } // namespace ot
diff --git a/src/posix/platform/spi_interface.hpp b/src/posix/platform/spi_interface.hpp
index 095214a..94b5507 100644
--- a/src/posix/platform/spi_interface.hpp
+++ b/src/posix/platform/spi_interface.hpp
@@ -31,11 +31,12 @@
  *   This file includes definitions for the SPI interface to radio (RCP).
  */
 
-#ifndef POSIX_PLATFORM_SPI_INTERFACE_HPP_
-#define POSIX_PLATFORM_SPI_INTERFACE_HPP_
+#ifndef OT_POSIX_PLATFORM_SPI_INTERFACE_HPP_
+#define OT_POSIX_PLATFORM_SPI_INTERFACE_HPP_
 
 #include "openthread-posix-config.h"
 
+#include "logger.hpp"
 #include "platform-posix.h"
 #include "lib/hdlc/hdlc.hpp"
 #include "lib/spinel/multi_frame_buffer.hpp"
@@ -51,9 +52,11 @@
  * Defines an SPI interface to the Radio Co-processor (RCP).
  *
  */
-class SpiInterface : public ot::Spinel::SpinelInterface
+class SpiInterface : public ot::Spinel::SpinelInterface, public Logger<SpiInterface>
 {
 public:
+    static const char kLogModuleName[]; ///< Module name used for logging.
+
     /**
      * Initializes the object.
      *
@@ -258,4 +261,4 @@
 } // namespace Posix
 } // namespace ot
 
-#endif // POSIX_PLATFORM_SPI_INTERFACE_HPP_
+#endif // OT_POSIX_PLATFORM_SPI_INTERFACE_HPP_
diff --git a/src/posix/platform/system.cpp b/src/posix/platform/system.cpp
index db10899..c209d63 100644
--- a/src/posix/platform/system.cpp
+++ b/src/posix/platform/system.cpp
@@ -40,6 +40,7 @@
 
 #include <openthread-core-config.h>
 #include <openthread/border_router.h>
+#include <openthread/cli.h>
 #include <openthread/heap.h>
 #include <openthread/tasklet.h>
 #include <openthread/platform/alarm-milli.h>
@@ -53,6 +54,7 @@
 #include "posix/platform/firewall.hpp"
 #include "posix/platform/infra_if.hpp"
 #include "posix/platform/mainloop.hpp"
+#include "posix/platform/mdns_socket.hpp"
 #include "posix/platform/radio_url.hpp"
 #include "posix/platform/udp.hpp"
 
@@ -144,7 +146,10 @@
 
 #if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
     ot::Posix::InfraNetif::Get().Init();
+#endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    ot::Posix::MdnsSocket::Get().Init();
 #endif
 
     gNetifName[0] = '\0';
@@ -196,6 +201,10 @@
     ot::Posix::Udp::Get().SetUp();
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    ot::Posix::MdnsSocket::Get().SetUp();
+#endif
+
 #if OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE
     ot::Posix::Daemon::Get().SetUp();
 #endif
@@ -243,6 +252,10 @@
     ot::Posix::InfraNetif::Get().TearDown();
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    ot::Posix::MdnsSocket::Get().TearDown();
+#endif
+
 exit:
     return;
 }
@@ -271,6 +284,10 @@
     ot::Posix::InfraNetif::Get().Deinit();
 #endif
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    ot::Posix::MdnsSocket::Get().Deinit();
+#endif
+
 exit:
     return;
 }
@@ -401,3 +418,15 @@
 }
 
 bool IsSystemDryRun(void) { return gDryRun; }
+
+#if OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE && OPENTHREAD_POSIX_CONFIG_DAEMON_CLI_ENABLE
+void otSysCliInitUsingDaemon(otInstance *aInstance)
+{
+    otCliInit(
+        aInstance,
+        [](void *aContext, const char *aFormat, va_list aArguments) -> int {
+            return static_cast<ot::Posix::Daemon *>(aContext)->OutputFormatV(aFormat, aArguments);
+        },
+        &ot::Posix::Daemon::Get());
+}
+#endif
diff --git a/src/posix/platform/trel.cpp b/src/posix/platform/trel.cpp
index ea06bca..6b6e6c0 100644
--- a/src/posix/platform/trel.cpp
+++ b/src/posix/platform/trel.cpp
@@ -45,6 +45,7 @@
 #include <openthread/logging.h>
 #include <openthread/platform/trel.h>
 
+#include "logger.hpp"
 #include "radio_url.hpp"
 #include "system.hpp"
 #include "common/code_utils.hpp"
@@ -73,6 +74,53 @@
 static bool sEnabled     = false;
 static int  sSocket      = -1;
 
+static const char kLogModuleName[] = "Trel";
+
+static void LogCrit(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_CRIT, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogWarn(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_WARN, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogNote(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_NOTE, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogInfo(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_INFO, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
+static void LogDebg(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    otLogPlatArgs(OT_LOG_LEVEL_DEBG, kLogModuleName, aFormat, args);
+    va_end(args);
+}
+
 static const char *Ip6AddrToString(const void *aAddress)
 {
     static char string[INET6_ADDRSTRLEN];
@@ -121,7 +169,7 @@
     struct sockaddr_in6 sockAddr;
     socklen_t           sockLen;
 
-    otLogDebgPlat("[trel] PrepareSocket()");
+    LogDebg("PrepareSocket()");
 
     sSocket = SocketWithCloseExec(AF_INET6, SOCK_DGRAM, 0, kSocketNonBlock);
     VerifyOrDie(sSocket >= 0, OT_EXIT_ERROR_ERRNO);
@@ -141,7 +189,7 @@
 
     if (bind(sSocket, (struct sockaddr *)&sockAddr, sizeof(sockAddr)) == -1)
     {
-        otLogCritPlat("[trel] Failed to bind socket");
+        LogCrit("Failed to bind socket");
         DieNow(OT_EXIT_ERROR_ERRNO);
     }
 
@@ -149,7 +197,7 @@
 
     if (getsockname(sSocket, (struct sockaddr *)&sockAddr, &sockLen) == -1)
     {
-        otLogCritPlat("[trel] Failed to get the socket name");
+        LogCrit("Failed to get the socket name");
         DieNow(OT_EXIT_ERROR_ERRNO);
     }
 
@@ -173,7 +221,7 @@
 
     if (ret != aLength)
     {
-        otLogDebgPlat("[trel] SendPacket() -- sendto() failed errno %d", errno);
+        LogDebg("SendPacket() -- sendto() failed errno %d", errno);
 
         switch (errno)
         {
@@ -194,8 +242,8 @@
     }
 
 exit:
-    otLogDebgPlat("[trel] SendPacket([%s]:%u) err:%s pkt:%s", Ip6AddrToString(&aDestSockAddr->mAddress),
-                  aDestSockAddr->mPort, otThreadErrorToString(error), BufferToString(aBuffer, aLength));
+    LogDebg("SendPacket([%s]:%u) err:%s pkt:%s", Ip6AddrToString(&aDestSockAddr->mAddress), aDestSockAddr->mPort,
+            otThreadErrorToString(error), BufferToString(aBuffer, aLength));
     if (error != OT_ERROR_NONE)
     {
         ++sCounters.mTxFailure;
@@ -222,8 +270,8 @@
         sRxPacketLength = sizeof(sRxPacketLength);
     }
 
-    otLogDebgPlat("[trel] ReceivePacket() - received from [%s]:%d, id:%d, pkt:%s", Ip6AddrToString(&sockAddr.sin6_addr),
-                  ntohs(sockAddr.sin6_port), sockAddr.sin6_scope_id, BufferToString(sRxPacketBuffer, sRxPacketLength));
+    LogDebg("ReceivePacket() - received from [%s]:%d, id:%d, pkt:%s", Ip6AddrToString(&sockAddr.sin6_addr),
+            ntohs(sockAddr.sin6_port), sockAddr.sin6_scope_id, BufferToString(sRxPacketBuffer, sRxPacketLength));
 
     if (sEnabled)
     {
@@ -257,7 +305,7 @@
 
         if (SendPacket(packet->mBuffer, packet->mLength, &packet->mDestSockAddr) == OT_ERROR_INVALID_STATE)
         {
-            otLogDebgPlat("[trel] SendQueuedPackets() - SendPacket() would block");
+            LogDebg("SendQueuedPackets() - SendPacket() would block");
             break;
         }
 
@@ -287,7 +335,7 @@
     // Allocate an available packet entry (from the free packet list)
     // and copy the packet content into it.
 
-    VerifyOrExit(sFreeTxPacketHead != NULL, otLogWarnPlat("[trel] EnqueuePacket failed, queue is full"));
+    VerifyOrExit(sFreeTxPacketHead != NULL, LogWarn("EnqueuePacket failed, queue is full"));
     packet            = sFreeTxPacketHead;
     sFreeTxPacketHead = sFreeTxPacketHead->mNext;
 
@@ -309,8 +357,8 @@
         sTxPacketQueueTail        = packet;
     }
 
-    otLogDebgPlat("[trel] EnqueuePacket([%s]:%u) - %s", Ip6AddrToString(&aDestSockAddr->mAddress), aDestSockAddr->mPort,
-                  BufferToString(aBuffer, aLength));
+    LogDebg("EnqueuePacket([%s]:%u) - %s", Ip6AddrToString(&aDestSockAddr->mAddress), aDestSockAddr->mPort,
+            BufferToString(aBuffer, aLength));
 
 exit:
     return;
@@ -518,7 +566,14 @@
 
 void platformTrelInit(const char *aTrelUrl)
 {
-    otLogDebgPlat("[trel] platformTrelInit(aTrelUrl:\"%s\")", aTrelUrl != nullptr ? aTrelUrl : "");
+    // To silence "unused function" warning.
+    (void)LogCrit;
+    (void)LogWarn;
+    (void)LogInfo;
+    (void)LogNote;
+    (void)LogDebg;
+
+    LogDebg("platformTrelInit(aTrelUrl:\"%s\")", aTrelUrl != nullptr ? aTrelUrl : "");
 
     assert(!sInitialized);
 
@@ -545,7 +600,7 @@
     otPlatTrelDisable(nullptr);
     sInterfaceName[0] = '\0';
     sInitialized      = false;
-    otLogDebgPlat("[trel] platformTrelDeinit()");
+    LogDebg("platformTrelDeinit()");
 
 exit:
     return;
diff --git a/src/posix/platform/udp.cpp b/src/posix/platform/udp.cpp
index 4161672..89c6d3d 100644
--- a/src/posix/platform/udp.cpp
+++ b/src/posix/platform/udp.cpp
@@ -69,10 +69,6 @@
 
 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 IsMulticast(const otIp6Address &aAddress) { return aAddress.mFields.m8[0] == 0xff; }
-
 otError transmitPacket(int aFd, uint8_t *aPayload, uint16_t aLength, const otMessageInfo &aMessageInfo)
 {
 #ifdef __APPLE__
@@ -93,9 +89,9 @@
     memset(&peerAddr, 0, sizeof(peerAddr));
     peerAddr.sin6_port   = htons(aMessageInfo.mPeerPort);
     peerAddr.sin6_family = AF_INET6;
-    memcpy(&peerAddr.sin6_addr, &aMessageInfo.mPeerAddr, sizeof(peerAddr.sin6_addr));
+    CopyIp6AddressTo(aMessageInfo.mPeerAddr, &peerAddr.sin6_addr);
 
-    if (IsLinkLocal(peerAddr.sin6_addr) && !aMessageInfo.mIsHostInterface)
+    if (IsIp6AddressLinkLocal(aMessageInfo.mPeerAddr) && !aMessageInfo.mIsHostInterface)
     {
         // sin6_scope_id only works for link local destinations
         peerAddr.sin6_scope_id = gNetifIndex;
@@ -127,8 +123,7 @@
         controlLength += CMSG_SPACE(sizeof(int));
     }
 
-    if (!IsMulticast(aMessageInfo.mSockAddr) &&
-        memcmp(&aMessageInfo.mSockAddr, &in6addr_any, sizeof(aMessageInfo.mSockAddr)))
+    if (!IsIp6AddressMulticast(aMessageInfo.mSockAddr) && !IsIp6AddressUnspecified(aMessageInfo.mSockAddr))
     {
         struct in6_pktinfo pktinfo;
 
@@ -139,7 +134,7 @@
 
         pktinfo.ipi6_ifindex = aMessageInfo.mIsHostInterface ? 0 : gNetifIndex;
 
-        memcpy(&pktinfo.ipi6_addr, &aMessageInfo.mSockAddr, sizeof(pktinfo.ipi6_addr));
+        CopyIp6AddressTo(aMessageInfo.mSockAddr, &pktinfo.ipi6_addr);
         memcpy(CMSG_DATA(cmsg), &pktinfo, sizeof(pktinfo));
 
         controlLength += CMSG_SPACE(sizeof(pktinfo));
@@ -206,13 +201,13 @@
                 memcpy(&pktinfo, CMSG_DATA(cmsg), sizeof(pktinfo));
 
                 aMessageInfo.mIsHostInterface = (pktinfo.ipi6_ifindex != gNetifIndex);
-                memcpy(&aMessageInfo.mSockAddr, &pktinfo.ipi6_addr, sizeof(aMessageInfo.mSockAddr));
+                ReadIp6AddressFrom(&pktinfo.ipi6_addr, aMessageInfo.mSockAddr);
             }
         }
     }
 
     aMessageInfo.mPeerPort = ntohs(peerAddr.sin6_port);
-    memcpy(&aMessageInfo.mPeerAddr, &peerAddr.sin6_addr, sizeof(aMessageInfo.mPeerAddr));
+    ReadIp6AddressFrom(&peerAddr.sin6_addr, aMessageInfo.mPeerAddr);
 
 exit:
     return rval > 0 ? OT_ERROR_NONE : OT_ERROR_FAILED;
@@ -270,7 +265,8 @@
         memset(&sin6, 0, sizeof(struct sockaddr_in6));
         sin6.sin6_port   = htons(aUdpSocket->mSockName.mPort);
         sin6.sin6_family = AF_INET6;
-        memcpy(&sin6.sin6_addr, &aUdpSocket->mSockName.mAddress, sizeof(sin6.sin6_addr));
+        CopyIp6AddressTo(aUdpSocket->mSockName.mAddress, &sin6.sin6_addr);
+
         VerifyOrExit(0 == bind(fd, reinterpret_cast<struct sockaddr *>(&sin6), sizeof(sin6)), error = OT_ERROR_FAILED);
     }
 
@@ -283,7 +279,7 @@
 exit:
     if (error == OT_ERROR_FAILED)
     {
-        otLogCritPlat("Failed to bind UDP socket: %s", strerror(errno));
+        ot::Posix::Udp::LogCrit("Failed to bind UDP socket: %s", strerror(errno));
     }
 
     return error;
@@ -325,7 +321,7 @@
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
         if (otSysGetInfraNetifName() == nullptr || otSysGetInfraNetifName()[0] == '\0')
         {
-            otLogWarnPlat("No backbone interface given, %s fails.", __func__);
+            ot::Posix::Udp::LogWarn("No backbone interface given, %s fails.", __func__);
             ExitNow(error = OT_ERROR_INVALID_ARGS);
         }
 #ifdef __linux__
@@ -358,8 +354,7 @@
     otError             error = OT_ERROR_NONE;
     struct sockaddr_in6 sin6;
     int                 fd;
-    bool isDisconnect = memcmp(&aUdpSocket->mPeerName.mAddress, &in6addr_any, sizeof(in6addr_any)) == 0 &&
-                        aUdpSocket->mPeerName.mPort == 0;
+    bool isDisconnect = IsIp6AddressUnspecified(aUdpSocket->mPeerName.mAddress) && (aUdpSocket->mPeerName.mPort == 0);
 
     VerifyOrExit(aUdpSocket->mHandle != nullptr, error = OT_ERROR_INVALID_ARGS);
 
@@ -367,10 +362,11 @@
 
     memset(&sin6, 0, sizeof(struct sockaddr_in6));
     sin6.sin6_port = htons(aUdpSocket->mPeerName.mPort);
+
     if (!isDisconnect)
     {
         sin6.sin6_family = AF_INET6;
-        memcpy(&sin6.sin6_addr, &aUdpSocket->mPeerName.mAddress, sizeof(sin6.sin6_addr));
+        CopyIp6AddressTo(aUdpSocket->mPeerName.mAddress, &sin6.sin6_addr);
     }
     else
     {
@@ -382,7 +378,7 @@
 
         if (getsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &netifName, &len) != 0)
         {
-                      otLogWarnPlat("Failed to read socket bound device: %s", strerror(errno));
+                      ot::Posix::Udp::LogWarn("Failed to read socket bound device: %s", strerror(errno));
                       len = 0;
         }
 
@@ -396,7 +392,7 @@
         {
                       fd = FdFromHandle(aUdpSocket->mHandle);
                       VerifyOrExit(setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &netifName, len) == 0, {
-                          otLogWarnPlat("Failed to bind to device: %s", strerror(errno));
+                          ot::Posix::Udp::LogWarn("Failed to bind to device: %s", strerror(errno));
                           error = OT_ERROR_FAILED;
                       });
         }
@@ -410,8 +406,9 @@
 #ifdef __APPLE__
         VerifyOrExit(errno == EAFNOSUPPORT && isDisconnect);
 #endif
-        otLogWarnPlat("Failed to connect to [%s]:%u: %s", Ip6AddressString(&aUdpSocket->mPeerName.mAddress).AsCString(),
-                      aUdpSocket->mPeerName.mPort, strerror(errno));
+        ot::Posix::Udp::LogWarn("Failed to connect to [%s]:%u: %s",
+                                Ip6AddressString(&aUdpSocket->mPeerName.mAddress).AsCString(),
+                                aUdpSocket->mPeerName.mPort, strerror(errno));
         error = OT_ERROR_FAILED;
     }
 
@@ -468,7 +465,7 @@
     VerifyOrExit(aUdpSocket->mHandle != nullptr, error = OT_ERROR_INVALID_ARGS);
     fd = FdFromHandle(aUdpSocket->mHandle);
 
-    memcpy(&mreq.ipv6mr_multiaddr, aAddress->mFields.m8, sizeof(mreq.ipv6mr_multiaddr));
+    CopyIp6AddressTo(*aAddress, &mreq.ipv6mr_multiaddr);
 
     switch (aNetifIdentifier)
     {
@@ -492,8 +489,9 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogCritPlat("IPV6_JOIN_GROUP failed: %s", strerror(errno));
+        ot::Posix::Udp::LogCrit("IPV6_JOIN_GROUP failed: %s", strerror(errno));
     }
+
     return error;
 }
 
@@ -508,7 +506,7 @@
     VerifyOrExit(aUdpSocket->mHandle != nullptr, error = OT_ERROR_INVALID_ARGS);
     fd = FdFromHandle(aUdpSocket->mHandle);
 
-    memcpy(&mreq.ipv6mr_multiaddr, aAddress->mFields.m8, sizeof(mreq.ipv6mr_multiaddr));
+    CopyIp6AddressTo(*aAddress, &mreq.ipv6mr_multiaddr);
 
     switch (aNetifIdentifier)
     {
@@ -532,14 +530,17 @@
 exit:
     if (error != OT_ERROR_NONE)
     {
-        otLogCritPlat("IPV6_LEAVE_GROUP failed: %s", strerror(errno));
+        ot::Posix::Udp::LogCrit("IPV6_LEAVE_GROUP failed: %s", strerror(errno));
     }
+
     return error;
 }
 
 namespace ot {
 namespace Posix {
 
+const char Udp::kLogModuleName[] = "Udp";
+
 void Udp::Update(otSysMainloopContext &aContext)
 {
     VerifyOrExit(gNetifIndex != 0);
diff --git a/src/posix/platform/udp.hpp b/src/posix/platform/udp.hpp
index adc252a..f22a8cf 100644
--- a/src/posix/platform/udp.hpp
+++ b/src/posix/platform/udp.hpp
@@ -29,14 +29,18 @@
 #define OT_POSIX_PLATFORM_UDP_HPP_
 
 #include "core/common/non_copyable.hpp"
-#include "posix/platform/mainloop.hpp"
+
+#include "logger.hpp"
+#include "mainloop.hpp"
 
 namespace ot {
 namespace Posix {
 
-class Udp : public Mainloop::Source, private NonCopyable
+class Udp : public Mainloop::Source, public Logger<Udp>, private NonCopyable
 {
 public:
+    static const char kLogModuleName[];
+
     static Udp &Get(void);
 
     void Init(const char *aIfName);
diff --git a/src/posix/platform/vendor_interface.hpp b/src/posix/platform/vendor_interface.hpp
index 4ee7a1a..449ebe5 100644
--- a/src/posix/platform/vendor_interface.hpp
+++ b/src/posix/platform/vendor_interface.hpp
@@ -31,8 +31,8 @@
  *   This file includes definitions for the vendor interface to radio (RCP).
  */
 
-#ifndef POSIX_APP_VENDOR_INTERFACE_HPP_
-#define POSIX_APP_VENDOR_INTERFACE_HPP_
+#ifndef OT_POSIX_APP_VENDOR_INTERFACE_HPP_
+#define OT_POSIX_APP_VENDOR_INTERFACE_HPP_
 
 #include "openthread-posix-config.h"
 
@@ -171,4 +171,4 @@
 } // namespace Posix
 } // namespace ot
 
-#endif // POSIX_APP_VENDOR_INTERFACE_HPP_
+#endif // OT_POSIX_APP_VENDOR_INTERFACE_HPP_
diff --git a/tests/scripts/expect/cli-tcat.exp b/tests/scripts/expect/cli-tcat.exp
new file mode 100755
index 0000000..00ce087
--- /dev/null
+++ b/tests/scripts/expect/cli-tcat.exp
@@ -0,0 +1,61 @@
+#!/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 "cli"
+
+switch_node 1
+send "tcat start\n"
+expect_line "Done"
+
+spawn python "tools/tcat_ble_client/bbtc.py" --simulation 1 --cert_path "tools/tcat_ble_client/auth"
+set py_client "$spawn_id"
+expect_line "Done"
+send "commission\n"
+expect_line "\tTYPE:\tRESPONSE_W_STATUS"
+expect_line "\tVALUE:\t0x00"
+
+send "thread start\n"
+expect_line "\tTYPE:\tRESPONSE_W_STATUS"
+expect_line "\tVALUE:\t0x00"
+
+send "exit\n"
+expect eof
+
+switch_node 1
+send "tcat stop\n"
+expect_line "Done"
+
+send "networkkey\n"
+expect_line "fda7c771a27202e232ecd04cf934f476"
+expect_line "Done"
+
+wait_for "state" "leader"
+expect_line "Done"
diff --git a/tests/scripts/expect/posix-rcp-local-host.exp b/tests/scripts/expect/posix-rcp-local-host.exp
new file mode 100755
index 0000000..e8c8201
--- /dev/null
+++ b/tests/scripts/expect/posix-rcp-local-host.exp
@@ -0,0 +1,44 @@
+#!/usr/bin/expect -f
+#
+#  Copyright (c) 2024, 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 rcp "spinel+hdlc+uart://$::env(OT_SIMULATION_APPS)/ncp/ot-rcp?forkpty-arg=-Llo&forkpty-arg=1"
+send "factoryreset\n"
+wait_for "state" "disabled"
+setup_default_network
+attach
+
+spawn_node 2 rcp "spinel+hdlc+uart://$::env(OT_SIMULATION_APPS)/ncp/ot-rcp?forkpty-arg=--local-host=127.0.0.1&forkpty-arg=2"
+send "factoryreset\n"
+wait_for "state" "disabled"
+setup_default_network
+attach child
+
+dispose_all
diff --git a/tests/scripts/thread-cert/addon_test_channel_manager_autocsl.py b/tests/scripts/thread-cert/addon_test_channel_manager_autocsl.py
new file mode 100755
index 0000000..2eaff4f
--- /dev/null
+++ b/tests/scripts/thread-cert/addon_test_channel_manager_autocsl.py
@@ -0,0 +1,143 @@
+#!/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 mle
+import thread_cert
+from pktverify import consts
+
+LEADER = 1
+ED = 2
+SSED = 3
+
+
+class SSED_CslChannelManager(thread_cert.TestCase):
+    TOPOLOGY = {
+        LEADER: {
+            'version': '1.2',
+        },
+        ED: {
+            'version': '1.2',
+            'is_mtd': False,
+            'mode': 'rn',
+        },
+        SSED: {
+            'version': '1.2',
+            'is_mtd': True,
+            'mode': '-',
+        },
+    }
+    """All nodes are created with default configurations"""
+
+    def test(self):
+
+        self.nodes[SSED].set_csl_period(consts.CSL_DEFAULT_PERIOD)
+        self.nodes[SSED].set_csl_timeout(consts.CSL_DEFAULT_TIMEOUT)
+
+        self.nodes[SSED].get_csl_info()
+
+        self.nodes[LEADER].start()
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual(self.nodes[LEADER].get_state(), 'leader')
+        channel = self.nodes[LEADER].get_channel()
+
+        self.nodes[SSED].start()
+        self.simulator.go(7)
+        self.assertEqual(self.nodes[SSED].get_state(), 'child')
+
+        csl_channel = 0
+        csl_config = self.nodes[SSED].get_csl_info()
+        self.assertTrue(int(csl_config['channel']) == csl_channel)
+        self.assertTrue(csl_config['period'] == '500000us')
+
+        print('SSED rloc:%s' % self.nodes[SSED].get_rloc())
+        self.assertTrue(self.nodes[LEADER].ping(self.nodes[SSED].get_rloc()))
+
+        # let channel monitor collect >970 sample counts
+        self.simulator.go(980 * 41)
+        results = self.nodes[SSED].get_channel_monitor_info()
+        self.assertTrue(int(results['count']) > 970)
+
+        # Configure channel manager channel masks
+        # Set cca threshold to 0 as we cannot change cca assessment in simulation.
+        # and shorten interval to speedup test
+        all_channels_mask = int('0x7fff800', 0)
+        chan_12_to_15_mask = int('0x000f000', 0)
+        interval = 30
+        self.nodes[SSED].set_channel_manager_supported(all_channels_mask)
+        self.nodes[SSED].set_channel_manager_favored(chan_12_to_15_mask)
+        self.nodes[SSED].set_channel_manager_cca_threshold('0x0000')
+        self.nodes[SSED].set_channel_manager_interval(interval)
+
+        # enable channel manager auto-select and check
+        # network channel is not changed by channel manager on SSED
+        # and also csl_channel is unchanged
+        self.nodes[SSED].set_channel_manager_auto_enable(True)
+        self.simulator.go(interval + 1)
+        results = self.nodes[SSED].get_channel_manager_config()
+        self.assertTrue(int(results['auto']) == 1)
+        self.assertTrue(results['cca threshold'] == '0x0000')
+        self.assertTrue(int(results['interval']) == interval)
+        self.assertTrue('11-26' in results['supported'])
+        self.simulator.go(1)
+        self.assertTrue(self.nodes[SSED].get_channel() == channel)
+        csl_config = self.nodes[SSED].get_csl_info()
+        self.assertTrue(int(csl_config['channel']) == csl_channel)
+
+        # check SSED can change csl channel
+        csl_channel = 25
+        self.flush_all()
+        self.nodes[SSED].set_csl_channel(csl_channel)
+        self.simulator.go(1)
+        ssed_messages = self.simulator.get_messages_sent_by(SSED)
+        self.assertIsNotNone(ssed_messages.next_mle_message(mle.CommandType.CHILD_UPDATE_REQUEST))
+        self.simulator.go(1)
+        csl_config = self.nodes[SSED].get_csl_info()
+        self.assertTrue(int(csl_config['channel']) == csl_channel)
+        self.simulator.go(1)
+        self.assertTrue(self.nodes[LEADER].ping(self.nodes[SSED].get_rloc()))
+
+        # enable channel manager autocsl-select in addition
+        # and check csl channel changed to best favored channel 12
+        csl_channel = 12
+        self.nodes[SSED].set_channel_manager_autocsl_enable(True)
+        self.simulator.go(interval + 1)
+        results = self.nodes[SSED].get_channel_manager_config()
+        self.assertTrue(int(results['autocsl']) == 1)
+        self.assertTrue(int(results['channel']) == csl_channel)
+        csl_config = self.nodes[SSED].get_csl_info()
+        self.assertTrue(int(csl_config['channel']) == csl_channel)
+        self.simulator.go(1)
+        self.assertTrue(self.nodes[LEADER].ping(self.nodes[SSED].get_rloc()))
+
+
+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 5c66bf9..2ae828b 100755
--- a/tests/scripts/thread-cert/border_router/test_advertising_proxy.py
+++ b/tests/scripts/thread-cert/border_router/test_advertising_proxy.py
@@ -27,7 +27,6 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 import ipaddress
-import logging
 import unittest
 
 import config
@@ -81,6 +80,12 @@
         host.start(start_radvd=False)
         self.simulator.go(5)
 
+        # Reserve UDP ports to verify that SRP server can skip the unavailable
+        # ports correctly
+        server.reserve_udp_port(53535)
+        server.reserve_udp_port(53536)
+        server.reserve_udp_port(53537)
+
         self.assertEqual(server.srp_server_get_state(), 'disabled')
         server.srp_server_set_enabled(True)
         server.srp_server_set_lease_range(LEASE, LEASE, KEY_LEASE, KEY_LEASE)
@@ -88,6 +93,7 @@
         self.simulator.go(config.BORDER_ROUTER_STARTUP_DELAY)
         self.assertEqual('leader', server.get_state())
         self.assertEqual(server.srp_server_get_state(), 'running')
+        self.assertNotIn(server.get_srp_server_port(), [53535, 53536, 53537])
 
         client.start()
         self.simulator.go(config.ROUTER_STARTUP_DELAY)
diff --git a/tests/scripts/thread-cert/border_router/test_manual_maddress.py b/tests/scripts/thread-cert/border_router/test_manual_maddress.py
index 536d3ad..78edac8 100755
--- a/tests/scripts/thread-cert/border_router/test_manual_maddress.py
+++ b/tests/scripts/thread-cert/border_router/test_manual_maddress.py
@@ -86,11 +86,12 @@
 
         # TD registers for multicast address, MA1, at BR_1.
         td.add_ipmaddr_tun(MA1)
-        self.simulator.go(5)
+        self.simulator.go(10)
 
         # Host sends a ping packet to the multicast address, MA1. TD should
         # respond to the ping request.
-        host.ping(MA1, backbone=True, ttl=10, interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0])
+        self.assertTrue(
+            host.ping(MA1, backbone=True, ttl=10, interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
         self.simulator.go(5)
 
     def verify(self, pv: pktverify.packet_verifier.PacketVerifier):
diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py
index 5359848..9f5d337 100755
--- a/tests/scripts/thread-cert/node.py
+++ b/tests/scripts/thread-cert/node.py
@@ -197,6 +197,9 @@
         self.pexpect.wait()
         self.pexpect.proc.kill()
 
+    def reserve_udp_port(self, port):
+        self.bash(f'socat -u UDP6-LISTEN:{port},bindtodevice=wpan0 - &')
+
     def destroy(self):
         logging.info("Destroying %s", self)
         self._shutdown_docker()
@@ -807,6 +810,18 @@
         results = [line for line in output if self._match_pattern(line, pattern)]
         return results
 
+    def _expect_key_value_pairs(self, pattern, separator=': '):
+        """Expect 'key: value' in multiple lines.
+
+        Returns:
+            Dictionary of the key:value pairs.
+        """
+        result = {}
+        for line in self._expect_results(pattern):
+            key, val = line.split(separator)
+            result.update({key: val})
+        return result
+
     @staticmethod
     def _match_pattern(line, pattern):
         if isinstance(pattern, str):
@@ -1700,6 +1715,10 @@
         self.send_command(cmd)
         self._expect_done()
 
+    def get_key_switch_guardtime(self):
+        self.send_command('keysequence guardtime')
+        return int(self._expect_result(r'\d+'))
+
     def set_key_switch_guardtime(self, key_switch_guardtime):
         cmd = 'keysequence guardtime %d' % key_switch_guardtime
         self.send_command(cmd)
@@ -1795,7 +1814,7 @@
 
     def get_csl_info(self):
         self.send_command('csl')
-        self._expect_done()
+        return self._expect_key_value_pairs(r'\S+')
 
     def set_csl_channel(self, csl_channel):
         self.send_command('csl channel %d' % csl_channel)
@@ -2683,7 +2702,7 @@
         self.send_command('dataset commit pending')
         self._expect_done()
 
-    def start_dataset_updater(self, panid=None, channel=None):
+    def start_dataset_updater(self, panid=None, channel=None, security_policy=None, delay=None):
         self.send_command('dataset clear')
         self._expect_done()
 
@@ -2697,6 +2716,18 @@
             self.send_command(cmd)
             self._expect_done()
 
+        if security_policy is not None:
+            cmd = 'dataset securitypolicy %d %s ' % (security_policy[0], security_policy[1])
+            if (len(security_policy) >= 3):
+                cmd += '%d ' % (security_policy[2])
+            self.send_command(cmd)
+            self._expect_done()
+
+        if delay is not None:
+            cmd = 'dataset delay %d ' % delay
+            self.send_command(cmd)
+            self._expect_done()
+
         self.send_command('dataset updater start')
         self._expect_done()
 
@@ -3582,6 +3613,81 @@
         line = self._expect_command_output()[0]
         return [int(item) for item in line.split()]
 
+    def get_channel_monitor_info(self) -> Dict:
+        """
+        Returns:
+            Dict of channel monitor info, e.g. 
+                {'enabled': '1',
+                 'interval': '41000',
+                 'threshold': '-75',
+                 'window': '960',
+                 'count': '985',
+                 'occupancies': {
+                    '11': '0.00%',
+                    '12': '3.50%',
+                    '13': '9.89%',
+                    '14': '15.36%',
+                    '15': '20.02%',
+                    '16': '21.95%',
+                    '17': '32.71%',
+                    '18': '35.76%',
+                    '19': '37.97%',
+                    '20': '43.68%',
+                    '21': '48.95%',
+                    '22': '54.05%',
+                    '23': '58.65%',
+                    '24': '68.26%',
+                    '25': '66.73%',
+                    '26': '73.12%'
+                    }
+                }
+        """
+        config = {}
+        self.send_command('channel monitor')
+
+        for line in self._expect_results(r'\S+'):
+            if re.match(r'.*:\s.*', line):
+                key, val = line.split(':')
+                config.update({key: val.strip()})
+            elif re.match(r'.*:', line):  # occupancy
+                occ_key, val = line.split(':')
+                val = {}
+                config.update({occ_key: val})
+            elif 'busy' in line:
+                # channel occupancies
+                key = line.split()[1]
+                val = line.split()[3]
+                config[occ_key].update({key: val})
+        return config
+
+    def set_channel_manager_auto_enable(self, enable: bool):
+        self.send_command(f'channel manager auto {int(enable)}')
+        self._expect_done()
+
+    def set_channel_manager_autocsl_enable(self, enable: bool):
+        self.send_command(f'channel manager autocsl {int(enable)}')
+        self._expect_done()
+
+    def set_channel_manager_supported(self, channel_mask: int):
+        self.send_command(f'channel manager supported {int(channel_mask)}')
+        self._expect_done()
+
+    def set_channel_manager_favored(self, channel_mask: int):
+        self.send_command(f'channel manager favored {int(channel_mask)}')
+        self._expect_done()
+
+    def set_channel_manager_interval(self, interval: int):
+        self.send_command(f'channel manager interval {interval}')
+        self._expect_done()
+
+    def set_channel_manager_cca_threshold(self, hex_value: str):
+        self.send_command(f'channel manager threshold {hex_value}')
+        self._expect_done()
+
+    def get_channel_manager_config(self):
+        self.send_command('channel manager')
+        return self._expect_key_value_pairs(r'\S+')
+
 
 class Node(NodeImpl, OtCli):
     pass
diff --git a/tests/scripts/thread-cert/test_detach.py b/tests/scripts/thread-cert/test_detach.py
index 954df98..c32f8b3 100755
--- a/tests/scripts/thread-cert/test_detach.py
+++ b/tests/scripts/thread-cert/test_detach.py
@@ -105,7 +105,7 @@
         self.assertFalse(list(filter(lambda x: x[1]['rloc16'] == router1_rloc16, leader.router_table().items())))
 
         router1.start()
-        self.simulator.go(5)
+        self.simulator.go(config.ROUTER_STARTUP_DELAY)
         self.assertEqual(router1.get_state(), 'router')
 
         child1.start()
@@ -121,7 +121,7 @@
         self.assertEqual(child1.get_state(), 'disabled')
 
         router1.start()
-        self.simulator.go(5)
+        self.simulator.go(config.ROUTER_STARTUP_DELAY)
         self.assertEqual(router1.get_state(), 'router')
 
         child1.start()
diff --git a/tests/scripts/thread-cert/test_key_rotation_and_key_guard_time.py b/tests/scripts/thread-cert/test_key_rotation_and_key_guard_time.py
new file mode 100755
index 0000000..595ffea
--- /dev/null
+++ b/tests/scripts/thread-cert/test_key_rotation_and_key_guard_time.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2024, 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 math
+
+import command
+import config
+import thread_cert
+
+# Test description:
+#
+#   This test verifies key rotation and key guard time mechanisms.
+#
+#
+# Topology:
+#
+#   leader ---  router
+#    |    \
+#    |     \
+#  child   reed
+#
+
+LEADER = 1
+CHILD = 2
+REED = 3
+ROUTER = 4
+
+
+class MleMsgKeySeqJump(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+    SUPPORT_NCP = False
+
+    TOPOLOGY = {
+        LEADER: {
+            'name': 'LEADER',
+            'mode': 'rdn',
+        },
+        CHILD: {
+            'name': 'CHILD',
+            'is_mtd': True,
+            'mode': 'rn',
+        },
+        REED: {
+            'name': 'REED',
+            'mode': 'rn'
+        },
+        ROUTER: {
+            'name': 'ROUTER',
+            'mode': 'rdn',
+        },
+    }
+
+    def test(self):
+        leader = self.nodes[LEADER]
+        child = self.nodes[CHILD]
+        reed = self.nodes[REED]
+        router = self.nodes[ROUTER]
+
+        nodes = [leader, child, reed, router]
+
+        #-------------------------------------------------------------------
+        # Form the network.
+
+        for node in nodes:
+            node.set_key_sequence_counter(0)
+
+        leader.start()
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual(leader.get_state(), 'leader')
+
+        child.start()
+        reed.start()
+        self.simulator.go(5)
+        self.assertEqual(child.get_state(), 'child')
+        self.assertEqual(reed.get_state(), 'child')
+
+        router.start()
+        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.assertEqual(router.get_state(), 'router')
+
+        #-------------------------------------------------------------------
+        # Validate the initial key seq counter and key switch guard time
+
+        for node in nodes:
+            self.assertEqual(node.get_key_sequence_counter(), 0)
+            self.assertEqual(node.get_key_switch_guardtime(), 624)
+
+        #-------------------------------------------------------------------
+        # Change the key rotation time a bunch of times and make sure that
+        # the key switch guard time is properly changed (should be set
+        # to 93% of the rotation time).
+
+        for rotation_time in [100, 1, 10, 888, 2]:
+            reed.start_dataset_updater(security_policy=[rotation_time, 'onrc'])
+            guardtime = math.floor(rotation_time * 93 / 100) if rotation_time >= 2 else 1
+            self.simulator.go(100)
+            for node in nodes:
+                self.assertEqual(node.get_key_switch_guardtime(), guardtime)
+
+        #-------------------------------------------------------------------
+        # Wait for key rotation time (2 hours) and check that all nodes
+        # moved to the next key seq counter
+
+        self.simulator.go(2 * 60 * 60)
+        for node in nodes:
+            self.assertEqual(node.get_key_sequence_counter(), 1)
+
+        #-------------------------------------------------------------------
+        # Manually increment the key sequence counter on leader and make
+        # sure other nodes are not updated due to key guard time.
+
+        router.set_key_sequence_counter(2)
+
+        self.simulator.go(50 * 60)
+
+        self.assertEqual(router.get_key_sequence_counter(), 2)
+
+        for node in [leader, reed, child]:
+            self.assertEqual(node.get_key_sequence_counter(), 1)
+
+        #-------------------------------------------------------------------
+        # Make sure nodes can communicate with each other.
+
+        self.assertTrue(leader.ping(router.get_mleid()))
+        self.assertTrue(router.ping(child.get_mleid()))
+
+        #-------------------------------------------------------------------
+        # Wait for rotation time to expire. Validate that the `router`
+        # has moved to key seq `3` and all other nodes also followed.
+
+        self.simulator.go(75 * 60)
+
+        self.assertEqual(router.get_key_sequence_counter(), 3)
+
+        for node in nodes:
+            self.assertEqual(node.get_key_sequence_counter(), 3)
+
+        self.assertTrue(leader.ping(router.get_mleid()))
+        self.assertTrue(router.ping(child.get_mleid()))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/scripts/thread-cert/test_mle_msg_key_seq_jump.py b/tests/scripts/thread-cert/test_mle_msg_key_seq_jump.py
index d400557..2463434 100755
--- a/tests/scripts/thread-cert/test_mle_msg_key_seq_jump.py
+++ b/tests/scripts/thread-cert/test_mle_msg_key_seq_jump.py
@@ -221,20 +221,20 @@
         self.assertEqual(reed.get_key_sequence_counter(), 20)
 
         #-------------------------------------------------------------------
-        # Move forward the key seq counter by one on router. Wait for max
+        # Move forward the key seq counter by two on router. Wait for max
         # time between advertisements. Validate that leader adopts the higher
         # counter value.
 
-        router.set_key_sequence_counter(21)
-        self.assertEqual(router.get_key_sequence_counter(), 21)
+        router.set_key_sequence_counter(22)
+        self.assertEqual(router.get_key_sequence_counter(), 22)
 
         self.simulator.go(52)
-        self.assertEqual(leader.get_key_sequence_counter(), 21)
-        self.assertEqual(reed.get_key_sequence_counter(), 21)
+        self.assertEqual(leader.get_key_sequence_counter(), 22)
+        self.assertEqual(reed.get_key_sequence_counter(), 22)
 
         child.set_mode('r')
         self.simulator.go(2)
-        self.assertEqual(child.get_key_sequence_counter(), 21)
+        self.assertEqual(child.get_key_sequence_counter(), 22)
 
         #-------------------------------------------------------------------
         # Force a reattachment from the child with a higher key seq counter,
@@ -247,6 +247,7 @@
 
         child.factory_reset()
         self.assertEqual(child.get_state(), 'disabled')
+        child.set_mode('r')
 
         child.set_active_dataset(channel=leader.get_channel(),
                                  network_key=leader.get_networkkey(),
diff --git a/tests/scripts/thread-cert/test_netdata_publisher.py b/tests/scripts/thread-cert/test_netdata_publisher.py
index 7192e6f..c31ac6e 100755
--- a/tests/scripts/thread-cert/test_netdata_publisher.py
+++ b/tests/scripts/thread-cert/test_netdata_publisher.py
@@ -70,7 +70,7 @@
 
 # The desired number of entries (based on related config).
 DESIRED_NUM_DNSSRP_ANYCAST = 8
-DESIRED_NUM_DNSSRP_UNCIAST = 2
+DESIRED_NUM_DNSSRP_UNICAST = 2
 DESIRED_NUM_ON_MESH_PREFIX = 3
 DESIRED_NUM_EXTERNAL_ROUTE = 10
 
@@ -263,52 +263,39 @@
             self.verify_anycast_services(services)
 
         #---------------------------------------------------------------------------------
-        # DNS/SRP unicast entries
+        # DNS/SRP service data unicast entries
 
-        # Publish DNS/SRP unicast address on all routers, first using
-        # MLE-EID address, then change to use specific address. Verify
-        # that number of entries in network data is correct in each step
-        # and that entries are switched correctly.
         num = 0
         for node in routers:
-            node.netdata_publish_dnssrp_unicast_mleid(DNSSRP_PORT)
-            self.simulator.go(WAIT_TIME)
-            num += 1
-            services = leader.get_services()
-            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNCIAST))
-            self.verify_unicast_services(services)
-
-        for node in routers:
             node.netdata_publish_dnssrp_unicast(DNSSRP_ADDRESS, DNSSRP_PORT)
             self.simulator.go(WAIT_TIME)
+            num += 1
             services = leader.get_services()
-            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNCIAST))
+            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNICAST))
             self.verify_unicast_services(services)
 
         for node in routers:
             node.srp_server_set_enabled(True)
             self.simulator.go(WAIT_TIME)
+
         self.assertEqual(sum(node.srp_server_get_state() == 'running' for node in routers),
-                         min(len(routers), DESIRED_NUM_DNSSRP_UNCIAST))
+                         min(len(routers), DESIRED_NUM_DNSSRP_UNICAST))
         self.assertEqual(sum(node.srp_server_get_state() == 'stopped' for node in routers),
-                         max(len(routers) - DESIRED_NUM_DNSSRP_UNCIAST, 0))
+                         max(len(routers) - DESIRED_NUM_DNSSRP_UNICAST, 0))
 
         for node in routers:
             node.netdata_unpublish_dnssrp()
             self.simulator.go(WAIT_TIME)
             num -= 1
             services = leader.get_services()
-            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNCIAST))
+            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNICAST))
             self.verify_unicast_services(services)
         for node in routers:
             node.srp_server_set_enabled(False)
             self.assertEqual(node.srp_server_get_state(), 'disabled')
 
         #---------------------------------------------------------------------------------
-        # DNS/SRP unicast and anycast entry
-
-        # Verify that publishing an anycast entry will update the limit
-        # for the unicast MLE-EID address entry and all are removed.
+        # DNS/SRP server data unicast entries
 
         num = 0
         for node in routers:
@@ -316,20 +303,77 @@
             self.simulator.go(WAIT_TIME)
             num += 1
             services = leader.get_services()
-            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNCIAST))
+            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNICAST))
             self.verify_unicast_services(services)
 
+        for node in routers:
+            node.srp_server_set_enabled(True)
+            self.simulator.go(WAIT_TIME)
+        self.assertEqual(sum(node.srp_server_get_state() == 'running' for node in routers),
+                         min(len(routers), DESIRED_NUM_DNSSRP_UNICAST))
+        self.assertEqual(sum(node.srp_server_get_state() == 'stopped' for node in routers),
+                         max(len(routers) - DESIRED_NUM_DNSSRP_UNICAST, 0))
+
+        for node in routers:
+            node.netdata_unpublish_dnssrp()
+            self.simulator.go(WAIT_TIME)
+            num -= 1
+            services = leader.get_services()
+            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNICAST))
+            self.verify_unicast_services(services)
+        for node in routers:
+            node.srp_server_set_enabled(False)
+            self.assertEqual(node.srp_server_get_state(), 'disabled')
+
+        #---------------------------------------------------------------------------------
+        # DNS/SRP server data unicast vs anycast
+
+        num = 0
+        for node in routers:
+            node.netdata_publish_dnssrp_unicast_mleid(DNSSRP_PORT)
+            self.simulator.go(WAIT_TIME)
+            num += 1
+            services = leader.get_services()
+            self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNICAST))
+            self.verify_unicast_services(services)
+
+        # Verify that publishing an anycast entry will update the
+        # limit for the server data unicast address entry and all are
+        # removed.
+
         leader.netdata_publish_dnssrp_anycast(ANYCAST_SEQ_NUM)
         self.simulator.go(WAIT_TIME)
         services = leader.get_services()
         self.assertEqual(len(services), 1)
         self.verify_anycast_services(services)
 
+        # Removing the anycast entry will cause the lower priority
+        # server data unicast entries to be added again.
+
         leader.netdata_unpublish_dnssrp()
         self.simulator.go(WAIT_TIME)
 
         services = leader.get_services()
-        self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNCIAST))
+        self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNICAST))
+        self.verify_unicast_services(services)
+
+        #---------------------------------------------------------------------------------
+        # DNS/SRP server data unicast vs service data unicast
+
+        leader.netdata_publish_dnssrp_unicast(DNSSRP_ADDRESS, DNSSRP_PORT)
+        self.simulator.go(WAIT_TIME)
+        services = leader.get_services()
+        self.assertEqual(len(services), 1)
+        self.verify_unicast_services(services)
+
+        # Removing the service data unicast entry will cause the lower
+        # priority server data unicast entries to be added again.
+
+        leader.netdata_unpublish_dnssrp()
+        self.simulator.go(WAIT_TIME)
+
+        services = leader.get_services()
+        self.assertEqual(len(services), min(num, DESIRED_NUM_DNSSRP_UNICAST))
         self.verify_unicast_services(services)
 
         for node in routers:
@@ -494,15 +538,15 @@
 
         # Replace the published route on leader with '::/0'.
         leader.netdata_publish_replace(EXTERNAL_ROUTE, '::/0', EXTERNAL_FLAGS, 'med')
-        self.simulator.go(0.2)
+        self.simulator.go(1)
         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.assertEqual([route.split(' ')[0] == '::/0' for route in routes].count(True), 0)
         self.check_num_of_routes(routes, num - 1, 0, 1)
 
         # Publish the same prefix on leader as an on-mesh prefix. Make
diff --git a/tests/scripts/thread-cert/test_srp_server_reboot_port.py b/tests/scripts/thread-cert/test_srp_server_reboot_port.py
index d78dc11..b38bcfe 100755
--- a/tests/scripts/thread-cert/test_srp_server_reboot_port.py
+++ b/tests/scripts/thread-cert/test_srp_server_reboot_port.py
@@ -94,14 +94,14 @@
 
         #
         # 2. Reboot the server without any service registered. The server should
-        # listen to the same port after the reboot.
+        # switch to a new port after re-enabling.
         #
         old_port = server.get_srp_server_port()
         server.srp_server_set_enabled(False)
         self.simulator.go(5)
         server.srp_server_set_enabled(True)
         self.simulator.go(5)
-        self.assertEqual(old_port, server.get_srp_server_port())
+        self.assertNotEqual(old_port, server.get_srp_server_port())
 
         #
         # 3. Register a service
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 6f8b01c..146b2b5 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
@@ -79,7 +79,7 @@
         msg.assertMleMessageContainsTlv(mle.Challenge)
         msg.assertMleMessageContainsTlv(mle.ScanMask)
         msg.assertMleMessageContainsTlv(mle.Version)
-        assert msg.get_mle_message_tlv(mle.Version).version >= config.THREAD_VERSION_1_2
+        self.assertGreaterEqual(msg.get_mle_message_tlv(mle.Version).version, config.THREAD_VERSION_1_2)
 
         scan_mask_tlv = msg.get_mle_message_tlv(mle.ScanMask)
         self.assertEqual(1, scan_mask_tlv.router)
@@ -97,7 +97,7 @@
         msg.assertMleMessageContainsTlv(mle.LinkMargin)
         msg.assertMleMessageContainsTlv(mle.Connectivity)
         msg.assertMleMessageContainsTlv(mle.Version)
-        assert msg.get_mle_message_tlv(mle.Version).version >= config.THREAD_VERSION_1_2
+        self.assertGreaterEqual(msg.get_mle_message_tlv(mle.Version).version, config.THREAD_VERSION_1_2)
 
         # 4 - Router_1 receives the MLE Parent Response and sends a Child ID Request
         msg = router_messages.next_mle_message(mle.CommandType.CHILD_ID_REQUEST)
@@ -110,7 +110,7 @@
         msg.assertMleMessageContainsTlv(mle.Version)
         msg.assertMleMessageContainsTlv(mle.TlvRequest)
         msg.assertMleMessageDoesNotContainTlv(mle.AddressRegistration)
-        assert msg.get_mle_message_tlv(mle.Version).version >= config.THREAD_VERSION_1_2
+        self.assertGreaterEqual(msg.get_mle_message_tlv(mle.Version).version, config.THREAD_VERSION_1_2)
 
         # 5 - Leader responds with a Child ID Response
         msg = leader_messages.next_mle_message(mle.CommandType.CHILD_ID_RESPONSE)
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 36d5264..888a6d9 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
@@ -128,7 +128,7 @@
         WAIT_TIME = BBR_REGISTRATION_JITTER + WAIT_REDUNDANCE
         self.simulator.go(WAIT_TIME)
         self.assertEqual(self.nodes[BBR_1].get_backbone_router_state(), 'Primary')
-        assert self.nodes[BBR_1].get_backbone_router()['seqno'] == 2
+        self.assertEqual(self.nodes[BBR_1].get_backbone_router()['seqno'], 2)
 
         # 3) Reset BBR_1 and bring it back after its original router id is released
         # 200s (100s MaxNeighborAge + 90s InfiniteCost + 10s redundance)
@@ -186,7 +186,7 @@
         # Check no SRV_DATA.ntf.
         messages = self.simulator.get_messages_sent_by(BBR_2)
         msg = messages.next_coap_message('0.02', '/a/sd', False)
-        assert (msg is None), "Error: %d sent unexpected SRV_DATA.ntf when there is PBbr already"
+        self.assertIsNone(msg)
 
         # Flush relative message queue.
         self.flush_nodes([BBR_1])
@@ -203,7 +203,7 @@
         messages.next_coap_message('0.02', '/a/sd', True)
         self.assertEqual(self.nodes[BBR_1].get_backbone_router_state(), 'Secondary')
         # Verify Sequence number increases when become Secondary from Primary.
-        assert self.nodes[BBR_1].get_backbone_router()['seqno'] == (BBR_1_SEQNO + 1)
+        self.assertEqual(self.nodes[BBR_1].get_backbone_router()['seqno'], BBR_1_SEQNO + 1)
 
         # 4a) Check communication via DUA.
         bbr2_dua = self.nodes[BBR_2].get_addr(config.DOMAIN_PREFIX)
@@ -238,7 +238,7 @@
 
         # 6a) Check the uniqueness of DUA by comparing the one in above 4a).
         bbr2_dua2 = self.nodes[BBR_2].get_addr(config.DOMAIN_PREFIX)
-        assert bbr2_dua == bbr2_dua2, 'Error: Unexpected different DUA ({} v.s. {})'.format(bbr2_dua, bbr2_dua2)
+        self.assertEqual(bbr2_dua, bbr2_dua2)
 
         # 6b) Check communication via DUA
         self.assertTrue(self.nodes[BBR_1].ping(bbr2_dua))
diff --git a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py
index 7a98680..81c9054 100755
--- a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py
+++ b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py
@@ -242,10 +242,8 @@
         WAIT_TIME = WAIT_REDUNDANCE
         self.simulator.go(WAIT_TIME)
         dua = self.nodes[MED_1_2].get_addr(config.DOMAIN_PREFIX)
-        assert ipaddress.ip_address(dua) == ipaddress.ip_address(
-            med_1_2_dua), 'Error: Expected SLAAC DUA not generated'
-        assert ipaddress.ip_address(med_1_2_dua) == ipaddress.ip_address(
-            dua), 'Error: Expected same SLAAC DUA not generated'
+        self.assertEqual(ipaddress.ip_address(dua), ipaddress.ip_address(med_1_2_dua))
+        self.assertEqual(ipaddress.ip_address(med_1_2_dua), ipaddress.ip_address(dua))
 
         self.__check_dua_registration(MED_1_2, med_1_2_dua_iid, domain_prefix_cid)
 
@@ -269,8 +267,7 @@
         self.simulator.go(WAIT_TIME)
         dua = self.nodes[MED_1_2].get_addr(config.DOMAIN_PREFIX)
         assert dua, 'Error: Expected DUA not found'
-        assert ipaddress.ip_address(med_1_2_dua) == ipaddress.ip_address(
-            dua), 'Error: Expected same SLAAC DUA not generated'
+        self.assertEqual(ipaddress.ip_address(med_1_2_dua), ipaddress.ip_address(dua))
 
         self.__check_dua_registration(MED_1_2, med_1_2_dua_iid, domain_prefix_cid)
 
@@ -299,8 +296,7 @@
         self.simulator.go(WAIT_TIME)
         dua = self.nodes[MED_1_2].get_addr(config.DOMAIN_PREFIX)
         assert dua, 'Error: Expected DUA not found'
-        assert ipaddress.ip_address(med_1_2_dua) == ipaddress.ip_address(
-            dua), 'Error: Expected same SLAAC DUA not generated'
+        self.assertEqual(ipaddress.ip_address(med_1_2_dua), ipaddress.ip_address(dua))
 
         self.__check_dua_registration(MED_1_2, med_1_2_dua_iid, domain_prefix_cid)
 
diff --git a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py
index 4053fec..76772ab 100755
--- a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py
+++ b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py
@@ -276,7 +276,7 @@
 
         dua2 = self.nodes[ROUTER_1_2].get_addr(config.DOMAIN_PREFIX)
         assert dua2, 'Error: Expected DUA ({}) not found'.format(dua2)
-        assert dua2 != dua, 'Error: Expected Different DUA not found, same DUA {}'.format(dua2)
+        self.assertNotEqual(dua2, dua)
 
         # e) (repeated) Configure BBR_1 to respond with per remaining error status:
         #   - increase BBR seqno to trigger reregistration
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 1887536..ac974de 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
@@ -704,7 +704,7 @@
 
     def __check_renewing(self, id, parent_id, addr, expect_mlr_req=True, expect_mlr_req_proxied=False):
         """Check if MLR works that a node can renew it's registered MAs"""
-        assert self.pbbr_id == BBR_1
+        self.assertEqual(self.pbbr_id, BBR_1)
         self.flush_all()
         self.simulator.go(MLR_TIMEOUT + WAIT_REDUNDANCE)
 
@@ -748,7 +748,7 @@
     def __check_rereg_pbbr_change(self, id, parent_id, addr, expect_mlr_req=True, expect_mlr_req_proxied=False):
         """Check if MLR works that a node can do MLR reregistration when PBBR changes"""
         # Make BBR_2 to be Primary and expect MLR.req within REREG_DELAY
-        assert self.pbbr_id == BBR_1
+        self.assertEqual(self.pbbr_id, BBR_1)
 
         self.flush_all()
         self.nodes[BBR_1].disable_backbone_router()
diff --git a/tests/scripts/thread-cert/v1_2_test_parent_selection.py b/tests/scripts/thread-cert/v1_2_test_parent_selection.py
index 7b55b91..caf979f 100755
--- a/tests/scripts/thread-cert/v1_2_test_parent_selection.py
+++ b/tests/scripts/thread-cert/v1_2_test_parent_selection.py
@@ -139,8 +139,8 @@
         assert (parent_cmp), "Error: Expected parent response not found"
 
         # Known that link margin for link quality 3 is 80 and link quality 2 is 15
-        assert ((parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin -
-                 parent_cmp.get_mle_message_tlv(mle.LinkMargin).link_margin) > 20)
+        self.assertGreater((parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin -
+                            parent_cmp.get_mle_message_tlv(mle.LinkMargin).link_margin), 20)
 
         # Check Child Id Request
         messages = self.simulator.get_messages_sent_by(REED_1_2)
@@ -174,8 +174,9 @@
         parent_cmp = messages.next_mle_message(mle.CommandType.PARENT_RESPONSE)
         assert (parent_cmp), "Error: Expected parent response not found"
 
-        assert (parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin == parent_cmp.get_mle_message_tlv(
-            mle.LinkMargin).link_margin)
+        self.assertEqual(
+            parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin,
+            parent_cmp.get_mle_message_tlv(mle.LinkMargin).link_margin)
 
         # Check Child Id Request
         messages = self.simulator.get_messages_sent_by(ROUTER_1_2)
@@ -204,11 +205,13 @@
         parent_cmp = messages.next_mle_message(mle.CommandType.PARENT_RESPONSE)
         assert (parent_cmp), "Error: Expected parent response not found"
 
-        assert (parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin == parent_cmp.get_mle_message_tlv(
-            mle.LinkMargin).link_margin)
+        self.assertEqual(
+            parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin,
+            parent_cmp.get_mle_message_tlv(mle.LinkMargin).link_margin)
 
-        assert (parent_prefer.get_mle_message_tlv(mle.Connectivity).pp > parent_cmp.get_mle_message_tlv(
-            mle.Connectivity).pp)
+        self.assertGreater(
+            parent_prefer.get_mle_message_tlv(mle.Connectivity).pp,
+            parent_cmp.get_mle_message_tlv(mle.Connectivity).pp)
 
         # Check Child Id Request
         messages = self.simulator.get_messages_sent_by(REED_1_1)
@@ -239,12 +242,15 @@
         parent_cmp = messages.next_mle_message(mle.CommandType.PARENT_RESPONSE)
         assert (parent_cmp), "Error: Expected parent response not found"
 
-        assert (parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin == parent_cmp.get_mle_message_tlv(
-            mle.LinkMargin).link_margin)
-        assert (parent_prefer.get_mle_message_tlv(mle.Connectivity).pp == parent_cmp.get_mle_message_tlv(
-            mle.Connectivity).pp)
-        assert (parent_prefer.get_mle_message_tlv(mle.Connectivity).link_quality_3 > parent_cmp.get_mle_message_tlv(
-            mle.Connectivity).link_quality_3)
+        self.assertEqual(
+            parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin,
+            parent_cmp.get_mle_message_tlv(mle.LinkMargin).link_margin)
+        self.assertEqual(
+            parent_prefer.get_mle_message_tlv(mle.Connectivity).pp,
+            parent_cmp.get_mle_message_tlv(mle.Connectivity).pp)
+        self.assertGreater(
+            parent_prefer.get_mle_message_tlv(mle.Connectivity).link_quality_3,
+            parent_cmp.get_mle_message_tlv(mle.Connectivity).link_quality_3)
 
         # Check Child Id Request
         messages = self.simulator.get_messages_sent_by(MED_1_1)
@@ -272,14 +278,18 @@
         parent_cmp = messages.next_mle_message(mle.CommandType.PARENT_RESPONSE)
         assert (parent_cmp), "Error: Expected parent response not found"
 
-        assert (parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin == parent_cmp.get_mle_message_tlv(
-            mle.LinkMargin).link_margin)
-        assert (parent_prefer.get_mle_message_tlv(mle.Connectivity).pp == parent_cmp.get_mle_message_tlv(
-            mle.Connectivity).pp)
-        assert (parent_prefer.get_mle_message_tlv(mle.Connectivity).link_quality_3 == parent_cmp.get_mle_message_tlv(
-            mle.Connectivity).link_quality_3)
-        assert (parent_prefer.get_mle_message_tlv(mle.Version).version > parent_cmp.get_mle_message_tlv(
-            mle.Version).version)
+        self.assertEqual(
+            parent_prefer.get_mle_message_tlv(mle.LinkMargin).link_margin,
+            parent_cmp.get_mle_message_tlv(mle.LinkMargin).link_margin)
+        self.assertEqual(
+            parent_prefer.get_mle_message_tlv(mle.Connectivity).pp,
+            parent_cmp.get_mle_message_tlv(mle.Connectivity).pp)
+        self.assertEqual(
+            parent_prefer.get_mle_message_tlv(mle.Connectivity).link_quality_3,
+            parent_cmp.get_mle_message_tlv(mle.Connectivity).link_quality_3)
+        self.assertGreater(
+            parent_prefer.get_mle_message_tlv(mle.Version).version,
+            parent_cmp.get_mle_message_tlv(mle.Version).version)
 
         # Check Child Id Request
         messages = self.simulator.get_messages_sent_by(MED_1_2)
diff --git a/tests/toranj/build.sh b/tests/toranj/build.sh
index 0c9a698..4b91cd4 100755
--- a/tests/toranj/build.sh
+++ b/tests/toranj/build.sh
@@ -113,8 +113,9 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_OPERATIONAL_DATASET_AUTO_INIT=ON -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
+            -DOT_BORDER_ROUTING=OFF \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
         ninja || die
@@ -126,8 +127,9 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_15_4=ON -DOT_TREL=OFF -DOT_OPERATIONAL_DATASET_AUTO_INIT=ON \
+            -DOT_BORDER_ROUTING=OFF \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
@@ -141,8 +143,9 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_15_4=OFF -DOT_TREL=ON -DOT_OPERATIONAL_DATASET_AUTO_INIT=ON \
+            -DOT_BORDER_ROUTING=OFF \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
@@ -156,8 +159,9 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_15_4=ON -DOT_TREL=ON -DOT_OPERATIONAL_DATASET_AUTO_INIT=ON \
+            -DOT_BORDER_ROUTING=OFF \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
@@ -171,7 +175,7 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
@@ -184,7 +188,7 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
             -DOT_15_4=ON -DOT_TREL=OFF \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
@@ -199,7 +203,7 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
             -DOT_15_4=OFF -DOT_TREL=ON \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
@@ -214,7 +218,7 @@
         echo "==================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
             -DOT_15_4=ON -DOT_TREL=ON \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
@@ -229,7 +233,7 @@
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=OFF -DOT_APP_NCP=OFF -DOT_APP_RCP=ON \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=OFF -DOT_APP_RCP=ON \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
@@ -242,7 +246,7 @@
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=posix -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-posix.h \
             "${top_srcdir}" || die
@@ -255,7 +259,7 @@
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=posix -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_15_4=ON -DOT_TREL=OFF \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-posix.h \
@@ -269,7 +273,7 @@
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=posix -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_15_4=OFF -DOT_TREL=ON \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-posix.h \
@@ -283,7 +287,7 @@
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=posix -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=OFF \
             -DOT_15_4=ON -DOT_TREL=ON \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-posix.h \
@@ -297,7 +301,7 @@
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
         cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
-            -DOT_THREAD_VERSION=1.3.1 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=ON \
+            -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=ON \
             -DOT_PLATFORM_KEY_REF=${ot_plat_key_ref} \
             -DOT_PROJECT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py
index 6bda083..8f278f4 100644
--- a/tests/toranj/cli/cli.py
+++ b/tests/toranj/cli/cli.py
@@ -223,6 +223,17 @@
     def set_channel(self, channel):
         self._cli_no_output('channel', channel)
 
+    def get_csl_config(self):
+        outputs = self.cli('csl')
+        result = {}
+        for line in outputs:
+            fields = line.split(':')
+            result[fields[0].strip()] = fields[1].strip()
+        return result
+
+    def set_csl_period(self, period):
+        self._cli_no_output('csl period', period)
+
     def get_ext_addr(self):
         return self._cli_single_output('extaddr')
 
@@ -378,22 +389,32 @@
     def set_vendor_sw_version(self, version):
         return self._cli_no_output('vendor swversion', version)
 
+    def get_vendor_app_url(self):
+        return self._cli_single_output('vendor appurl')
+
+    def set_vendor_app_url(self, url):
+        return self._cli_no_output('vendor appurl', url)
+
     #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # netdata
 
-    def get_netdata(self):
-        outputs = self.cli('netdata show')
+    def get_netdata(self, rloc16=None):
+        outputs = self.cli('netdata show', rloc16)
         outputs = [line.strip() for line in outputs]
         routes_index = outputs.index('Routes:')
         services_index = outputs.index('Services:')
-        contexts_index = outputs.index('Contexts:')
-        commissioning_index = outputs.index('Commissioning:')
+        if rloc16 is None:
+            contexts_index = outputs.index('Contexts:')
+            commissioning_index = outputs.index('Commissioning:')
         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:commissioning_index]
-        result['commissioning'] = outputs[commissioning_index + 1:]
+        if rloc16 is None:
+            result['services'] = outputs[services_index + 1:contexts_index]
+            result['contexts'] = outputs[contexts_index + 1:commissioning_index]
+            result['commissioning'] = outputs[commissioning_index + 1:]
+        else:
+            result['services'] = outputs[services_index + 1:]
 
         return result
 
@@ -466,6 +487,24 @@
         return self._cli_single_output('mleadvimax')
 
     #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # Border Agent
+
+    def ba_get_state(self):
+        return self._cli_single_output('ba state')
+
+    def ba_get_port(self):
+        return self._cli_single_output('ba port')
+
+    def ba_is_ephemeral_key_active(self):
+        return self._cli_single_output('ba ephemeralkey')
+
+    def ba_set_ephemeral_key(self, keystring, timeout=None, port=None):
+        self._cli_no_output('ba ephemeralkey set', keystring, timeout, port)
+
+    def ba_clear_ephemeral_key(self):
+        self._cli_no_output('ba ephemeralkey clear')
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # UDP
 
     def udp_open(self):
@@ -618,6 +657,12 @@
     def srp_server_disable(self):
         self._cli_no_output('srp server disable')
 
+    def srp_server_auto_enable(self):
+        self._cli_no_output('srp server auto enable')
+
+    def srp_server_auto_disable(self):
+        self._cli_no_output('srp server auto disable')
+
     def srp_server_set_lease(self, min_lease, max_lease, min_key_lease, max_key_lease):
         self._cli_no_output('srp server lease', min_lease, max_lease, min_key_lease, max_key_lease)
 
@@ -736,6 +781,18 @@
     def br_get_state(self):
         return self._cli_single_output('br state')
 
+    def br_get_favored_omrprefix(self):
+        return self._cli_single_output('br omrprefix favored')
+
+    def br_get_local_omrprefix(self):
+        return self._cli_single_output('br omrprefix local')
+
+    def br_get_favored_onlinkprefix(self):
+        return self._cli_single_output('br onlinkprefix favored')
+
+    def br_get_local_onlinkprefix(self):
+        return self._cli_single_output('br onlinkprefix local')
+
     def br_get_routeprf(self):
         return self._cli_single_output('br routeprf')
 
@@ -745,6 +802,9 @@
     def br_clear_routeprf(self):
         self._cli_no_output('br routeprf clear')
 
+    def br_get_routers(self):
+        return self.cli('br routers')
+
     # ------------------------------------------------------------------------------------------------------------------
     # Helper methods
 
@@ -926,7 +986,8 @@
         except VerifyError as e:
             if time.time() - start_time > wait_time:
                 print('Took too long to pass the condition ({}>{} sec)'.format(time.time() - start_time, wait_time))
-                print(e.message)
+                if hasattr(e, 'message'):
+                    print(e.message)
                 raise e
         except BaseException:
             raise
diff --git a/tests/toranj/cli/test-011-network-data-timeout.py b/tests/toranj/cli/test-011-network-data-timeout.py
index a3a4086..bf52487 100755
--- a/tests/toranj/cli/test-011-network-data-timeout.py
+++ b/tests/toranj/cli/test-011-network-data-timeout.py
@@ -89,11 +89,6 @@
 # -----------------------------------------------------------------------------------------------------------------------
 # 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')
@@ -104,6 +99,9 @@
 r2.add_prefix('fd00:abba::/64', 'paros', 'med')
 c2.add_prefix('fd00:abba::/64', 'paros', 'low')
 
+r1.add_route('fd00:cafe::/64', 's', 'med')
+r2.add_route('fd00:cafe::/64', 's', 'med')
+
 r1.register_netdata()
 r2.register_netdata()
 c2.register_netdata()
@@ -112,12 +110,19 @@
 def check_netdata_on_all_nodes():
     for node in nodes:
         netdata = node.get_netdata()
-        prefixes = netdata['prefixes']
-        verify(len(prefixes) == 6)
+        verify(len(netdata['prefixes']) == 6)
+        verify(len(netdata['routes']) == 2)
 
 
 verify_within(check_netdata_on_all_nodes, 10)
 
+# Check netdata filtering for r1 entries only.
+
+r1_rloc = int(r1.get_rloc16(), 16)
+netdata = r1.get_netdata(r1_rloc)
+verify(len(netdata['prefixes']) == 2)
+verify(len(netdata['routes']) == 1)
+
 # Remove `r2`. This should trigger all the prefixes added by it or its
 # child to timeout and be removed.
 
@@ -127,8 +132,11 @@
 
 def check_netdata_on_r1():
     netdata = r1.get_netdata()
-    prefixes = netdata['prefixes']
-    verify(len(prefixes) == 2)
+    verify(len(netdata['prefixes']) == 2)
+    verify(len(netdata['routes']) == 1)
+    netdata = r1.get_netdata(r1_rloc)
+    verify(len(netdata['prefixes']) == 2)
+    verify(len(netdata['routes']) == 1)
 
 
 verify_within(check_netdata_on_r1, 120)
diff --git a/tests/toranj/cli/test-020-net-diag-vendor-info.py b/tests/toranj/cli/test-020-net-diag-vendor-info.py
index f923dde..463854a 100755
--- a/tests/toranj/cli/test-020-net-diag-vendor-info.py
+++ b/tests/toranj/cli/test-020-net-diag-vendor-info.py
@@ -69,17 +69,20 @@
 VENDOR_SW_VERSION_TLV = 27
 THREAD_STACK_VERSION_TLV = 28
 MLE_COUNTERS_TLV = 34
+VENDOR_APP_URL = 35
 
 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 # 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')
+r1.set_vendor_sw_version('ot-1.4')
+r1.set_vendor_app_url('https://example.com/vendor-app')
 
 verify(r1.get_vendor_name() == 'nest')
 verify(r1.get_vendor_model() == 'marble')
-verify(r1.get_vendor_sw_version() == 'ot-1.3.1')
+verify(r1.get_vendor_sw_version() == 'ot-1.4')
+verify(r1.get_vendor_app_url() == 'https://example.com/vendor-app')
 
 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 # Check invalid names (too long)
@@ -126,6 +129,8 @@
 
 verify(errored)
 
+r2.set_vendor_app_url("https://example.com/vendor-app")
+
 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 # Perform net diag query
 
@@ -153,6 +158,13 @@
 verify(result[1].startswith("Vendor SW Version:"))
 verify(result[1].split(':')[1].strip() == r1.get_vendor_sw_version())
 
+# Get vendor app URL (TLV 35)
+
+result = r2.cli('networkdiagnostic get', r1_rloc, VENDOR_APP_URL)
+verify(len(result) == 2)
+verify(result[1].startswith("Vendor App URL:"))
+verify(result[1].split(':', 1)[1].strip() == r1.get_vendor_app_url())
+
 # Get thread stack version (TLV 30)
 
 result = r2.cli('networkdiagnostic get', r1_rloc, THREAD_STACK_VERSION_TLV)
@@ -163,8 +175,8 @@
 # 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)
+                THREAD_STACK_VERSION_TLV, VENDOR_APP_URL)
+verify(len(result) == 6)
 for line in result[1:]:
     if line.startswith("Vendor Name:"):
         verify(line.split(':')[1].strip() == r2.get_vendor_name())
@@ -172,6 +184,8 @@
         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("Vendor App URL:"):
+        verify(line.split(':', 1)[1].strip() == r2.get_vendor_app_url())
     elif line.startswith("Thread Stack Version:"):
         verify(r2.get_version().startswith(line.split(':', 1)[1].strip()))
     else:
diff --git a/tests/toranj/cli/test-028-border-agent-ephemeral-key.py b/tests/toranj/cli/test-028-border-agent-ephemeral-key.py
new file mode 100755
index 0000000..47a02ee
--- /dev/null
+++ b/tests/toranj/cli/test-028-border-agent-ephemeral-key.py
@@ -0,0 +1,96 @@
+#!/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:
+#
+# Validate changes to `IntervalMax` for MLE Advertisement Trickle Timer based on number of
+# router neighbors of the device.
+#
+
+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)
+
+leader = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+leader.form('ba-ephemeral')
+
+verify(leader.get_state() == 'leader')
+
+verify(leader.ba_is_ephemeral_key_active() == 'inactive')
+
+port = int(leader.ba_get_port())
+
+leader.ba_set_ephemeral_key('password', 10000, 1234)
+
+time.sleep(0.1)
+
+verify(leader.ba_is_ephemeral_key_active() == 'active')
+verify(int(leader.ba_get_port()) == 1234)
+
+leader.ba_set_ephemeral_key('password2', 200, 45678)
+
+time.sleep(0.100 / speedup)
+verify(leader.ba_is_ephemeral_key_active() == 'active')
+verify(int(leader.ba_get_port()) == 45678)
+
+time.sleep(0.150 / speedup)
+verify(leader.ba_is_ephemeral_key_active() == 'inactive')
+verify(int(leader.ba_get_port()) == port)
+
+leader.ba_set_ephemeral_key('newkey')
+verify(leader.ba_is_ephemeral_key_active() == 'active')
+
+time.sleep(0.1)
+verify(leader.ba_is_ephemeral_key_active() == 'active')
+
+leader.ba_clear_ephemeral_key()
+verify(leader.ba_is_ephemeral_key_active() == 'inactive')
+verify(int(leader.ba_get_port()) == port)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-400-srp-client-server.py b/tests/toranj/cli/test-400-srp-client-server.py
index e65d6ff..0e3edb2 100755
--- a/tests/toranj/cli/test-400-srp-client-server.py
+++ b/tests/toranj/cli/test-400-srp-client-server.py
@@ -121,6 +121,22 @@
 verify(service['host'] == 'host')
 verify(service['addresses'] == ['fd00:0:0:0:0:0:0:cafe'])
 
+# Check the client address is added in EID cache table (snoop).
+
+cache_table = server.get_eidcache()
+client_rloc = int(client.get_rloc16(), 16)
+found_entry = False
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if (fields[0] == 'fd00:0:0:0:0:0:0:cafe'):
+        verify(int(fields[1], 16) == client_rloc)
+        verify(fields[2] == 'snoop')
+        found_entry = True
+        break
+
+verify(found_entry)
+
 # -----------------------------------------------------------------------------------------------------------------------
 # Test finished
 
diff --git a/tests/toranj/cli/test-401-srp-server-address-cache-snoop.py b/tests/toranj/cli/test-401-srp-server-address-cache-snoop.py
new file mode 100755
index 0000000..2462d0a
--- /dev/null
+++ b/tests/toranj/cli/test-401-srp-server-address-cache-snoop.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2024, 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:
+#
+# Validate registered host addresses are properly added in address cache table
+# on SRP server.
+#
+#    r1 (leader) ----- r2 ------ r3
+#     /  |                        \
+#    /   |                         \
+#   fed1  sed1                     sed2
+#
+
+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()
+fed1 = cli.Node()
+sed1 = cli.Node()
+sed2 = cli.Node()
+
+WAIT_TIME = 5
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(fed1)
+r1.allowlist_node(sed1)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+r2.allowlist_node(sed2)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(sed2)
+
+fed1.allowlist_node(r1)
+sed1.allowlist_node(r1)
+
+sed2.allowlist_node(r3)
+
+r1.form('srp-snoop')
+r2.join(r1)
+r3.join(r2)
+fed1.join(r1, cli.JOIN_TYPE_REED)
+sed1.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+sed2.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+sed1.set_pollperiod(500)
+sed2.set_pollperiod(500)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r2.get_state() == 'router')
+verify(sed1.get_state() == 'child')
+verify(sed2.get_state() == 'child')
+verify(fed1.get_state() == 'child')
+
+r2_rloc = int(r2.get_rloc16(), 16)
+r3_rloc = int(r3.get_rloc16(), 16)
+fed1_rloc = int(fed1.get_rloc16(), 16)
+
+# Start server and client and register single service
+r1.srp_server_enable()
+
+r2.srp_client_enable_auto_start_mode()
+r2.srp_client_set_host_name('r2')
+r2.srp_client_set_host_address('fd00::2')
+r2.srp_client_add_service('srv2', '_test._udp', 222, 0, 0)
+
+
+def check_server_has_host(num_hosts):
+    verify(len(r1.srp_server_get_hosts()) >= num_hosts)
+
+
+verify_within(check_server_has_host, WAIT_TIME, arg=1)
+
+cache_table = r1.get_eidcache()
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if (fields[0] == 'fd00:0:0:0:0:0:0:2'):
+        verify(int(fields[1], 16) == r2_rloc)
+        verify(fields[2] == 'snoop')
+        break
+else:
+    verify(False)  # did not find cache entry
+
+# Register from r3 which one hop away from r1 server.
+
+r3.srp_client_enable_auto_start_mode()
+r3.srp_client_set_host_name('r3')
+r3.srp_client_set_host_address('fd00::3')
+r3.srp_client_add_service('srv3', '_test._udp', 333, 0, 0)
+
+verify_within(check_server_has_host, WAIT_TIME, arg=2)
+
+cache_table = r1.get_eidcache()
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if (fields[0] == 'fd00:0:0:0:0:0:0:3'):
+        verify(int(fields[1], 16) == r3_rloc)
+        verify(fields[2] == 'snoop')
+        break
+else:
+    verify(False)  # did not find cache entry
+
+# Register from sed2 which child of r3. The cache table should
+# use the `r3` as the parent of sed2.
+
+sed2.srp_client_enable_auto_start_mode()
+sed2.srp_client_set_host_name('sed2')
+sed2.srp_client_set_host_address('fd00::1:3')
+sed2.srp_client_add_service('srv4', '_test._udp', 333, 0, 0)
+
+verify_within(check_server_has_host, WAIT_TIME, arg=3)
+
+cache_table = r1.get_eidcache()
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if (fields[0] == 'fd00:0:0:0:0:0:1:3'):
+        verify(int(fields[1], 16) == r3_rloc)
+        verify(fields[2] == 'snoop')
+        break
+else:
+    verify(False)  # did not find cache entry
+
+# Register from fed1 which child of server (r1) itself. The cache table should
+# be properly updated
+
+fed1.srp_client_enable_auto_start_mode()
+fed1.srp_client_set_host_name('fed1')
+fed1.srp_client_set_host_address('fd00::2:3')
+fed1.srp_client_add_service('srv5', '_test._udp', 555, 0, 0)
+
+verify_within(check_server_has_host, WAIT_TIME, arg=4)
+
+cache_table = r1.get_eidcache()
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if (fields[0] == 'fd00:0:0:0:0:0:2:3'):
+        verify(int(fields[1], 16) == fed1_rloc)
+        verify(fields[2] == 'snoop')
+        break
+else:
+    verify(False)  # did not find cache entry
+
+# Register from sed1 which is a sleepy child of server (r1).
+# The cache table should not be updated for sleepy child.
+
+sed1.srp_client_enable_auto_start_mode()
+sed1.srp_client_set_host_name('sed1')
+sed1.srp_client_set_host_address('fd00::3:4')
+sed1.srp_client_add_service('srv5', '_test._udp', 555, 0, 0)
+
+verify_within(check_server_has_host, WAIT_TIME, arg=4)
+
+cache_table = r1.get_eidcache()
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    verify(fields[0] != 'fd00:0:0:0:0:0:3:4')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-500-two-brs-two-networks.py b/tests/toranj/cli/test-500-two-brs-two-networks.py
new file mode 100755
index 0000000..12c61a7
--- /dev/null
+++ b/tests/toranj/cli/test-500-two-brs-two-networks.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2024, 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:
+#
+# Two BRs on two different Thread networks.
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 60
+cli.Node.set_time_speedup_factor(speedup)
+
+br1 = cli.Node()
+br2 = cli.Node()
+
+WAIT_TIME = 5
+IF_INDEX = 1
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test implementation
+
+# Start first BR with its own Thread network
+
+br1.form('net1')
+verify(br1.get_state() == 'leader')
+
+br1.br_init(IF_INDEX, 1)
+br1.br_enable()
+
+time.sleep(1)
+verify(br1.br_get_state() == 'running')
+
+br1_local_omr = br1.br_get_local_omrprefix()
+br1_favored_omr = br1.br_get_favored_omrprefix().split()[0]
+verify(br1_local_omr == br1_favored_omr)
+
+br1_local_onlink = br1.br_get_local_onlinkprefix()
+br1_favored_onlink = br1.br_get_favored_onlinkprefix().split()[0]
+verify(br1_local_onlink == br1_favored_onlink)
+
+# Start second BR with its own Thread network.
+
+br2.form('net2')
+verify(br2.get_state() == 'leader')
+
+br2.br_init(IF_INDEX, 1)
+br2.br_enable()
+
+time.sleep(1)
+verify(br2.br_get_state() == 'running')
+
+br2_local_omr = br2.br_get_local_omrprefix()
+br2_favored_omr = br2.br_get_favored_omrprefix().split()[0]
+verify(br2_local_omr == br2_favored_omr)
+
+br2_local_onlink = br2.br_get_local_onlinkprefix()
+br2_favored_onlink = br2.br_get_favored_onlinkprefix().split()[0]
+verify(br2_local_onlink != br2_favored_onlink)
+
+# BR2 should see and favor the on-link prefix already advertised by BR1.
+
+verify(br1_favored_onlink == br2_favored_onlink)
+
+br1_routers = br1.br_get_routers()
+br2_routers = br2.br_get_routers()
+
+verify(len(br1_routers) > 0)
+verify(len(br2_routers) > 0)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-501-multi-br-failure-recovery.py b/tests/toranj/cli/test-501-multi-br-failure-recovery.py
new file mode 100755
index 0000000..ecf41b9
--- /dev/null
+++ b/tests/toranj/cli/test-501-multi-br-failure-recovery.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2024, 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 with two BRs, none of them acting as leader. Removing BR1 ensuring BR2 taking over.
+#
+#      ________________
+#     /                \
+#   br1 --- leader --- br2
+#   /         / \        \
+#  c1       c2  c3       c4
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 60
+cli.Node.set_time_speedup_factor(speedup)
+
+leader = cli.Node()
+br1 = cli.Node()
+br2 = cli.Node()
+c1 = cli.Node()
+c2 = cli.Node()
+c3 = cli.Node()
+c4 = cli.Node()
+
+IF_INDEX = 1
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+leader.allowlist_node(br1)
+leader.allowlist_node(br2)
+leader.allowlist_node(c2)
+leader.allowlist_node(c3)
+
+br1.allowlist_node(leader)
+br1.allowlist_node(br2)
+br1.allowlist_node(c1)
+
+br2.allowlist_node(leader)
+br2.allowlist_node(br1)
+br2.allowlist_node(c4)
+
+c1.allowlist_node(br1)
+
+c2.allowlist_node(leader)
+c3.allowlist_node(leader)
+
+c4.allowlist_node(br2)
+
+leader.form("multi-br")
+br1.join(leader)
+br2.join(leader)
+c1.join(leader, cli.JOIN_TYPE_END_DEVICE)
+c2.join(leader, cli.JOIN_TYPE_END_DEVICE)
+c3.join(leader, cli.JOIN_TYPE_END_DEVICE)
+c4.join(leader, cli.JOIN_TYPE_END_DEVICE)
+
+verify(leader.get_state() == 'leader')
+verify(br1.get_state() == 'router')
+verify(br2.get_state() == 'router')
+verify(c1.get_state() == 'child')
+verify(c2.get_state() == 'child')
+verify(c3.get_state() == 'child')
+verify(c4.get_state() == 'child')
+
+nodes_non_br = [leader, c1, c2, c3, c4]
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test implementation
+
+# Start the first BR
+
+br1.srp_server_set_addr_mode('unicast')
+br1.srp_server_auto_enable()
+
+br1.br_init(IF_INDEX, 1)
+br1.br_enable()
+
+time.sleep(1)
+verify(br1.br_get_state() == 'running')
+
+br1_local_omr = br1.br_get_local_omrprefix()
+br1_favored_omr = br1.br_get_favored_omrprefix().split()[0]
+verify(br1_local_omr == br1_favored_omr)
+
+br1_local_onlink = br1.br_get_local_onlinkprefix()
+br1_favored_onlink = br1.br_get_favored_onlinkprefix().split()[0]
+verify(br1_local_onlink == br1_favored_onlink)
+
+# Start the second BR
+
+br2.br_init(IF_INDEX, 1)
+br2.br_enable()
+
+time.sleep(1)
+verify(br2.br_get_state() == 'running')
+
+br2_local_omr = br2.br_get_local_omrprefix()
+br2_favored_omr = br2.br_get_favored_omrprefix().split()[0]
+verify(br2_favored_omr == br1_favored_omr)
+
+br2_favored_onlink = br2.br_get_favored_onlinkprefix().split()[0]
+verify(br2_favored_onlink == br1_favored_onlink)
+
+verify(br1.srp_server_get_state() == 'running')
+verify(br2.srp_server_get_state() == 'disabled')
+
+# Register SRP services on all nodes
+
+for node in nodes_non_br:
+    verify(node.srp_client_get_auto_start_mode() == 'Enabled')
+    node.srp_client_set_host_name('host' + str(node.index))
+    node.srp_client_enable_auto_host_address()
+    node.srp_client_add_service('srv' + str(node.index), '_test._udp', 777, 0, 0)
+
+time.sleep(1)
+
+hosts = br1.srp_server_get_hosts()
+verify(len(hosts) == len(nodes_non_br))
+
+services = br1.srp_server_get_services()
+verify(len(services) == len(nodes_non_br))
+
+# Ensure that all registered addresses are derived from BR1 OMR.
+
+for host in hosts:
+    verify(host['addresses'][0].startswith(br1_local_omr[:-4]))
+
+# Start SRP server on BR2
+
+br2.srp_server_set_addr_mode('unicast')
+br2.srp_server_auto_enable()
+
+time.sleep(1)
+
+verify(br2.srp_server_get_state() == 'running')
+
+# De-activate BR1
+
+br1.br_disable()
+br1.thread_stop()
+br1.interface_down()
+del br1
+
+c1.allowlist_node(br2)
+br2.allowlist_node(c1)
+
+# Wait long enough for BR2 to take over
+
+time.sleep(5)
+
+# Validate that everything is registered with BR2
+
+hosts = br2.srp_server_get_hosts()
+verify(len(hosts) == len(nodes_non_br))
+
+services = br2.srp_server_get_services()
+verify(len(services) == len(nodes_non_br))
+
+# Ensure that all registered addresses are now derived from BR2
+# OMR prefix.
+
+for host in hosts:
+    verify(host['addresses'][0].startswith(br2_local_omr[:-4]))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-502-multi-br-leader-failure-recovery.py b/tests/toranj/cli/test-502-multi-br-leader-failure-recovery.py
new file mode 100755
index 0000000..71d2ebc
--- /dev/null
+++ b/tests/toranj/cli/test-502-multi-br-leader-failure-recovery.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2024, 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 with two BRs, BR1 acting as leader. Removing BR1 ensuring BR2 taking over.
+#
+#      ________________
+#     /                \
+#   br1 --- router --- br2
+#   /        / \        \
+#  c1       c2  c3      c4
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 60
+cli.Node.set_time_speedup_factor(speedup)
+
+br1 = cli.Node()
+br2 = cli.Node()
+router = cli.Node()
+c1 = cli.Node()
+c2 = cli.Node()
+c3 = cli.Node()
+c4 = cli.Node()
+
+IF_INDEX = 1
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+br1.allowlist_node(router)
+br1.allowlist_node(br2)
+br1.allowlist_node(c1)
+
+br2.allowlist_node(router)
+br2.allowlist_node(br1)
+br2.allowlist_node(c4)
+
+router.allowlist_node(br1)
+router.allowlist_node(br2)
+router.allowlist_node(c2)
+router.allowlist_node(c3)
+
+c1.allowlist_node(br1)
+
+c2.allowlist_node(router)
+c3.allowlist_node(router)
+
+c4.allowlist_node(br2)
+
+br1.form("multi-br")
+br2.join(br1)
+router.join(br1)
+c1.join(br1, cli.JOIN_TYPE_END_DEVICE)
+c2.join(br1, cli.JOIN_TYPE_END_DEVICE)
+c3.join(br1, cli.JOIN_TYPE_END_DEVICE)
+c4.join(br1, cli.JOIN_TYPE_END_DEVICE)
+
+verify(br1.get_state() == 'leader')
+verify(br2.get_state() == 'router')
+verify(router.get_state() == 'router')
+verify(c1.get_state() == 'child')
+verify(c2.get_state() == 'child')
+verify(c3.get_state() == 'child')
+verify(c4.get_state() == 'child')
+
+nodes_non_br = [router, c1, c2, c3, c4]
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test implementation
+
+# Start the first BR
+
+br1.srp_server_set_addr_mode('unicast')
+br1.srp_server_auto_enable()
+
+br1.br_init(IF_INDEX, 1)
+br1.br_enable()
+
+time.sleep(1)
+verify(br1.br_get_state() == 'running')
+
+br1_local_omr = br1.br_get_local_omrprefix()
+br1_favored_omr = br1.br_get_favored_omrprefix().split()[0]
+verify(br1_local_omr == br1_favored_omr)
+
+br1_local_onlink = br1.br_get_local_onlinkprefix()
+br1_favored_onlink = br1.br_get_favored_onlinkprefix().split()[0]
+verify(br1_local_onlink == br1_favored_onlink)
+
+# Start the second BR
+
+br2.br_init(IF_INDEX, 1)
+br2.br_enable()
+
+time.sleep(1)
+verify(br2.br_get_state() == 'running')
+
+br2_local_omr = br2.br_get_local_omrprefix()
+br2_favored_omr = br2.br_get_favored_omrprefix().split()[0]
+verify(br2_favored_omr == br1_favored_omr)
+
+br2_favored_onlink = br2.br_get_favored_onlinkprefix().split()[0]
+verify(br2_favored_onlink == br1_favored_onlink)
+
+verify(br1.srp_server_get_state() == 'running')
+verify(br2.srp_server_get_state() == 'disabled')
+
+# Register SRP services on all nodes
+
+for node in nodes_non_br:
+    verify(node.srp_client_get_auto_start_mode() == 'Enabled')
+    node.srp_client_set_host_name('host' + str(node.index))
+    node.srp_client_enable_auto_host_address()
+    node.srp_client_add_service('srv' + str(node.index), '_test._udp', 777, 0, 0)
+
+time.sleep(1)
+
+hosts = br1.srp_server_get_hosts()
+verify(len(hosts) == len(nodes_non_br))
+
+services = br1.srp_server_get_services()
+verify(len(services) == len(nodes_non_br))
+
+# Ensure that all registered addresses are derived from BR1 OMR.
+
+for host in hosts:
+    verify(host['addresses'][0].startswith(br1_local_omr[:-4]))
+
+# Start SRP server on BR2
+
+br2.srp_server_set_addr_mode('unicast')
+br2.srp_server_auto_enable()
+
+time.sleep(1)
+
+verify(br2.srp_server_get_state() == 'running')
+
+# De-activate BR1
+
+br1.br_disable()
+br1.thread_stop()
+br1.interface_down()
+del br1
+
+c1.allowlist_node(br2)
+br2.allowlist_node(c1)
+
+# Wait long enough for BR2 to take over
+
+time.sleep(5)
+
+# Validate that everything is registered with BR2
+
+hosts = br2.srp_server_get_hosts()
+verify(len(hosts) == len(nodes_non_br))
+
+services = br2.srp_server_get_services()
+verify(len(services) == len(nodes_non_br))
+
+# Ensure that all registered addresses are now derived from BR2
+# OMR prefix.
+
+for host in hosts:
+    verify(host['addresses'][0].startswith(br2_local_omr[:-4]))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# 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
index 0ddf358..596cfdc 100755
--- a/tests/toranj/cli/test-602-channel-manager-channel-select.py
+++ b/tests/toranj/cli/test-602-channel-manager-channel-select.py
@@ -66,6 +66,10 @@
     verify(int(node.get_channel()) == channel)
 
 
+delay = int(node.cli('channel manager delay')[0])
+# add kRequestStartJitterInterval=10000ms to expected channel manager delay
+delay += 10 / speedup
+
 check_channel()
 
 all_channels_mask = int('0x7fff800', 0)
@@ -99,7 +103,7 @@
 result = cli.Node.parse_list(node.cli('channel manager'))
 verify(result['channel'] == '11')
 channel = 11
-verify_within(check_channel, 2)
+verify_within(check_channel, delay)
 
 # Set channels 12-15 as favorable and request a channel select, verify
 # that channel is switched to 12.
@@ -112,13 +116,13 @@
 
 channel = 25
 node.cli('channel manager change', channel)
-verify_within(check_channel, 2)
+verify_within(check_channel, delay)
 
 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)
+verify_within(check_channel, delay)
 
 # Set channels 15-17 as favorables and request a channel select,
 # verify that channel is switched to 11.
@@ -129,7 +133,7 @@
 
 channel = 25
 node.cli('channel manager change', channel)
-verify_within(check_channel, 2)
+verify_within(check_channel, delay)
 
 node.cli('channel manager favored', chan_15_to_17_mask)
 
@@ -137,7 +141,7 @@
 result = cli.Node.parse_list(node.cli('channel manager'))
 verify(result['channel'] == '11')
 channel = 11
-verify_within(check_channel, 2)
+verify_within(check_channel, delay)
 
 # Set channels 12-15 as favorable and request a channel select, verify
 # that channel is not switched.
@@ -145,10 +149,11 @@
 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)
+verify_within(check_channel, delay)
 
 # Starting from channel 12 and issuing a channel select (which would
 # pick 11 as best channel). However, since quality difference between
@@ -157,14 +162,60 @@
 
 channel = 12
 node.cli('channel manager change', channel)
-verify_within(check_channel, 2)
+verify_within(check_channel, delay)
 
 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)
+verify_within(check_channel, delay)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Auto Select
+
+# Set channel manager cca failure rate threshold to 0
+# as we cannot control cca success in simulation
+node.cli('channel manager threshold 0')
+
+# Set short channel selection interval to speedup
+interval = 30
+node.cli(f'channel manager interval {interval}')
+
+# Set channels 15-17 as favorable and request a channel select, verify
+# that channel is switched to 11.
+
+channel = 25
+node.cli('channel manager change', channel)
+verify_within(check_channel, delay)
+node.cli('channel manager favored', chan_15_to_17_mask)
+
+# Active auto channel selection
+node.cli('channel manager auto 1')
+
+channel = 11
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['auto'] == '1')
+verify(result['channel'] == str(channel))
+
+verify_within(check_channel, delay)
+
+# while channel selection timer is running change to channel 25,
+# set channels 12-15 as favorable, wait for auto channel selection
+# and verify that channel is switched to 12.
+
+node.cli('channel manager favored', chan_12_to_15_mask)
+channel = 25
+node.cli('channel manager change', channel)
+
+# wait for timeout of auto selection timer
+time.sleep(2 * interval / speedup)
+
+channel = 12
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['channel'] == str(channel))
+
+verify_within(check_channel, delay)
 
 # -----------------------------------------------------------------------------------------------------------------------
 # Test finished
diff --git a/tests/toranj/openthread-core-toranj-config-posix.h b/tests/toranj/openthread-core-toranj-config-posix.h
index 60e8677..2dc34b0 100644
--- a/tests/toranj/openthread-core-toranj-config-posix.h
+++ b/tests/toranj/openthread-core-toranj-config-posix.h
@@ -39,6 +39,24 @@
 
 #define OPENTHREAD_CONFIG_PLATFORM_INFO "POSIX-toranj"
 
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE 1
+
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE 1
+
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE 1
+
+#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 0
+
+#ifdef __linux__
+#define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1
+#endif
+
+#ifndef OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
+#define OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE 0
+#endif
+
+#define OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE 1
+
 #define OPENTHREAD_CONFIG_LOG_OUTPUT OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED
 
 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1
diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h
index bc933d1..6bf449e 100644
--- a/tests/toranj/openthread-core-toranj-config-simulation.h
+++ b/tests/toranj/openthread-core-toranj-config-simulation.h
@@ -42,13 +42,24 @@
 #define OPENTHREAD_CONFIG_PLATFORM_INFO "SIMULATION-RCP-toranj"
 #else
 #define OPENTHREAD_CONFIG_PLATFORM_INFO "SIMULATION-toranj"
-
 #endif
 
 #define OPENTHREAD_CONFIG_COAP_API_ENABLE 1
 
 #define OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE 1
 
+#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 1
+
+#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION 1
+
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE 1
+
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_PUBLIC_API_ENABLE 1
+
+#define OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF 0
+
+#define OPENTHREAD_SIMULATION_MDNS_SOCKET_IMPLEMENT_POSIX 1
+
 #define OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE 0
 
 #define OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE 1
@@ -61,14 +72,14 @@
 
 #define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 1
 
-#define OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE 1
-
-#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 1
+#define OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
 #define OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE 1
 
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1
 
+#define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1
+
 #define OPENTHREAD_CONFIG_RADIO_STATS_ENABLE 0
 
 #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 f2bcbb8..c1a9689 100644
--- a/tests/toranj/openthread-core-toranj-config.h
+++ b/tests/toranj/openthread-core-toranj-config.h
@@ -49,22 +49,32 @@
 
 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1
 
+#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1
+#endif
 
-#define OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE 1
+#define OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 1
 
 #define OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE 1
 
+#define OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE 1
+
 #define OPENTHREAD_CONFIG_MESH_DIAG_ENABLE 1
 
+#define OPENTHREAD_CONFIG_BLE_TCAT_ENABLE 1
+
 #define OPENTHREAD_CONFIG_COMMISSIONER_ENABLE 1
 
 #define OPENTHREAD_CONFIG_COMMISSIONER_MAX_JOINER_ENTRIES 4
 
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 1
 
+#define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE 1
+
+#define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1
+
 #define OPENTHREAD_CONFIG_DIAG_ENABLE 1
 
 #define OPENTHREAD_CONFIG_JOINER_ENABLE 1
@@ -170,8 +180,6 @@
 
 #define OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE 1
 
-#define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1
-
 #define OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK 1
 
 #define OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE 1
diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh
index 5915459..04cef96 100755
--- a/tests/toranj/start.sh
+++ b/tests/toranj/start.sh
@@ -192,7 +192,12 @@
     run cli/test-025-mesh-local-prefix-change.py
     run cli/test-026-coaps-conn-limit.py
     run cli/test-027-slaac-address.py
+    run cli/test-028-border-agent-ephemeral-key.py
     run cli/test-400-srp-client-server.py
+    run cli/test-401-srp-server-address-cache-snoop.py
+    run cli/test-500-two-brs-two-networks.py
+    run cli/test-501-multi-br-failure-recovery.py
+    run cli/test-502-multi-br-leader-failure-recovery.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.
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 1a9b182..3b491bf 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -723,6 +723,27 @@
 
 add_test(NAME ot-test-macros COMMAND ot-test-macros)
 
+add_executable(ot-test-mdns
+    test_mdns.cpp
+)
+
+target_include_directories(ot-test-mdns
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-mdns
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-mdns
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-mdns COMMAND ot-test-mdns)
+
 add_executable(ot-test-message
     test_message.cpp
 )
@@ -1180,6 +1201,27 @@
 
 add_test(NAME ot-test-string COMMAND ot-test-string)
 
+add_executable(ot-test-tcat
+    test_tcat.cpp
+)
+
+target_include_directories(ot-test-tcat
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-tcat
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-tcat
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-tcat COMMAND ot-test-tcat)
+
 add_executable(ot-test-timer
     test_timer.cpp
 )
diff --git a/tests/unit/test_checksum.cpp b/tests/unit/test_checksum.cpp
index dafd2db..0c16f31 100644
--- a/tests/unit/test_checksum.cpp
+++ b/tests/unit/test_checksum.cpp
@@ -30,11 +30,13 @@
 #include "common/message.hpp"
 #include "common/numeric_limits.hpp"
 #include "common/random.hpp"
+#include "common/string.hpp"
 #include "instance/instance.hpp"
 #include "net/checksum.hpp"
 #include "net/icmp6.hpp"
 #include "net/ip4_types.hpp"
 #include "net/udp6.hpp"
+#include "utils/verhoeff_checksum.hpp"
 
 #include "test_platform.h"
 #include "test_util.hpp"
@@ -467,6 +469,62 @@
     }
 };
 
+#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
+void TestVerhoeffChecksum(void)
+{
+    static constexpr uint16_t kMaxStringSize = 50;
+
+    const char *kExamples[] = {"307318421", "487300178", "123455672", "0",   "15",
+                               "999999994", "000000001", "100000000", "2363"};
+
+    const char *kInvalidFormats[] = {
+        "307 318421",
+        "307318421 ",
+        " 307318421",
+        "ABCDE",
+    };
+
+    char string[kMaxStringSize];
+    char checksum;
+    char expectedChecksum;
+
+    printf("\nVerhoeffChecksum\n");
+
+    for (const char *example : kExamples)
+    {
+        uint16_t length = StringLength(example, kMaxStringSize - 1);
+
+        memcpy(string, example, length + 1);
+
+        printf("- \"%s\"\n", string);
+
+        SuccessOrQuit(Utils::VerhoeffChecksum::Validate(string));
+
+        expectedChecksum = string[length - 1];
+
+        string[length - 1] = (expectedChecksum == '0') ? '9' : (expectedChecksum - 1);
+        VerifyOrQuit(Utils::VerhoeffChecksum::Validate(string) == kErrorFailed);
+
+        string[length - 1] = '\0';
+        SuccessOrQuit(Utils::VerhoeffChecksum::Calculate(string, checksum));
+        VerifyOrQuit(checksum == expectedChecksum);
+
+        string[length - 1] = expectedChecksum == '0' ? '9' : (expectedChecksum - 1);
+    }
+
+    printf("\nInvalid format:\n");
+
+    for (const char *example : kInvalidFormats)
+    {
+        printf("- \"%s\"\n", example);
+        VerifyOrQuit(Utils::VerhoeffChecksum::Validate(example) == kErrorInvalidArgs);
+        VerifyOrQuit(Utils::VerhoeffChecksum::Calculate(example, checksum) == kErrorInvalidArgs);
+    }
+}
+
+#endif // OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+
 } // namespace ot
 
 int main(void)
@@ -477,6 +535,10 @@
     ot::TestTcp4MessageChecksum();
     ot::TestUdp4MessageChecksum();
     ot::TestIcmp4MessageChecksum();
+#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE
+    ot::TestVerhoeffChecksum();
+#endif
+
     printf("All tests passed\n");
     return 0;
 }
diff --git a/tests/unit/test_dns.cpp b/tests/unit/test_dns.cpp
index 4566cde..e3bac5c 100644
--- a/tests/unit/test_dns.cpp
+++ b/tests/unit/test_dns.cpp
@@ -276,79 +276,77 @@
     printf("----------------------------------------------------------------\n");
     printf("Extracting label(s) and removing domains:\n");
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "default.service.arpa.";
-    SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name));
-    VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
+    {
+        struct TestCase
+        {
+            const char *mFullName;
+            const char *mSuffixName;
+            const char *mLabels;
+        };
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa";
-    suffixName = "default.service.arpa";
-    SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name));
-    VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
+        static const TestCase kTestCases[] = {
+            {"my-service._ipps._tcp.default.service.arpa.", "default.service.arpa.", "my-service._ipps._tcp"},
+            {"my-service._ipps._tcp.default.service.arpa", "default.service.arpa", "my-service._ipps._tcp"},
+            {"my.service._ipps._tcp.default.service.arpa.", "_ipps._tcp.default.service.arpa.", "my.service"},
+            {"my-service._ipps._tcp.default.service.arpa.", "DeFault.SerVice.ARPA.", "my-service._ipps._tcp"},
+            {"my-service._ipps._tcp.default.service.arpa", "DeFault.SerVice.ARPA", "my-service._ipps._tcp"},
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa";
-    suffixName = "default.service.arpa.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
+            {"my-service._ipps._tcp.default.service.arpa", "default.service.arpa.", nullptr},
+            {"my-service._ipps._tcp.default.service.arpa.", "default.service.arpa", nullptr},
+            {"my-service._ipps._tcp.default.service.arpa.", "efault.service.arpa.", nullptr},
+            {"my-service._ipps._tcp.default.service.arpa", "efault.service.arpa", nullptr},
+            {"my-service._ipps._tcp.default.service.arpa.", "xdefault.service.arpa.", nullptr},
+            {"my-service._ipps._tcp.default.service.arpa.", ".default.service.arpa.", nullptr},
+            {"my-service._ipps._tcp.default.service.arpa.", "default.service.arp.", nullptr},
+            {"default.service.arpa.", "default.service.arpa.", nullptr},
+            {"default.service.arpa", "default.service.arpa", nullptr},
+            {"efault.service.arpa.", "default.service.arpa.", nullptr},
+        };
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "default.service.arpa";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
+        for (const TestCase &testCase : kTestCases)
+        {
+            Error error;
 
-    fullName   = "my.service._ipps._tcp.default.service.arpa.";
-    suffixName = "_ipps._tcp.default.service.arpa.";
-    SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name));
-    VerifyOrQuit(strcmp(name, "my.service") == 0);
+            printf("\n");
+            printf("  FullName        : %s\n", testCase.mFullName);
+            printf("  SuffixName      : %s\n", testCase.mSuffixName);
+            printf("  Extracted labels: %s\n", (testCase.mLabels != nullptr) ? testCase.mLabels : "(parse)");
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "DeFault.SerVice.ARPA.";
-    SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name));
-    VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
+            error = Dns::Name::ExtractLabels(testCase.mFullName, testCase.mSuffixName, name);
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa";
-    suffixName = "DeFault.SerVice.ARPA";
-    SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name));
-    VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
+            if (testCase.mLabels != nullptr)
+            {
+                SuccessOrQuit(error);
+                VerifyOrQuit(strcmp(name, testCase.mLabels) == 0);
+            }
+            else
+            {
+                VerifyOrQuit(error == kErrorParse);
+            }
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "efault.service.arpa.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
+            strcpy(name, testCase.mFullName);
+            error = Dns::Name::StripName(name, testCase.mSuffixName);
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa";
-    suffixName = "efault.service.arpa";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
+            if (testCase.mLabels != nullptr)
+            {
+                SuccessOrQuit(error);
+                VerifyOrQuit(strcmp(name, testCase.mLabels) == 0);
+            }
+            else
+            {
+                VerifyOrQuit(error == kErrorParse);
+            }
+        }
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "xdefault.service.arpa.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
+        fullName   = "my-service._ipps._tcp.default.service.arpa.";
+        suffixName = "default.service.arpa.";
+        SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, 22));
+        VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
 
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = ".default.service.arpa.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
-
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "default.service.arp.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
-
-    fullName   = "default.service.arpa.";
-    suffixName = "default.service.arpa.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
-
-    fullName   = "default.service.arpa";
-    suffixName = "default.service.arpa";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
-
-    fullName   = "efault.service.arpa.";
-    suffixName = "default.service.arpa.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name) == kErrorParse);
-
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "default.service.arpa.";
-    SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, 22));
-    VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
-
-    fullName   = "my-service._ipps._tcp.default.service.arpa.";
-    suffixName = "default.service.arpa.";
-    VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, 21) == kErrorNoBufs);
+        fullName   = "my-service._ipps._tcp.default.service.arpa.";
+        suffixName = "default.service.arpa.";
+        VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, 21) == kErrorNoBufs);
+    }
 
     printf("----------------------------------------------------------------\n");
     printf("Append names, check encoded bytes, parse name and read labels:\n");
diff --git a/tests/unit/test_mdns.cpp b/tests/unit/test_mdns.cpp
new file mode 100644
index 0000000..72e63ca
--- /dev/null
+++ b/tests/unit/test_mdns.cpp
@@ -0,0 +1,6841 @@
+/*
+ *  Copyright (c) 2024, 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/arg_macros.hpp"
+#include "common/array.hpp"
+#include "common/as_core_type.hpp"
+#include "common/num_utils.hpp"
+#include "common/owning_list.hpp"
+#include "common/string.hpp"
+#include "common/time.hpp"
+#include "instance/instance.hpp"
+#include "net/dns_dso.hpp"
+#include "net/mdns.hpp"
+
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+namespace ot {
+namespace Dns {
+namespace Multicast {
+
+#define ENABLE_TEST_LOG 1 // Enable to get logs from unit test.
+
+// Logs a message and adds current time (sNow) as "<hours>:<min>:<secs>.<msec>"
+#if ENABLE_TEST_LOG
+#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__))
+#else
+#define Log(...)
+#endif
+
+//---------------------------------------------------------------------------------------------------------------------
+// Constants
+
+static constexpr uint16_t kClassQueryUnicastFlag = (1U << 15);
+static constexpr uint16_t kClassCacheFlushFlag   = (1U << 15);
+static constexpr uint16_t kClassMask             = 0x7fff;
+static constexpr uint16_t kStringSize            = 300;
+static constexpr uint16_t kMaxDataSize           = 400;
+static constexpr uint16_t kNumAnnounces          = 3;
+static constexpr uint16_t kNumInitalQueries      = 3;
+static constexpr uint16_t kNumRefreshQueries     = 4;
+static constexpr bool     kCacheFlush            = true;
+static constexpr uint16_t kMdnsPort              = 5353;
+static constexpr uint32_t kInfraIfIndex          = 1;
+
+static const char kDeviceIp6Address[] = "fd01::1";
+
+class DnsMessage;
+
+//---------------------------------------------------------------------------------------------------------------------
+// Variables
+
+static Instance *sInstance;
+
+static uint32_t sNow = 0;
+static uint32_t sAlarmTime;
+static bool     sAlarmOn = false;
+
+OwningList<DnsMessage> sDnsMessages;
+uint32_t               sInfraIfIndex;
+
+//---------------------------------------------------------------------------------------------------------------------
+// Prototypes
+
+static const char *RecordTypeToString(uint16_t aType);
+
+//---------------------------------------------------------------------------------------------------------------------
+// Types
+
+template <typename Type> class Allocatable
+{
+public:
+    static Type *Allocate(void)
+    {
+        void *buf = calloc(1, sizeof(Type));
+
+        VerifyOrQuit(buf != nullptr);
+        return new (buf) Type();
+    }
+
+    void Free(void)
+    {
+        static_cast<Type *>(this)->~Type();
+        free(this);
+    }
+};
+
+struct DnsName
+{
+    Name::Buffer mName;
+
+    void ParseFrom(const Message &aMessage, uint16_t &aOffset)
+    {
+        SuccessOrQuit(Name::ReadName(aMessage, aOffset, mName));
+    }
+
+    void CopyFrom(const char *aName)
+    {
+        if (aName == nullptr)
+        {
+            mName[0] = '\0';
+        }
+        else
+        {
+            uint16_t len = StringLength(aName, sizeof(mName));
+
+            VerifyOrQuit(len < sizeof(mName));
+            memcpy(mName, aName, len + 1);
+        }
+    }
+
+    const char *AsCString(void) const { return mName; }
+    bool        Matches(const char *aName) const { return StringMatch(mName, aName, kStringCaseInsensitiveMatch); }
+};
+
+typedef String<Name::kMaxNameSize> DnsNameString;
+
+struct AddrAndTtl
+{
+    bool operator==(const AddrAndTtl &aOther) const { return (mTtl == aOther.mTtl) && (mAddress == aOther.mAddress); }
+
+    Ip6::Address mAddress;
+    uint32_t     mTtl;
+};
+
+struct DnsQuestion : public Allocatable<DnsQuestion>, public LinkedListEntry<DnsQuestion>
+{
+    DnsQuestion *mNext;
+    DnsName      mName;
+    uint16_t     mType;
+    uint16_t     mClass;
+    bool         mUnicastResponse;
+
+    void ParseFrom(const Message &aMessage, uint16_t &aOffset)
+    {
+        Question question;
+
+        mName.ParseFrom(aMessage, aOffset);
+        SuccessOrQuit(aMessage.Read(aOffset, question));
+        aOffset += sizeof(Question);
+
+        mNext            = nullptr;
+        mType            = question.GetType();
+        mClass           = question.GetClass() & kClassMask;
+        mUnicastResponse = question.GetClass() & kClassQueryUnicastFlag;
+
+        Log("      %s %s %s class:%u", mName.AsCString(), RecordTypeToString(mType), mUnicastResponse ? "QU" : "QM",
+            mClass);
+    }
+
+    bool Matches(const char *aName) const { return mName.Matches(aName); }
+};
+
+struct DnsQuestions : public OwningList<DnsQuestion>
+{
+    bool Contains(uint16_t aRrType, const DnsNameString &aFullName, bool aUnicastResponse = false) const
+    {
+        bool               contains = false;
+        const DnsQuestion *question = FindMatching(aFullName.AsCString());
+
+        VerifyOrExit(question != nullptr);
+        VerifyOrExit(question->mType == aRrType);
+        VerifyOrExit(question->mClass == ResourceRecord::kClassInternet);
+        VerifyOrExit(question->mUnicastResponse == aUnicastResponse);
+        contains = true;
+
+    exit:
+        return contains;
+    }
+
+    bool Contains(const DnsNameString &aFullName, bool aUnicastResponse) const
+    {
+        return Contains(ResourceRecord::kTypeAny, aFullName, aUnicastResponse);
+    }
+};
+
+enum TtlCheckMode : uint8_t
+{
+    kZeroTtl,
+    kNonZeroTtl,
+};
+
+enum Section : uint8_t
+{
+    kInAnswerSection,
+    kInAdditionalSection,
+};
+
+struct Data : public ot::Data<kWithUint16Length>
+{
+    Data(const void *aBuffer, uint16_t aLength) { Init(aBuffer, aLength); }
+
+    bool Matches(const Array<uint8_t, kMaxDataSize> &aDataArray) const
+    {
+        return (aDataArray.GetLength() == GetLength()) && MatchesBytesIn(aDataArray.GetArrayBuffer());
+    }
+};
+
+struct DnsRecord : public Allocatable<DnsRecord>, public LinkedListEntry<DnsRecord>
+{
+    struct SrvData
+    {
+        uint16_t mPriority;
+        uint16_t mWeight;
+        uint16_t mPort;
+        DnsName  mHostName;
+    };
+
+    union RecordData
+    {
+        RecordData(void) { memset(this, 0, sizeof(*this)); }
+
+        Ip6::Address                 mIp6Address; // For AAAAA (or A)
+        SrvData                      mSrv;        // For SRV
+        Array<uint8_t, kMaxDataSize> mData;       // For TXT or KEY
+        DnsName                      mPtrName;    // For PTR
+        NsecRecord::TypeBitMap       mNsecBitmap; // For NSEC
+    };
+
+    DnsRecord *mNext;
+    DnsName    mName;
+    uint16_t   mType;
+    uint16_t   mClass;
+    uint32_t   mTtl;
+    bool       mCacheFlush;
+    RecordData mData;
+
+    bool Matches(const char *aName) const { return mName.Matches(aName); }
+
+    void ParseFrom(const Message &aMessage, uint16_t &aOffset)
+    {
+        String<kStringSize> logStr;
+        ResourceRecord      record;
+        uint16_t            offset;
+
+        mName.ParseFrom(aMessage, aOffset);
+        SuccessOrQuit(aMessage.Read(aOffset, record));
+        aOffset += sizeof(ResourceRecord);
+
+        mNext       = nullptr;
+        mType       = record.GetType();
+        mClass      = record.GetClass() & kClassMask;
+        mCacheFlush = record.GetClass() & kClassCacheFlushFlag;
+        mTtl        = record.GetTtl();
+
+        logStr.Append("%s %s%s cls:%u ttl:%u", mName.AsCString(), RecordTypeToString(mType),
+                      mCacheFlush ? " cache-flush" : "", mClass, mTtl);
+
+        offset = aOffset;
+
+        switch (mType)
+        {
+        case ResourceRecord::kTypeAaaa:
+            VerifyOrQuit(record.GetLength() == sizeof(Ip6::Address));
+            SuccessOrQuit(aMessage.Read(offset, mData.mIp6Address));
+            logStr.Append(" %s", mData.mIp6Address.ToString().AsCString());
+            break;
+
+        case ResourceRecord::kTypeKey:
+        case ResourceRecord::kTypeTxt:
+            VerifyOrQuit(record.GetLength() > 0);
+            VerifyOrQuit(record.GetLength() < kMaxDataSize);
+            mData.mData.SetLength(record.GetLength());
+            SuccessOrQuit(aMessage.Read(offset, mData.mData.GetArrayBuffer(), record.GetLength()));
+            logStr.Append(" data-len:%u", record.GetLength());
+            break;
+
+        case ResourceRecord::kTypePtr:
+            mData.mPtrName.ParseFrom(aMessage, offset);
+            VerifyOrQuit(offset - aOffset == record.GetLength());
+            logStr.Append(" %s", mData.mPtrName.AsCString());
+            break;
+
+        case ResourceRecord::kTypeSrv:
+        {
+            SrvRecord srv;
+
+            offset -= sizeof(ResourceRecord);
+            SuccessOrQuit(aMessage.Read(offset, srv));
+            offset += sizeof(srv);
+            mData.mSrv.mHostName.ParseFrom(aMessage, offset);
+            VerifyOrQuit(offset - aOffset == record.GetLength());
+            mData.mSrv.mPriority = srv.GetPriority();
+            mData.mSrv.mWeight   = srv.GetWeight();
+            mData.mSrv.mPort     = srv.GetPort();
+            logStr.Append(" port:%u w:%u prio:%u host:%s", mData.mSrv.mPort, mData.mSrv.mWeight, mData.mSrv.mPriority,
+                          mData.mSrv.mHostName.AsCString());
+            break;
+        }
+
+        case ResourceRecord::kTypeNsec:
+        {
+            NsecRecord::TypeBitMap &bitmap = mData.mNsecBitmap;
+
+            SuccessOrQuit(Name::CompareName(aMessage, offset, mName.AsCString()));
+            SuccessOrQuit(aMessage.Read(offset, &bitmap, NsecRecord::TypeBitMap::kMinSize));
+            VerifyOrQuit(bitmap.GetBlockNumber() == 0);
+            VerifyOrQuit(bitmap.GetBitmapLength() <= NsecRecord::TypeBitMap::kMaxLength);
+            SuccessOrQuit(aMessage.Read(offset, &bitmap, bitmap.GetSize()));
+
+            offset += bitmap.GetSize();
+            VerifyOrQuit(offset - aOffset == record.GetLength());
+
+            logStr.Append(" [ ");
+
+            for (uint16_t type = 0; type < bitmap.GetBitmapLength() * kBitsPerByte; type++)
+            {
+                if (bitmap.ContainsType(type))
+                {
+                    logStr.Append("%s ", RecordTypeToString(type));
+                }
+            }
+
+            logStr.Append("]");
+            break;
+        }
+
+        default:
+            break;
+        }
+
+        Log("      %s", logStr.AsCString());
+
+        aOffset += record.GetLength();
+    }
+
+    bool MatchesTtl(TtlCheckMode aTtlCheckMode, uint32_t aTtl) const
+    {
+        bool matches = false;
+
+        switch (aTtlCheckMode)
+        {
+        case kZeroTtl:
+            VerifyOrExit(mTtl == 0);
+            break;
+        case kNonZeroTtl:
+            if (aTtl > 0)
+            {
+                VerifyOrQuit(mTtl == aTtl);
+            }
+
+            VerifyOrExit(mTtl > 0);
+            break;
+        }
+
+        matches = true;
+
+    exit:
+        return matches;
+    }
+};
+
+struct DnsRecords : public OwningList<DnsRecord>
+{
+    bool ContainsAaaa(const DnsNameString &aFullName,
+                      const Ip6::Address  &aAddress,
+                      bool                 aCacheFlush,
+                      TtlCheckMode         aTtlCheckMode,
+                      uint32_t             aTtl = 0) const
+    {
+        bool contains = false;
+
+        for (const DnsRecord &record : *this)
+        {
+            if (record.Matches(aFullName.AsCString()) && (record.mType == ResourceRecord::kTypeAaaa) &&
+                (record.mData.mIp6Address == aAddress))
+            {
+                VerifyOrExit(record.mClass == ResourceRecord::kClassInternet);
+                VerifyOrExit(record.mCacheFlush == aCacheFlush);
+                VerifyOrExit(record.MatchesTtl(aTtlCheckMode, aTtl));
+                contains = true;
+                ExitNow();
+            }
+        }
+
+    exit:
+        return contains;
+    }
+
+    bool ContainsKey(const DnsNameString &aFullName,
+                     const Data          &aKeyData,
+                     bool                 aCacheFlush,
+                     TtlCheckMode         aTtlCheckMode,
+                     uint32_t             aTtl = 0) const
+    {
+        bool contains = false;
+
+        for (const DnsRecord &record : *this)
+        {
+            if (record.Matches(aFullName.AsCString()) && (record.mType == ResourceRecord::kTypeKey) &&
+                aKeyData.Matches(record.mData.mData))
+            {
+                VerifyOrExit(record.mClass == ResourceRecord::kClassInternet);
+                VerifyOrExit(record.mCacheFlush == aCacheFlush);
+                VerifyOrExit(record.MatchesTtl(aTtlCheckMode, aTtl));
+                contains = true;
+                ExitNow();
+            }
+        }
+
+    exit:
+        return contains;
+    }
+
+    bool ContainsSrv(const DnsNameString &aFullName,
+                     const Core::Service &aService,
+                     bool                 aCacheFlush,
+                     TtlCheckMode         aTtlCheckMode,
+                     uint32_t             aTtl = 0) const
+    {
+        bool          contains = false;
+        DnsNameString hostName;
+
+        hostName.Append("%s.local.", aService.mHostName);
+
+        for (const DnsRecord &record : *this)
+        {
+            if (record.Matches(aFullName.AsCString()) && (record.mType == ResourceRecord::kTypeSrv))
+            {
+                VerifyOrExit(record.mClass == ResourceRecord::kClassInternet);
+                VerifyOrExit(record.mCacheFlush == aCacheFlush);
+                VerifyOrExit(record.MatchesTtl(aTtlCheckMode, aTtl));
+                VerifyOrExit(record.mData.mSrv.mPort == aService.mPort);
+                VerifyOrExit(record.mData.mSrv.mPriority == aService.mPriority);
+                VerifyOrExit(record.mData.mSrv.mWeight == aService.mWeight);
+                VerifyOrExit(record.mData.mSrv.mHostName.Matches(hostName.AsCString()));
+                contains = true;
+                ExitNow();
+            }
+        }
+
+    exit:
+        return contains;
+    }
+
+    bool ContainsTxt(const DnsNameString &aFullName,
+                     const Core::Service &aService,
+                     bool                 aCacheFlush,
+                     TtlCheckMode         aTtlCheckMode,
+                     uint32_t             aTtl = 0) const
+    {
+        static const uint8_t kEmptyTxtData[1] = {0};
+
+        bool contains = false;
+        Data txtData(aService.mTxtData, aService.mTxtDataLength);
+
+        if ((aService.mTxtData == nullptr) || (aService.mTxtDataLength == 0))
+        {
+            txtData.Init(kEmptyTxtData, sizeof(kEmptyTxtData));
+        }
+
+        for (const DnsRecord &record : *this)
+        {
+            if (record.Matches(aFullName.AsCString()) && (record.mType == ResourceRecord::kTypeTxt) &&
+                txtData.Matches(record.mData.mData))
+            {
+                VerifyOrExit(record.mClass == ResourceRecord::kClassInternet);
+                VerifyOrExit(record.mCacheFlush == aCacheFlush);
+                VerifyOrExit(record.MatchesTtl(aTtlCheckMode, aTtl));
+                contains = true;
+                ExitNow();
+            }
+        }
+
+    exit:
+        return contains;
+    }
+
+    bool ContainsPtr(const DnsNameString &aFullName,
+                     const DnsNameString &aPtrName,
+                     TtlCheckMode         aTtlCheckMode,
+                     uint32_t             aTtl = 0) const
+    {
+        bool contains = false;
+
+        for (const DnsRecord &record : *this)
+        {
+            if (record.Matches(aFullName.AsCString()) && (record.mType == ResourceRecord::kTypePtr) &&
+                (record.mData.mPtrName.Matches(aPtrName.AsCString())))
+            {
+                VerifyOrExit(record.mClass == ResourceRecord::kClassInternet);
+                VerifyOrExit(!record.mCacheFlush); // PTR should never use cache-flush
+                VerifyOrExit(record.MatchesTtl(aTtlCheckMode, aTtl));
+                contains = true;
+                ExitNow();
+            }
+        }
+
+    exit:
+        return contains;
+    }
+
+    bool ContainsServicesPtr(const DnsNameString &aServiceType) const
+    {
+        DnsNameString allServices;
+
+        allServices.Append("_services._dns-sd._udp.local.");
+
+        return ContainsPtr(allServices, aServiceType, kNonZeroTtl, 0);
+    }
+
+    bool ContainsNsec(const DnsNameString &aFullName, uint16_t aRecordType) const
+    {
+        bool contains = false;
+
+        for (const DnsRecord &record : *this)
+        {
+            if (record.Matches(aFullName.AsCString()) && (record.mType == ResourceRecord::kTypeNsec))
+            {
+                VerifyOrQuit(!contains); // Ensure only one NSEC record
+                VerifyOrExit(record.mData.mNsecBitmap.ContainsType(aRecordType));
+                contains = true;
+            }
+        }
+
+    exit:
+        return contains;
+    }
+};
+
+// Bit-flags used in `Validate()` with a `Service`
+// to specify which records should be checked in the announce
+// message.
+
+typedef uint8_t AnnounceCheckFlags;
+
+static constexpr uint8_t kCheckSrv         = (1 << 0);
+static constexpr uint8_t kCheckTxt         = (1 << 1);
+static constexpr uint8_t kCheckPtr         = (1 << 2);
+static constexpr uint8_t kCheckServicesPtr = (1 << 3);
+
+enum GoodBye : bool // Used to indicate "goodbye" records (with zero TTL)
+{
+    kNotGoodBye = false,
+    kGoodBye    = true,
+};
+
+enum DnsMessageType : uint8_t
+{
+    kMulticastQuery,
+    kMulticastResponse,
+    kUnicastResponse,
+};
+
+struct DnsMessage : public Allocatable<DnsMessage>, public LinkedListEntry<DnsMessage>
+{
+    DnsMessage       *mNext;
+    uint32_t          mTimestamp;
+    DnsMessageType    mType;
+    Core::AddressInfo mUnicastDest;
+    Header            mHeader;
+    DnsQuestions      mQuestions;
+    DnsRecords        mAnswerRecords;
+    DnsRecords        mAuthRecords;
+    DnsRecords        mAdditionalRecords;
+
+    DnsMessage(void)
+        : mNext(nullptr)
+        , mTimestamp(sNow)
+    {
+    }
+
+    const DnsRecords &RecordsFor(Section aSection) const
+    {
+        const DnsRecords *records = nullptr;
+
+        switch (aSection)
+        {
+        case kInAnswerSection:
+            records = &mAnswerRecords;
+            break;
+        case kInAdditionalSection:
+            records = &mAdditionalRecords;
+            break;
+        }
+
+        VerifyOrQuit(records != nullptr);
+
+        return *records;
+    }
+
+    void ParseRecords(const Message         &aMessage,
+                      uint16_t              &aOffset,
+                      uint16_t               aNumRecords,
+                      OwningList<DnsRecord> &aRecords,
+                      const char            *aSectionName)
+    {
+        if (aNumRecords > 0)
+        {
+            Log("   %s", aSectionName);
+        }
+
+        for (; aNumRecords > 0; aNumRecords--)
+        {
+            DnsRecord *record = DnsRecord::Allocate();
+
+            record->ParseFrom(aMessage, aOffset);
+            aRecords.PushAfterTail(*record);
+        }
+    }
+
+    void ParseFrom(const Message &aMessage)
+    {
+        uint16_t offset = 0;
+
+        SuccessOrQuit(aMessage.Read(offset, mHeader));
+        offset += sizeof(Header);
+
+        Log("   %s id:%u qt:%u t:%u rcode:%u [q:%u ans:%u auth:%u addn:%u]",
+            mHeader.GetType() == Header::kTypeQuery ? "Query" : "Response", mHeader.GetMessageId(),
+            mHeader.GetQueryType(), mHeader.IsTruncationFlagSet(), mHeader.GetResponseCode(),
+            mHeader.GetQuestionCount(), mHeader.GetAnswerCount(), mHeader.GetAuthorityRecordCount(),
+            mHeader.GetAdditionalRecordCount());
+
+        if (mHeader.GetQuestionCount() > 0)
+        {
+            Log("   Question");
+        }
+
+        for (uint16_t num = mHeader.GetQuestionCount(); num > 0; num--)
+        {
+            DnsQuestion *question = DnsQuestion::Allocate();
+
+            question->ParseFrom(aMessage, offset);
+            mQuestions.PushAfterTail(*question);
+        }
+
+        ParseRecords(aMessage, offset, mHeader.GetAnswerCount(), mAnswerRecords, "Answer");
+        ParseRecords(aMessage, offset, mHeader.GetAuthorityRecordCount(), mAuthRecords, "Authority");
+        ParseRecords(aMessage, offset, mHeader.GetAdditionalRecordCount(), mAdditionalRecords, "Additional");
+    }
+
+    void ValidateHeader(DnsMessageType aType,
+                        uint16_t       aQuestionCount,
+                        uint16_t       aAnswerCount,
+                        uint16_t       aAuthCount,
+                        uint16_t       aAdditionalCount) const
+    {
+        VerifyOrQuit(mType == aType);
+        VerifyOrQuit(mHeader.GetQuestionCount() == aQuestionCount);
+        VerifyOrQuit(mHeader.GetAnswerCount() == aAnswerCount);
+        VerifyOrQuit(mHeader.GetAuthorityRecordCount() == aAuthCount);
+        VerifyOrQuit(mHeader.GetAdditionalRecordCount() == aAdditionalCount);
+
+        if (aType == kUnicastResponse)
+        {
+            Ip6::Address ip6Address;
+
+            SuccessOrQuit(ip6Address.FromString(kDeviceIp6Address));
+
+            VerifyOrQuit(mUnicastDest.mPort == kMdnsPort);
+            VerifyOrQuit(mUnicastDest.GetAddress() == ip6Address);
+        }
+    }
+
+    static void DetemineFullNameForKey(const Core::Key &aKey, DnsNameString &aFullName)
+    {
+        if (aKey.mServiceType != nullptr)
+        {
+            aFullName.Append("%s.%s.local.", aKey.mName, aKey.mServiceType);
+        }
+        else
+        {
+            aFullName.Append("%s.local.", aKey.mName);
+        }
+    }
+
+    void ValidateAsProbeFor(const Core::Host &aHost, bool aUnicastResponse) const
+    {
+        DnsNameString fullName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeQuery);
+        VerifyOrQuit(!mHeader.IsTruncationFlagSet());
+
+        fullName.Append("%s.local.", aHost.mHostName);
+        VerifyOrQuit(mQuestions.Contains(fullName, aUnicastResponse));
+
+        for (uint16_t index = 0; index < aHost.mAddressesLength; index++)
+        {
+            VerifyOrQuit(mAuthRecords.ContainsAaaa(fullName, AsCoreType(&aHost.mAddresses[index]), !kCacheFlush,
+                                                   kNonZeroTtl, aHost.mTtl));
+        }
+    }
+
+    void ValidateAsProbeFor(const Core::Service &aService, bool aUnicastResponse) const
+    {
+        DnsNameString serviceName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeQuery);
+        VerifyOrQuit(!mHeader.IsTruncationFlagSet());
+
+        serviceName.Append("%s.%s.local.", aService.mServiceInstance, aService.mServiceType);
+
+        VerifyOrQuit(mQuestions.Contains(serviceName, aUnicastResponse));
+
+        VerifyOrQuit(mAuthRecords.ContainsSrv(serviceName, aService, !kCacheFlush, kNonZeroTtl, aService.mTtl));
+        VerifyOrQuit(mAuthRecords.ContainsTxt(serviceName, aService, !kCacheFlush, kNonZeroTtl, aService.mTtl));
+    }
+
+    void ValidateAsProbeFor(const Core::Key &aKey, bool aUnicastResponse) const
+    {
+        DnsNameString fullName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeQuery);
+        VerifyOrQuit(!mHeader.IsTruncationFlagSet());
+
+        DetemineFullNameForKey(aKey, fullName);
+
+        VerifyOrQuit(mQuestions.Contains(fullName, aUnicastResponse));
+        VerifyOrQuit(mAuthRecords.ContainsKey(fullName, Data(aKey.mKeyData, aKey.mKeyDataLength), !kCacheFlush,
+                                              kNonZeroTtl, aKey.mTtl));
+    }
+
+    void Validate(const Core::Host &aHost, Section aSection, GoodBye aIsGoodBye = kNotGoodBye) const
+    {
+        DnsNameString fullName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeResponse);
+
+        fullName.Append("%s.local.", aHost.mHostName);
+
+        for (uint16_t index = 0; index < aHost.mAddressesLength; index++)
+        {
+            VerifyOrQuit(RecordsFor(aSection).ContainsAaaa(fullName, AsCoreType(&aHost.mAddresses[index]), kCacheFlush,
+                                                           aIsGoodBye ? kZeroTtl : kNonZeroTtl, aHost.mTtl));
+        }
+
+        if (!aIsGoodBye && (aSection == kInAnswerSection))
+        {
+            VerifyOrQuit(mAdditionalRecords.ContainsNsec(fullName, ResourceRecord::kTypeAaaa));
+        }
+    }
+
+    void Validate(const Core::Service &aService,
+                  Section              aSection,
+                  AnnounceCheckFlags   aCheckFlags,
+                  GoodBye              aIsGoodBye = kNotGoodBye) const
+    {
+        DnsNameString serviceName;
+        DnsNameString serviceType;
+        bool          checkNsec = false;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeResponse);
+
+        serviceName.Append("%s.%s.local.", aService.mServiceInstance, aService.mServiceType);
+        serviceType.Append("%s.local.", aService.mServiceType);
+
+        if (aCheckFlags & kCheckSrv)
+        {
+            VerifyOrQuit(RecordsFor(aSection).ContainsSrv(serviceName, aService, kCacheFlush,
+                                                          aIsGoodBye ? kZeroTtl : kNonZeroTtl, aService.mTtl));
+            checkNsec = true;
+        }
+
+        if (aCheckFlags & kCheckTxt)
+        {
+            VerifyOrQuit(RecordsFor(aSection).ContainsTxt(serviceName, aService, kCacheFlush,
+                                                          aIsGoodBye ? kZeroTtl : kNonZeroTtl, aService.mTtl));
+            checkNsec = true;
+        }
+
+        if (aCheckFlags & kCheckPtr)
+        {
+            VerifyOrQuit(RecordsFor(aSection).ContainsPtr(serviceType, serviceName, aIsGoodBye ? kZeroTtl : kNonZeroTtl,
+                                                          aService.mTtl));
+        }
+
+        if (aCheckFlags & kCheckServicesPtr)
+        {
+            VerifyOrQuit(RecordsFor(aSection).ContainsServicesPtr(serviceType));
+        }
+
+        if (!aIsGoodBye && checkNsec && (aSection == kInAnswerSection))
+        {
+            VerifyOrQuit(mAdditionalRecords.ContainsNsec(serviceName, ResourceRecord::kTypeSrv));
+            VerifyOrQuit(mAdditionalRecords.ContainsNsec(serviceName, ResourceRecord::kTypeTxt));
+        }
+    }
+
+    void Validate(const Core::Key &aKey, Section aSection, GoodBye aIsGoodBye = kNotGoodBye) const
+    {
+        DnsNameString fullName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeResponse);
+
+        DetemineFullNameForKey(aKey, fullName);
+        VerifyOrQuit(RecordsFor(aSection).ContainsKey(fullName, Data(aKey.mKeyData, aKey.mKeyDataLength), kCacheFlush,
+                                                      aIsGoodBye ? kZeroTtl : kNonZeroTtl, aKey.mTtl));
+
+        if (!aIsGoodBye && (aSection == kInAnswerSection))
+        {
+            VerifyOrQuit(mAdditionalRecords.ContainsNsec(fullName, ResourceRecord::kTypeKey));
+        }
+    }
+
+    void ValidateSubType(const char *aSubLabel, const Core::Service &aService, GoodBye aIsGoodBye = kNotGoodBye) const
+    {
+        DnsNameString serviceName;
+        DnsNameString subServiceType;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeResponse);
+
+        serviceName.Append("%s.%s.local.", aService.mServiceInstance, aService.mServiceType);
+        subServiceType.Append("%s._sub.%s.local.", aSubLabel, aService.mServiceType);
+
+        VerifyOrQuit(mAnswerRecords.ContainsPtr(subServiceType, serviceName, aIsGoodBye ? kZeroTtl : kNonZeroTtl,
+                                                aService.mTtl));
+    }
+
+    void ValidateAsQueryFor(const Core::Browser &aBrowser) const
+    {
+        DnsNameString fullServiceType;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeQuery);
+        VerifyOrQuit(!mHeader.IsTruncationFlagSet());
+
+        if (aBrowser.mSubTypeLabel == nullptr)
+        {
+            fullServiceType.Append("%s.local.", aBrowser.mServiceType);
+        }
+        else
+        {
+            fullServiceType.Append("%s._sub.%s.local", aBrowser.mSubTypeLabel, aBrowser.mServiceType);
+        }
+
+        VerifyOrQuit(mQuestions.Contains(ResourceRecord::kTypePtr, fullServiceType));
+    }
+
+    void ValidateAsQueryFor(const Core::SrvResolver &aResolver) const
+    {
+        DnsNameString fullName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeQuery);
+        VerifyOrQuit(!mHeader.IsTruncationFlagSet());
+
+        fullName.Append("%s.%s.local.", aResolver.mServiceInstance, aResolver.mServiceType);
+
+        VerifyOrQuit(mQuestions.Contains(ResourceRecord::kTypeSrv, fullName));
+    }
+
+    void ValidateAsQueryFor(const Core::TxtResolver &aResolver) const
+    {
+        DnsNameString fullName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeQuery);
+        VerifyOrQuit(!mHeader.IsTruncationFlagSet());
+
+        fullName.Append("%s.%s.local.", aResolver.mServiceInstance, aResolver.mServiceType);
+
+        VerifyOrQuit(mQuestions.Contains(ResourceRecord::kTypeTxt, fullName));
+    }
+
+    void ValidateAsQueryFor(const Core::AddressResolver &aResolver) const
+    {
+        DnsNameString fullName;
+
+        VerifyOrQuit(mHeader.GetType() == Header::kTypeQuery);
+        VerifyOrQuit(!mHeader.IsTruncationFlagSet());
+
+        fullName.Append("%s.local.", aResolver.mHostName);
+
+        VerifyOrQuit(mQuestions.Contains(ResourceRecord::kTypeAaaa, fullName));
+    }
+};
+
+struct RegCallback
+{
+    void Reset(void) { mWasCalled = false; }
+
+    bool  mWasCalled;
+    Error mError;
+};
+
+static constexpr uint16_t kMaxCallbacks = 8;
+
+static RegCallback sRegCallbacks[kMaxCallbacks];
+
+static void HandleCallback(otInstance *aInstance, otMdnsRequestId aRequestId, otError aError)
+{
+    Log("Register callback - ResuestId:%u Error:%s", aRequestId, otThreadErrorToString(aError));
+
+    VerifyOrQuit(aInstance == sInstance);
+    VerifyOrQuit(aRequestId < kMaxCallbacks);
+
+    VerifyOrQuit(!sRegCallbacks[aRequestId].mWasCalled);
+
+    sRegCallbacks[aRequestId].mWasCalled = true;
+    sRegCallbacks[aRequestId].mError     = aError;
+}
+
+static void HandleSuccessCallback(otInstance *aInstance, otMdnsRequestId aRequestId, otError aError)
+{
+    HandleCallback(aInstance, aRequestId, aError);
+    SuccessOrQuit(aError);
+}
+
+struct ConflictCallback
+{
+    void Reset(void) { mWasCalled = false; }
+
+    void Handle(const char *aName, const char *aServiceType)
+    {
+        VerifyOrQuit(!mWasCalled);
+
+        mWasCalled = true;
+        mName.Clear();
+        mName.Append("%s", aName);
+
+        mHasServiceType = (aServiceType != nullptr);
+        VerifyOrExit(mHasServiceType);
+        mServiceType.Clear();
+        mServiceType.Append("%s", aServiceType);
+
+    exit:
+        return;
+    }
+
+    bool          mWasCalled;
+    bool          mHasServiceType;
+    DnsNameString mName;
+    DnsNameString mServiceType;
+};
+
+static ConflictCallback sConflictCallback;
+
+static void HandleConflict(otInstance *aInstance, const char *aName, const char *aServiceType)
+{
+    Log("Conflict callback - %s %s", aName, (aServiceType == nullptr) ? "" : aServiceType);
+
+    VerifyOrQuit(aInstance == sInstance);
+    sConflictCallback.Handle(aName, aServiceType);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Helper functions and methods
+
+static const char *RecordTypeToString(uint16_t aType)
+{
+    const char *str = "Other";
+
+    switch (aType)
+    {
+    case ResourceRecord::kTypeZero:
+        str = "ZERO";
+        break;
+    case ResourceRecord::kTypeA:
+        str = "A";
+        break;
+    case ResourceRecord::kTypeSoa:
+        str = "SOA";
+        break;
+    case ResourceRecord::kTypeCname:
+        str = "CNAME";
+        break;
+    case ResourceRecord::kTypePtr:
+        str = "PTR";
+        break;
+    case ResourceRecord::kTypeTxt:
+        str = "TXT";
+        break;
+    case ResourceRecord::kTypeSig:
+        str = "SIG";
+        break;
+    case ResourceRecord::kTypeKey:
+        str = "KEY";
+        break;
+    case ResourceRecord::kTypeAaaa:
+        str = "AAAA";
+        break;
+    case ResourceRecord::kTypeSrv:
+        str = "SRV";
+        break;
+    case ResourceRecord::kTypeOpt:
+        str = "OPT";
+        break;
+    case ResourceRecord::kTypeNsec:
+        str = "NSEC";
+        break;
+    case ResourceRecord::kTypeAny:
+        str = "ANY";
+        break;
+    }
+
+    return str;
+}
+
+static void ParseMessage(const Message &aMessage, const Core::AddressInfo *aUnicastDest)
+{
+    DnsMessage *msg = DnsMessage::Allocate();
+
+    msg->ParseFrom(aMessage);
+
+    switch (msg->mHeader.GetType())
+    {
+    case Header::kTypeQuery:
+        msg->mType = kMulticastQuery;
+        VerifyOrQuit(aUnicastDest == nullptr);
+        break;
+
+    case Header::kTypeResponse:
+        if (aUnicastDest == nullptr)
+        {
+            msg->mType = kMulticastResponse;
+        }
+        else
+        {
+            msg->mType        = kUnicastResponse;
+            msg->mUnicastDest = *aUnicastDest;
+        }
+    }
+
+    sDnsMessages.PushAfterTail(*msg);
+}
+
+static void SendQuery(const char *aName,
+                      uint16_t    aRecordType,
+                      uint16_t    aRecordClass = ResourceRecord::kClassInternet,
+                      bool        aTruncated   = false)
+{
+    Message          *message;
+    Header            header;
+    Core::AddressInfo senderAddrInfo;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeQuery);
+    header.SetQuestionCount(1);
+
+    if (aTruncated)
+    {
+        header.SetTruncationFlag();
+    }
+
+    SuccessOrQuit(message->Append(header));
+    SuccessOrQuit(Name::AppendName(aName, *message));
+    SuccessOrQuit(message->Append(Question(aRecordType, aRecordClass)));
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending query for %s %s", aName, RecordTypeToString(aRecordType));
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+static void SendQueryForTwo(const char *aName1, uint16_t aRecordType1, const char *aName2, uint16_t aRecordType2)
+{
+    // Send query with two questions.
+
+    Message          *message;
+    Header            header;
+    Core::AddressInfo senderAddrInfo;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeQuery);
+    header.SetQuestionCount(2);
+
+    SuccessOrQuit(message->Append(header));
+    SuccessOrQuit(Name::AppendName(aName1, *message));
+    SuccessOrQuit(message->Append(Question(aRecordType1, ResourceRecord::kClassInternet)));
+    SuccessOrQuit(Name::AppendName(aName2, *message));
+    SuccessOrQuit(message->Append(Question(aRecordType2, ResourceRecord::kClassInternet)));
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending query for %s %s and %s %s", aName1, RecordTypeToString(aRecordType1), aName2,
+        RecordTypeToString(aRecordType2));
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+static void SendPtrResponse(const char *aName, const char *aPtrName, uint32_t aTtl, Section aSection)
+{
+    Message          *message;
+    Header            header;
+    PtrRecord         ptr;
+    Core::AddressInfo senderAddrInfo;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeResponse);
+
+    switch (aSection)
+    {
+    case kInAnswerSection:
+        header.SetAnswerCount(1);
+        break;
+    case kInAdditionalSection:
+        header.SetAdditionalRecordCount(1);
+        break;
+    }
+
+    SuccessOrQuit(message->Append(header));
+    SuccessOrQuit(Name::AppendName(aName, *message));
+
+    ptr.Init();
+    ptr.SetTtl(aTtl);
+    ptr.SetLength(StringLength(aPtrName, Name::kMaxNameSize) + 1);
+    SuccessOrQuit(message->Append(ptr));
+    SuccessOrQuit(Name::AppendName(aPtrName, *message));
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending PTR response for %s with %s, ttl:%lu", aName, aPtrName, ToUlong(aTtl));
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+static void SendSrvResponse(const char *aServiceName,
+                            const char *aHostName,
+                            uint16_t    aPort,
+                            uint16_t    aPriority,
+                            uint16_t    aWeight,
+                            uint32_t    aTtl,
+                            Section     aSection)
+{
+    Message          *message;
+    Header            header;
+    SrvRecord         srv;
+    Core::AddressInfo senderAddrInfo;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeResponse);
+
+    switch (aSection)
+    {
+    case kInAnswerSection:
+        header.SetAnswerCount(1);
+        break;
+    case kInAdditionalSection:
+        header.SetAdditionalRecordCount(1);
+        break;
+    }
+
+    SuccessOrQuit(message->Append(header));
+    SuccessOrQuit(Name::AppendName(aServiceName, *message));
+
+    srv.Init();
+    srv.SetTtl(aTtl);
+    srv.SetPort(aPort);
+    srv.SetPriority(aPriority);
+    srv.SetWeight(aWeight);
+    srv.SetLength(sizeof(srv) - sizeof(ResourceRecord) + StringLength(aHostName, Name::kMaxNameSize) + 1);
+    SuccessOrQuit(message->Append(srv));
+    SuccessOrQuit(Name::AppendName(aHostName, *message));
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending SRV response for %s, host:%s, port:%u, ttl:%lu", aServiceName, aHostName, aPort, ToUlong(aTtl));
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+static void SendTxtResponse(const char    *aServiceName,
+                            const uint8_t *aTxtData,
+                            uint16_t       aTxtDataLength,
+                            uint32_t       aTtl,
+                            Section        aSection)
+{
+    Message          *message;
+    Header            header;
+    TxtRecord         txt;
+    Core::AddressInfo senderAddrInfo;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeResponse);
+
+    switch (aSection)
+    {
+    case kInAnswerSection:
+        header.SetAnswerCount(1);
+        break;
+    case kInAdditionalSection:
+        header.SetAdditionalRecordCount(1);
+        break;
+    }
+
+    SuccessOrQuit(message->Append(header));
+    SuccessOrQuit(Name::AppendName(aServiceName, *message));
+
+    txt.Init();
+    txt.SetTtl(aTtl);
+    txt.SetLength(aTxtDataLength);
+    SuccessOrQuit(message->Append(txt));
+    SuccessOrQuit(message->AppendBytes(aTxtData, aTxtDataLength));
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending TXT response for %s, len:%u, ttl:%lu", aServiceName, aTxtDataLength, ToUlong(aTtl));
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+static void SendHostAddrResponse(const char *aHostName,
+                                 AddrAndTtl *aAddrAndTtls,
+                                 uint32_t    aNumAddrs,
+                                 bool        aCacheFlush,
+                                 Section     aSection)
+{
+    Message          *message;
+    Header            header;
+    AaaaRecord        record;
+    Core::AddressInfo senderAddrInfo;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeResponse);
+
+    switch (aSection)
+    {
+    case kInAnswerSection:
+        header.SetAnswerCount(aNumAddrs);
+        break;
+    case kInAdditionalSection:
+        header.SetAdditionalRecordCount(aNumAddrs);
+        break;
+    }
+
+    SuccessOrQuit(message->Append(header));
+
+    record.Init();
+
+    if (aCacheFlush)
+    {
+        record.SetClass(record.GetClass() | kClassCacheFlushFlag);
+    }
+
+    Log("Sending AAAA response for %s numAddrs:%u, cach-flush:%u", aHostName, aNumAddrs, aCacheFlush);
+
+    for (uint32_t index = 0; index < aNumAddrs; index++)
+    {
+        record.SetTtl(aAddrAndTtls[index].mTtl);
+        record.SetAddress(aAddrAndTtls[index].mAddress);
+
+        SuccessOrQuit(Name::AppendName(aHostName, *message));
+        SuccessOrQuit(message->Append(record));
+
+        Log(" - %s, ttl:%lu", aAddrAndTtls[index].mAddress.ToString().AsCString(), ToUlong(aAddrAndTtls[index].mTtl));
+    }
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+static void SendResponseWithEmptyKey(const char *aName, Section aSection)
+{
+    Message          *message;
+    Header            header;
+    ResourceRecord    record;
+    Core::AddressInfo senderAddrInfo;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeResponse);
+
+    switch (aSection)
+    {
+    case kInAnswerSection:
+        header.SetAnswerCount(1);
+        break;
+    case kInAdditionalSection:
+        header.SetAdditionalRecordCount(1);
+        break;
+    }
+
+    SuccessOrQuit(message->Append(header));
+    SuccessOrQuit(Name::AppendName(aName, *message));
+
+    record.Init(ResourceRecord::kTypeKey);
+    record.SetTtl(4500);
+    record.SetLength(0);
+    SuccessOrQuit(message->Append(record));
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending response with empty key for %s", aName);
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+struct KnownAnswer
+{
+    const char *mPtrAnswer;
+    uint32_t    mTtl;
+};
+
+static void SendPtrQueryWithKnownAnswers(const char *aName, const KnownAnswer *aKnownAnswers, uint16_t aNumAnswers)
+{
+    Message          *message;
+    Header            header;
+    Core::AddressInfo senderAddrInfo;
+    uint16_t          nameOffset;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeQuery);
+    header.SetQuestionCount(1);
+    header.SetAnswerCount(aNumAnswers);
+
+    SuccessOrQuit(message->Append(header));
+    nameOffset = message->GetLength();
+    SuccessOrQuit(Name::AppendName(aName, *message));
+    SuccessOrQuit(message->Append(Question(ResourceRecord::kTypePtr, ResourceRecord::kClassInternet)));
+
+    for (uint16_t index = 0; index < aNumAnswers; index++)
+    {
+        PtrRecord ptr;
+
+        ptr.Init();
+        ptr.SetTtl(aKnownAnswers[index].mTtl);
+        ptr.SetLength(StringLength(aKnownAnswers[index].mPtrAnswer, Name::kMaxNameSize) + 1);
+
+        SuccessOrQuit(Name::AppendPointerLabel(nameOffset, *message));
+        SuccessOrQuit(message->Append(ptr));
+        SuccessOrQuit(Name::AppendName(aKnownAnswers[index].mPtrAnswer, *message));
+    }
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending query for %s PTR with %u known-answers", aName, aNumAnswers);
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+static void SendEmtryPtrQueryWithKnownAnswers(const char *aName, const KnownAnswer *aKnownAnswers, uint16_t aNumAnswers)
+{
+    Message          *message;
+    Header            header;
+    Core::AddressInfo senderAddrInfo;
+    uint16_t          nameOffset = 0;
+
+    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
+    VerifyOrQuit(message != nullptr);
+
+    header.Clear();
+    header.SetType(Header::kTypeQuery);
+    header.SetAnswerCount(aNumAnswers);
+
+    SuccessOrQuit(message->Append(header));
+
+    for (uint16_t index = 0; index < aNumAnswers; index++)
+    {
+        PtrRecord ptr;
+
+        ptr.Init();
+        ptr.SetTtl(aKnownAnswers[index].mTtl);
+        ptr.SetLength(StringLength(aKnownAnswers[index].mPtrAnswer, Name::kMaxNameSize) + 1);
+
+        if (nameOffset == 0)
+        {
+            nameOffset = message->GetLength();
+            SuccessOrQuit(Name::AppendName(aName, *message));
+        }
+        else
+        {
+            SuccessOrQuit(Name::AppendPointerLabel(nameOffset, *message));
+        }
+
+        SuccessOrQuit(message->Append(ptr));
+        SuccessOrQuit(Name::AppendName(aKnownAnswers[index].mPtrAnswer, *message));
+    }
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    Log("Sending empty query with %u known-answers for %s", aNumAnswers, aName);
+
+    otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// `otPlatLog`
+
+extern "C" {
+
+#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);
+    OT_UNUSED_VARIABLE(aFormat);
+
+#if ENABLE_TEST_LOG
+    va_list args;
+
+    printf("   ");
+    va_start(args, aFormat);
+    vprintf(aFormat, args);
+    va_end(args);
+
+    printf("\n");
+#endif
+}
+
+#endif
+
+//----------------------------------------------------------------------------------------------------------------------
+// `otPlatAlarm`
+
+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; }
+
+//----------------------------------------------------------------------------------------------------------------------
+// Heap allocation
+
+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
+
+//----------------------------------------------------------------------------------------------------------------------
+// `otPlatMdns`
+
+otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    VerifyOrQuit(aInstance == sInstance);
+    sInfraIfIndex = aInfraIfIndex;
+
+    Log("otPlatMdnsSetListeningEnabled(%s)", aEnable ? "true" : "false");
+
+    return kErrorNone;
+}
+
+void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex)
+{
+    Message          &message = AsCoreType(aMessage);
+    Core::AddressInfo senderAddrInfo;
+
+    VerifyOrQuit(aInfraIfIndex == sInfraIfIndex);
+
+    Log("otPlatMdnsSendMulticast(msg-len:%u)", message.GetLength());
+    ParseMessage(message, nullptr);
+
+    // Pass the multicast message back.
+
+    SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+    senderAddrInfo.mPort         = kMdnsPort;
+    senderAddrInfo.mInfraIfIndex = 0;
+
+    otPlatMdnsHandleReceive(sInstance, aMessage, /* aIsUnicast */ false, &senderAddrInfo);
+}
+
+void otPlatMdnsSendUnicast(otInstance *aInstance, otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress)
+{
+    Message                 &message = AsCoreType(aMessage);
+    const Core::AddressInfo &address = AsCoreType(aAddress);
+    Ip6::Address             deviceAddress;
+
+    Log("otPlatMdnsSendUnicast() - [%s]:%u", address.GetAddress().ToString().AsCString(), address.mPort);
+    ParseMessage(message, AsCoreTypePtr(aAddress));
+
+    SuccessOrQuit(deviceAddress.FromString(kDeviceIp6Address));
+
+    if ((address.GetAddress() == deviceAddress) && (address.mPort == kMdnsPort))
+    {
+        Core::AddressInfo senderAddrInfo;
+
+        SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
+        senderAddrInfo.mPort         = kMdnsPort;
+        senderAddrInfo.mInfraIfIndex = 0;
+
+        Log("otPlatMdnsSendUnicast() - unicast msg matches this device address, passing it back");
+        otPlatMdnsHandleReceive(sInstance, &message, /* aIsUnicast */ true, &senderAddrInfo);
+    }
+    else
+    {
+        message.Free();
+    }
+}
+
+} // extern "C"
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void ProcessTasklets(void)
+{
+    while (otTaskletsArePending(sInstance))
+    {
+        otTaskletsProcess(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))
+    {
+        ProcessTasklets();
+        sNow = sAlarmTime;
+        otPlatAlarmMilliFired(sInstance);
+    }
+
+    ProcessTasklets();
+    sNow = time;
+}
+
+Core *InitTest(void)
+{
+    sNow     = 0;
+    sAlarmOn = false;
+
+    sDnsMessages.Clear();
+
+    for (RegCallback &regCallbck : sRegCallbacks)
+    {
+        regCallbck.Reset();
+    }
+
+    sConflictCallback.Reset();
+
+    sInstance = testInitInstance();
+
+    VerifyOrQuit(sInstance != nullptr);
+
+    return &sInstance->Get<Core>();
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+
+static const uint8_t kKey1[]         = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};
+static const uint8_t kKey2[]         = {0x12, 0x34, 0x56};
+static const uint8_t kTxtData1[]     = {3, 'a', '=', '1', 0};
+static const uint8_t kTxtData2[]     = {1, 'b', 0};
+static const uint8_t kEmptyTxtData[] = {0};
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestHostReg(void)
+{
+    Core             *mdns = InitTest();
+    Core::Host        host;
+    Ip6::Address      hostAddresses[3];
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     hostFullName;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestHostReg");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(hostAddresses[0].FromString("fd00::aaaa"));
+    SuccessOrQuit(hostAddresses[1].FromString("fd00::bbbb"));
+    SuccessOrQuit(hostAddresses[2].FromString("fd00::cccc"));
+
+    host.mHostName        = "myhost";
+    host.mAddresses       = hostAddresses;
+    host.mAddressesLength = 3;
+    host.mTtl             = 1500;
+
+    hostFullName.Append("%s.local.", host.mHostName);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a `HostEntry`, check probes and announcements");
+
+    sDnsMessages.Clear();
+
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterHost(host, 0, HandleSuccessCallback));
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 3, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ (probeCount == 0));
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(host, kInAnswerSection);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for AAAA record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(hostFullName.AsCString(), ResourceRecord::kTypeAaaa);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(host, kInAnswerSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for ANY record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(hostFullName.AsCString(), ResourceRecord::kTypeAny);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(host, kInAnswerSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for non-existing record and validate the response with NSEC");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(hostFullName.AsCString(), ResourceRecord::kTypeA);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 1);
+    VerifyOrQuit(dnsMsg->mAdditionalRecords.ContainsNsec(hostFullName, ResourceRecord::kTypeAaaa));
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Update number of host addresses and validate new announcements");
+
+    host.mAddressesLength = 2;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterHost(host, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(host, kInAnswerSection);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Change the addresses and validate the first announce");
+
+    host.mAddressesLength = 3;
+
+    sRegCallbacks[0].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterHost(host, 0, HandleSuccessCallback));
+
+    AdvanceTime(300);
+    VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+    VerifyOrQuit(!sDnsMessages.IsEmpty());
+    dnsMsg = sDnsMessages.GetHead();
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(host, kInAnswerSection);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    Log("Change the address list again before second announce");
+
+    host.mAddressesLength = 1;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterHost(host, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(host, kInAnswerSection);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Change `HostEntry` TTL and validate announcements");
+
+    host.mTtl = 120;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterHost(host, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(host, kInAnswerSection);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for AAAA record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(hostFullName.AsCString(), ResourceRecord::kTypeAaaa);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(host, kInAnswerSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Unregister the host and validate the goodbye announces");
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->UnregisterHost(host));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->Validate(host, kInAnswerSection, kGoodBye);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    AdvanceTime(15000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestKeyReg(void)
+{
+    Core             *mdns = InitTest();
+    Core::Key         key;
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestKeyReg");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    // Run all tests twice. first with key for a host name, followed
+    // by key for service instance name.
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        DnsNameString fullName;
+
+        if (iter == 0)
+        {
+            Log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
+            Log("Registering key for 'myhost' host name");
+            key.mName        = "myhost";
+            key.mServiceType = nullptr;
+
+            fullName.Append("%s.local.", key.mName);
+        }
+        else
+        {
+            Log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
+            Log("Registering key for 'mysrv._srv._udo' service name");
+
+            key.mName        = "mysrv";
+            key.mServiceType = "_srv._udp";
+
+            fullName.Append("%s.%s.local.", key.mName, key.mServiceType);
+        }
+
+        key.mKeyData       = kKey1;
+        key.mKeyDataLength = sizeof(kKey1);
+        key.mTtl           = 8000;
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register a key record and check probes and announcements");
+
+        sDnsMessages.Clear();
+
+        sRegCallbacks[0].Reset();
+        SuccessOrQuit(mdns->RegisterKey(key, 0, HandleSuccessCallback));
+
+        for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+        {
+            sDnsMessages.Clear();
+
+            VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+            AdvanceTime(250);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 1, /* Addnl */ 0);
+            dnsMsg->ValidateAsProbeFor(key, /* aUnicastRequest */ (probeCount == 0));
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            sDnsMessages.Clear();
+
+            AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+            VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+            dnsMsg->Validate(key, kInAnswerSection);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Send a query for KEY record and validate the response");
+
+        AdvanceTime(2000);
+
+        sDnsMessages.Clear();
+        SendQuery(fullName.AsCString(), ResourceRecord::kTypeKey);
+
+        AdvanceTime(1000);
+
+        dnsMsg = sDnsMessages.GetHead();
+        VerifyOrQuit(dnsMsg != nullptr);
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(key, kInAnswerSection);
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Send a query for ANY record and validate the response");
+
+        AdvanceTime(2000);
+
+        sDnsMessages.Clear();
+        SendQuery(fullName.AsCString(), ResourceRecord::kTypeAny);
+
+        AdvanceTime(1000);
+
+        dnsMsg = sDnsMessages.GetHead();
+        VerifyOrQuit(dnsMsg != nullptr);
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(key, kInAnswerSection);
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Send a query for non-existing record and validate the response with NSEC");
+
+        AdvanceTime(2000);
+
+        sDnsMessages.Clear();
+        SendQuery(fullName.AsCString(), ResourceRecord::kTypeA);
+
+        AdvanceTime(1000);
+
+        dnsMsg = sDnsMessages.GetHead();
+        VerifyOrQuit(dnsMsg != nullptr);
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 1);
+        VerifyOrQuit(dnsMsg->mAdditionalRecords.ContainsNsec(fullName, ResourceRecord::kTypeKey));
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Change the TTL");
+
+        key.mTtl = 0; // Use default
+
+        sRegCallbacks[1].Reset();
+        sDnsMessages.Clear();
+        SuccessOrQuit(mdns->RegisterKey(key, 1, HandleSuccessCallback));
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+            VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+            dnsMsg->Validate(key, kInAnswerSection);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+            sDnsMessages.Clear();
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Change the key");
+
+        key.mKeyData       = kKey2;
+        key.mKeyDataLength = sizeof(kKey2);
+
+        sRegCallbacks[1].Reset();
+        sDnsMessages.Clear();
+        SuccessOrQuit(mdns->RegisterKey(key, 1, HandleSuccessCallback));
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+            VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+            dnsMsg->Validate(key, kInAnswerSection);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+            sDnsMessages.Clear();
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Unregister the key and validate the goodbye announces");
+
+        sDnsMessages.Clear();
+        SuccessOrQuit(mdns->UnregisterKey(key));
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+            dnsMsg->Validate(key, kInAnswerSection, kGoodBye);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+            sDnsMessages.Clear();
+        }
+    }
+
+    AdvanceTime(15000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestServiceReg(void)
+{
+    Core             *mdns = InitTest();
+    Core::Service     service;
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     fullServiceName;
+    DnsNameString     fullServiceType;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestServiceReg");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    service.mHostName            = "myhost";
+    service.mServiceInstance     = "myservice";
+    service.mServiceType         = "_srv._udp";
+    service.mSubTypeLabels       = nullptr;
+    service.mSubTypeLabelsLength = 0;
+    service.mTxtData             = kTxtData1;
+    service.mTxtDataLength       = sizeof(kTxtData1);
+    service.mPort                = 1234;
+    service.mPriority            = 1;
+    service.mWeight              = 2;
+    service.mTtl                 = 1000;
+
+    fullServiceName.Append("%s.%s.local.", service.mServiceInstance, service.mServiceType);
+    fullServiceType.Append("%s.local.", service.mServiceType);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a `ServiceEntry`, check probes and announcements");
+
+    sDnsMessages.Clear();
+
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterService(service, 0, HandleSuccessCallback));
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ (probeCount == 0));
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 4, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for SRV record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceName.AsCString(), ResourceRecord::kTypeSrv);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckSrv);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for TXT record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceName.AsCString(), ResourceRecord::kTypeTxt);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for ANY record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceName.AsCString(), ResourceRecord::kTypeAny);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for PTR record for service type and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceType.AsCString(), ResourceRecord::kTypePtr);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 2);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service, kInAdditionalSection, kCheckSrv | kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for PTR record for `services._dns-sd` and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery("_services._dns-sd._udp.local.", ResourceRecord::kTypePtr);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckServicesPtr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Update service port number and validate new announcements of SRV record");
+
+    service.mPort = 4567;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Update TXT data and validate new announcements of TXT record");
+
+    service.mTxtData       = nullptr;
+    service.mTxtDataLength = 0;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckTxt);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Update both service and TXT data and validate new announcements of both records");
+
+    service.mTxtData       = kTxtData2;
+    service.mTxtDataLength = sizeof(kTxtData2);
+    service.mWeight        = 0;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Update service host name and validate new announcements of SRV record");
+
+    service.mHostName = "newhost";
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Update TTL and validate new announcements of SRV, TXT and PTR records");
+
+    service.mTtl = 0;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Unregister the service and validate the goodbye announces");
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->UnregisterService(service));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr, kGoodBye);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    AdvanceTime(15000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestUnregisterBeforeProbeFinished(void)
+{
+    const uint8_t kKey1[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};
+
+    Core             *mdns = InitTest();
+    Core::Host        host;
+    Core::Service     service;
+    Core::Key         key;
+    Ip6::Address      hostAddresses[3];
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestUnregisterBeforeProbeFinished");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(hostAddresses[0].FromString("fd00::aaaa"));
+    SuccessOrQuit(hostAddresses[1].FromString("fd00::bbbb"));
+    SuccessOrQuit(hostAddresses[2].FromString("fd00::cccc"));
+
+    host.mHostName        = "myhost";
+    host.mAddresses       = hostAddresses;
+    host.mAddressesLength = 3;
+    host.mTtl             = 1500;
+
+    service.mHostName            = "myhost";
+    service.mServiceInstance     = "myservice";
+    service.mServiceType         = "_srv._udp";
+    service.mSubTypeLabels       = nullptr;
+    service.mSubTypeLabelsLength = 0;
+    service.mTxtData             = kTxtData1;
+    service.mTxtDataLength       = sizeof(kTxtData1);
+    service.mPort                = 1234;
+    service.mPriority            = 1;
+    service.mWeight              = 2;
+    service.mTtl                 = 1000;
+
+    key.mName          = "mysrv";
+    key.mServiceType   = "_srv._udp";
+    key.mKeyData       = kKey1;
+    key.mKeyDataLength = sizeof(kKey1);
+    key.mTtl           = 8000;
+
+    // Repeat the same test 3 times for host and service and key registration.
+
+    for (uint8_t iter = 0; iter < 3; iter++)
+    {
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register an entry, check for the first two probes");
+
+        sDnsMessages.Clear();
+
+        sRegCallbacks[0].Reset();
+
+        switch (iter)
+        {
+        case 0:
+            SuccessOrQuit(mdns->RegisterHost(host, 0, HandleSuccessCallback));
+            break;
+        case 1:
+            SuccessOrQuit(mdns->RegisterService(service, 0, HandleSuccessCallback));
+            break;
+        case 2:
+            SuccessOrQuit(mdns->RegisterKey(key, 0, HandleSuccessCallback));
+            break;
+        }
+
+        for (uint8_t probeCount = 0; probeCount < 2; probeCount++)
+        {
+            sDnsMessages.Clear();
+
+            VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+            AdvanceTime(250);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+
+            switch (iter)
+            {
+            case 0:
+                dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 3, /* Addnl */ 0);
+                dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ (probeCount == 0));
+                break;
+            case 1:
+                dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+                dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ (probeCount == 0));
+                break;
+            case 2:
+                dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 1, /* Addnl */ 0);
+                dnsMsg->ValidateAsProbeFor(key, /* aUnicastRequest */ (probeCount == 0));
+                break;
+            }
+
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        sDnsMessages.Clear();
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Unregister the entry before the last probe and make sure probing stops");
+
+        switch (iter)
+        {
+        case 0:
+            SuccessOrQuit(mdns->UnregisterHost(host));
+            break;
+        case 1:
+            SuccessOrQuit(mdns->UnregisterService(service));
+            break;
+        case 2:
+            SuccessOrQuit(mdns->UnregisterKey(key));
+            break;
+        }
+
+        AdvanceTime(20 * 1000);
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+    }
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestServiceSubTypeReg(void)
+{
+    static const char *const kSubTypes1[] = {"_s1", "_r2", "_vXy", "_last"};
+    static const char *const kSubTypes2[] = {"_vxy", "_r1", "_r2", "_zzz"};
+
+    Core             *mdns = InitTest();
+    Core::Service     service;
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     fullServiceName;
+    DnsNameString     fullServiceType;
+    DnsNameString     fullSubServiceType;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestServiceSubTypeReg");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    service.mHostName            = "tarnished";
+    service.mServiceInstance     = "elden";
+    service.mServiceType         = "_ring._udp";
+    service.mSubTypeLabels       = kSubTypes1;
+    service.mSubTypeLabelsLength = 3;
+    service.mTxtData             = kTxtData1;
+    service.mTxtDataLength       = sizeof(kTxtData1);
+    service.mPort                = 1234;
+    service.mPriority            = 1;
+    service.mWeight              = 2;
+    service.mTtl                 = 6000;
+
+    fullServiceName.Append("%s.%s.local.", service.mServiceInstance, service.mServiceType);
+    fullServiceType.Append("%s.local.", service.mServiceType);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a `ServiceEntry` with sub-types, check probes and announcements");
+
+    sDnsMessages.Clear();
+
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterService(service, 0, HandleSuccessCallback));
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ (probeCount == 0));
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 7, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+
+        for (uint16_t index = 0; index < service.mSubTypeLabelsLength; index++)
+        {
+            dnsMsg->ValidateSubType(service.mSubTypeLabels[index], service);
+        }
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for SRV record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceName.AsCString(), ResourceRecord::kTypeSrv);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckSrv);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for TXT record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceName.AsCString(), ResourceRecord::kTypeTxt);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for ANY record and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceName.AsCString(), ResourceRecord::kTypeAny);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 1);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for PTR record for service type and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceType.AsCString(), ResourceRecord::kTypePtr);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 2);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service, kInAdditionalSection, kCheckSrv | kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for PTR record for `services._dns-sd` and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery("_services._dns-sd._udp.local.", ResourceRecord::kTypePtr);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckServicesPtr);
+
+    for (uint16_t index = 0; index < service.mSubTypeLabelsLength; index++)
+    {
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Send a PTR query for sub-type `%s` and validate the response", service.mSubTypeLabels[index]);
+
+        fullSubServiceType.Clear();
+        fullSubServiceType.Append("%s._sub.%s", service.mSubTypeLabels[index], fullServiceType.AsCString());
+
+        AdvanceTime(2000);
+
+        sDnsMessages.Clear();
+        SendQuery(fullSubServiceType.AsCString(), ResourceRecord::kTypePtr);
+
+        AdvanceTime(1000);
+
+        dnsMsg = sDnsMessages.GetHead();
+        VerifyOrQuit(dnsMsg != nullptr);
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateSubType(service.mSubTypeLabels[index], service);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query for non-existing sub-type and validate there is no response");
+
+    AdvanceTime(2000);
+
+    fullSubServiceType.Clear();
+    fullSubServiceType.Append("_none._sub.%s", fullServiceType.AsCString());
+
+    sDnsMessages.Clear();
+    SendQuery(fullSubServiceType.AsCString(), ResourceRecord::kTypePtr);
+
+    AdvanceTime(2000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a new sub-type and validate announcements of PTR record for it");
+
+    service.mSubTypeLabelsLength = 4;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateSubType(service.mSubTypeLabels[3], service);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Remove a previous sub-type and validate announcements of its removal");
+
+    service.mSubTypeLabels++;
+    service.mSubTypeLabelsLength = 3;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateSubType(kSubTypes1[0], service, kGoodBye);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Update TTL and validate announcement of all records");
+
+    service.mTtl = 0;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 6, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr);
+
+        for (uint16_t index = 0; index < service.mSubTypeLabelsLength; index++)
+        {
+            dnsMsg->ValidateSubType(service.mSubTypeLabels[index], service);
+        }
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Add and remove sub-types at the same time and check proper announcements");
+
+    // Registered sub-types: _r2, _vXy, _last
+    // New sub-types list  : _vxy, _r1, _r2, _zzz
+    //
+    // Should announce removal of `_last` and addition of
+    // `_r1` and `_zzz`. The `_vxy` should match with `_vXy`.
+
+    service.mSubTypeLabels       = kSubTypes2;
+    service.mSubTypeLabelsLength = 4;
+
+    sRegCallbacks[1].Reset();
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 0);
+
+        dnsMsg->ValidateSubType(kSubTypes1[3], service, kGoodBye);
+        dnsMsg->ValidateSubType(kSubTypes2[1], service);
+        dnsMsg->ValidateSubType(kSubTypes2[3], service);
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Unregister the service and validate the goodbye announces for service and its sub-types");
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->UnregisterService(service));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 7, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr, kGoodBye);
+
+        for (uint16_t index = 0; index < service.mSubTypeLabelsLength; index++)
+        {
+            dnsMsg->ValidateSubType(service.mSubTypeLabels[index], service, kGoodBye);
+        }
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        sDnsMessages.Clear();
+    }
+
+    AdvanceTime(15000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+void TestHostOrServiceAndKeyReg(void)
+{
+    Core             *mdns = InitTest();
+    Core::Host        host;
+    Core::Service     service;
+    Core::Key         key;
+    Ip6::Address      hostAddresses[2];
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestHostOrServiceAndKeyReg");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(hostAddresses[0].FromString("fd00::1"));
+    SuccessOrQuit(hostAddresses[1].FromString("fd00::2"));
+
+    host.mHostName        = "myhost";
+    host.mAddresses       = hostAddresses;
+    host.mAddressesLength = 2;
+    host.mTtl             = 5000;
+
+    key.mKeyData       = kKey1;
+    key.mKeyDataLength = sizeof(kKey1);
+    key.mTtl           = 80000;
+
+    service.mHostName            = "myhost";
+    service.mServiceInstance     = "myservice";
+    service.mServiceType         = "_srv._udp";
+    service.mSubTypeLabels       = nullptr;
+    service.mSubTypeLabelsLength = 0;
+    service.mTxtData             = kTxtData1;
+    service.mTxtDataLength       = sizeof(kTxtData1);
+    service.mPort                = 1234;
+    service.mPriority            = 1;
+    service.mWeight              = 2;
+    service.mTtl                 = 1000;
+
+    // Run all test step twice, first time registering host and key,
+    // second time registering service and key.
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        if (iter == 0)
+        {
+            key.mName        = host.mHostName;
+            key.mServiceType = nullptr;
+        }
+        else
+        {
+            key.mName        = service.mServiceInstance;
+            key.mServiceType = service.mServiceType;
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register a %s entry, check the first probe is sent", iter == 0 ? "host" : "service");
+
+        sDnsMessages.Clear();
+
+        sRegCallbacks[0].Reset();
+
+        if (iter == 0)
+        {
+            SuccessOrQuit(mdns->RegisterHost(host, 0, HandleSuccessCallback));
+        }
+        else
+        {
+            SuccessOrQuit(mdns->RegisterService(service, 0, HandleSuccessCallback));
+        }
+
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+
+        if (iter == 0)
+        {
+            dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ true);
+        }
+        else
+        {
+            dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ true);
+        }
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register a `KeyEntry` for same name, check that probes continue");
+
+        sRegCallbacks[1].Reset();
+        SuccessOrQuit(mdns->RegisterKey(key, 1, HandleSuccessCallback));
+
+        for (uint8_t probeCount = 1; probeCount < 3; probeCount++)
+        {
+            sDnsMessages.Clear();
+
+            VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+            VerifyOrQuit(!sRegCallbacks[1].mWasCalled);
+
+            AdvanceTime(250);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 3, /* Addnl */ 0);
+
+            if (iter == 0)
+            {
+                dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ false);
+            }
+            else
+            {
+                dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ false);
+            }
+
+            dnsMsg->ValidateAsProbeFor(key, /* aUnicastRequest */ (probeCount == 0));
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Validate Announces for both entry and key");
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            sDnsMessages.Clear();
+
+            AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+            VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+            VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+
+            if (iter == 0)
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 1);
+                dnsMsg->Validate(host, kInAnswerSection);
+            }
+            else
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 5, /* Auth */ 0, /* Addnl */ 1);
+                dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+            }
+
+            dnsMsg->Validate(key, kInAnswerSection);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Unregister the entry and validate its goodbye announces");
+
+        sDnsMessages.Clear();
+
+        if (iter == 0)
+        {
+            SuccessOrQuit(mdns->UnregisterHost(host));
+        }
+        else
+        {
+            SuccessOrQuit(mdns->UnregisterService(service));
+        }
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+
+            if (iter == 0)
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 1);
+                dnsMsg->Validate(host, kInAnswerSection, kGoodBye);
+            }
+            else
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 1);
+                dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr, kGoodBye);
+            }
+
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+            sDnsMessages.Clear();
+        }
+
+        AdvanceTime(15000);
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register the entry again, validate its announcements");
+
+        sDnsMessages.Clear();
+
+        sRegCallbacks[2].Reset();
+
+        if (iter == 0)
+        {
+            SuccessOrQuit(mdns->RegisterHost(host, 2, HandleSuccessCallback));
+        }
+        else
+        {
+            SuccessOrQuit(mdns->RegisterService(service, 2, HandleSuccessCallback));
+        }
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            sDnsMessages.Clear();
+
+            AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+            VerifyOrQuit(sRegCallbacks[2].mWasCalled);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+
+            if (iter == 0)
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 1);
+                dnsMsg->Validate(host, kInAnswerSection);
+            }
+            else
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 4, /* Auth */ 0, /* Addnl */ 1);
+                dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+            }
+
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Unregister the key and validate its goodbye announcements");
+
+        sDnsMessages.Clear();
+        SuccessOrQuit(mdns->UnregisterKey(key));
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            AdvanceTime((anncCount == 0) ? 0 : (1U << (anncCount - 1)) * 1000);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+            dnsMsg->Validate(key, kInAnswerSection, kGoodBye);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+            sDnsMessages.Clear();
+        }
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register the key again, validate its announcements");
+
+        sDnsMessages.Clear();
+
+        sRegCallbacks[3].Reset();
+        SuccessOrQuit(mdns->RegisterKey(key, 3, HandleSuccessCallback));
+
+        for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+        {
+            sDnsMessages.Clear();
+
+            AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+            VerifyOrQuit(sRegCallbacks[3].mWasCalled);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+            dnsMsg->Validate(key, kInAnswerSection);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        sDnsMessages.Clear();
+        AdvanceTime(15000);
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Unregister key first, validate two of its goodbye announcements");
+
+        sDnsMessages.Clear();
+
+        SuccessOrQuit(mdns->UnregisterKey(key));
+
+        for (uint8_t anncCount = 0; anncCount < 2; anncCount++)
+        {
+            sDnsMessages.Clear();
+
+            AdvanceTime((anncCount == 0) ? 1 : (1U << (anncCount - 1)) * 1000);
+
+            VerifyOrQuit(!sDnsMessages.IsEmpty());
+            dnsMsg = sDnsMessages.GetHead();
+            dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+            dnsMsg->Validate(key, kInAnswerSection, kGoodBye);
+            VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+        }
+
+        Log("Unregister entry as well");
+
+        if (iter == 0)
+        {
+            SuccessOrQuit(mdns->UnregisterHost(host));
+        }
+        else
+        {
+            SuccessOrQuit(mdns->UnregisterService(service));
+        }
+
+        AdvanceTime(15000);
+
+        for (uint16_t anncCount = 0; anncCount < 4; anncCount++)
+        {
+            dnsMsg = dnsMsg->GetNext();
+            VerifyOrQuit(dnsMsg != nullptr);
+
+            if (anncCount == 2)
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+                dnsMsg->Validate(key, kInAnswerSection, kGoodBye);
+            }
+            else if (iter == 0)
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 0);
+                dnsMsg->Validate(host, kInAnswerSection, kGoodBye);
+            }
+            else
+            {
+                dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 3, /* Auth */ 0, /* Addnl */ 0);
+                dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr, kGoodBye);
+            }
+        }
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        sDnsMessages.Clear();
+        AdvanceTime(15000);
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+    }
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestQuery(void)
+{
+    static const char *const kSubTypes[] = {"_s", "_r"};
+
+    Core             *mdns = InitTest();
+    Core::Host        host1;
+    Core::Host        host2;
+    Core::Service     service1;
+    Core::Service     service2;
+    Core::Service     service3;
+    Core::Key         key1;
+    Core::Key         key2;
+    Ip6::Address      host1Addresses[3];
+    Ip6::Address      host2Addresses[2];
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     host1FullName;
+    DnsNameString     host2FullName;
+    DnsNameString     service1FullName;
+    DnsNameString     service2FullName;
+    DnsNameString     service3FullName;
+    KnownAnswer       knownAnswers[2];
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestQuery");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(host1Addresses[0].FromString("fd00::1:aaaa"));
+    SuccessOrQuit(host1Addresses[1].FromString("fd00::1:bbbb"));
+    SuccessOrQuit(host1Addresses[2].FromString("fd00::1:cccc"));
+    host1.mHostName        = "host1";
+    host1.mAddresses       = host1Addresses;
+    host1.mAddressesLength = 3;
+    host1.mTtl             = 1500;
+    host1FullName.Append("%s.local.", host1.mHostName);
+
+    SuccessOrQuit(host2Addresses[0].FromString("fd00::2:eeee"));
+    SuccessOrQuit(host2Addresses[1].FromString("fd00::2:ffff"));
+    host2.mHostName        = "host2";
+    host2.mAddresses       = host2Addresses;
+    host2.mAddressesLength = 2;
+    host2.mTtl             = 1500;
+    host2FullName.Append("%s.local.", host2.mHostName);
+
+    service1.mHostName            = host1.mHostName;
+    service1.mServiceInstance     = "srv1";
+    service1.mServiceType         = "_srv._udp";
+    service1.mSubTypeLabels       = kSubTypes;
+    service1.mSubTypeLabelsLength = 2;
+    service1.mTxtData             = kTxtData1;
+    service1.mTxtDataLength       = sizeof(kTxtData1);
+    service1.mPort                = 1111;
+    service1.mPriority            = 0;
+    service1.mWeight              = 0;
+    service1.mTtl                 = 1500;
+    service1FullName.Append("%s.%s.local.", service1.mServiceInstance, service1.mServiceType);
+
+    service2.mHostName            = host1.mHostName;
+    service2.mServiceInstance     = "srv2";
+    service2.mServiceType         = "_tst._tcp";
+    service2.mSubTypeLabels       = nullptr;
+    service2.mSubTypeLabelsLength = 0;
+    service2.mTxtData             = nullptr;
+    service2.mTxtDataLength       = 0;
+    service2.mPort                = 2222;
+    service2.mPriority            = 2;
+    service2.mWeight              = 2;
+    service2.mTtl                 = 1500;
+    service2FullName.Append("%s.%s.local.", service2.mServiceInstance, service2.mServiceType);
+
+    service3.mHostName            = host2.mHostName;
+    service3.mServiceInstance     = "srv3";
+    service3.mServiceType         = "_srv._udp";
+    service3.mSubTypeLabels       = kSubTypes;
+    service3.mSubTypeLabelsLength = 1;
+    service3.mTxtData             = kTxtData2;
+    service3.mTxtDataLength       = sizeof(kTxtData2);
+    service3.mPort                = 3333;
+    service3.mPriority            = 3;
+    service3.mWeight              = 3;
+    service3.mTtl                 = 1500;
+    service3FullName.Append("%s.%s.local.", service3.mServiceInstance, service3.mServiceType);
+
+    key1.mName          = host2.mHostName;
+    key1.mServiceType   = nullptr;
+    key1.mKeyData       = kKey1;
+    key1.mKeyDataLength = sizeof(kKey1);
+    key1.mTtl           = 8000;
+
+    key2.mName          = service3.mServiceInstance;
+    key2.mServiceType   = service3.mServiceType;
+    key2.mKeyData       = kKey1;
+    key2.mKeyDataLength = sizeof(kKey1);
+    key2.mTtl           = 8000;
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register 2 hosts and 3 services and 2 keys");
+
+    sDnsMessages.Clear();
+
+    for (RegCallback &regCallbck : sRegCallbacks)
+    {
+        regCallbck.Reset();
+    }
+
+    SuccessOrQuit(mdns->RegisterHost(host1, 0, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterHost(host2, 1, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterService(service1, 2, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterService(service2, 3, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterService(service3, 4, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterKey(key1, 5, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterKey(key2, 6, HandleSuccessCallback));
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate probes for all entries");
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+
+        for (uint16_t index = 0; index < 7; index++)
+        {
+            VerifyOrQuit(!sRegCallbacks[index].mWasCalled);
+        }
+
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 5, /* Ans */ 0, /* Auth */ 13, /* Addnl */ 0);
+
+        dnsMsg->ValidateAsProbeFor(host1, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(host2, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(service1, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(service2, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(service3, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(key1, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(key2, /* aUnicastRequest */ (probeCount == 0));
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate announcements for all entries");
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+
+        for (uint16_t index = 0; index < 7; index++)
+        {
+            VerifyOrQuit(sRegCallbacks[index].mWasCalled);
+        }
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 21, /* Auth */ 0, /* Addnl */ 5);
+
+        dnsMsg->Validate(host1, kInAnswerSection);
+        dnsMsg->Validate(host2, kInAnswerSection);
+        dnsMsg->Validate(service1, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+        dnsMsg->Validate(service2, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+        dnsMsg->Validate(service2, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+        dnsMsg->Validate(key1, kInAnswerSection);
+        dnsMsg->Validate(key2, kInAnswerSection);
+
+        for (uint16_t index = 0; index < service1.mSubTypeLabelsLength; index++)
+        {
+            dnsMsg->ValidateSubType(service1.mSubTypeLabels[index], service1);
+        }
+
+        for (uint16_t index = 0; index < service3.mSubTypeLabelsLength; index++)
+        {
+            dnsMsg->ValidateSubType(service3.mSubTypeLabels[index], service3);
+        }
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    sDnsMessages.Clear();
+    AdvanceTime(15000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query (browse) for `_srv._udp` and validate two answers and additional data");
+
+    AdvanceTime(2000);
+    sDnsMessages.Clear();
+
+    SendQuery("_srv._udp.local.", ResourceRecord::kTypePtr);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 9);
+
+    dnsMsg->Validate(service1, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service1, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(host1, kInAdditionalSection);
+    dnsMsg->Validate(host2, kInAdditionalSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Resend the same query but request a unicast response, validate the response");
+
+    sDnsMessages.Clear();
+    SendQuery("_srv._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassInternet | kClassQueryUnicastFlag);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    dnsMsg->ValidateHeader(kUnicastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 9);
+
+    dnsMsg->Validate(service1, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service1, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(host1, kInAdditionalSection);
+    dnsMsg->Validate(host2, kInAdditionalSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Resend the same multicast query and validate that response is not emitted (rate limit)");
+
+    sDnsMessages.Clear();
+    SendQuery("_srv._udp.local.", ResourceRecord::kTypePtr);
+
+    AdvanceTime(1000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Wait for > 1 second and resend the query and validate that now a response is emitted");
+
+    SendQuery("_srv._udp.local.", ResourceRecord::kTypePtr);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 9);
+
+    dnsMsg->Validate(service1, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service1, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(host1, kInAdditionalSection);
+    dnsMsg->Validate(host2, kInAdditionalSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Browse for sub-type `_s._sub._srv._udp` and validate two answers");
+
+    sDnsMessages.Clear();
+    SendQuery("_s._sub._srv._udp.local.", ResourceRecord::kTypePtr);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 0);
+
+    dnsMsg->ValidateSubType("_s", service1);
+    dnsMsg->ValidateSubType("_s", service3);
+
+    // Send same query again and make sure it is ignored (rate limit).
+
+    sDnsMessages.Clear();
+    SendQuery("_s._sub._srv._udp.local.", ResourceRecord::kTypePtr);
+
+    AdvanceTime(1000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate that query with `ANY class` instead of `IN class` is responded");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery("_r._sub._srv._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassAny);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->ValidateSubType("_r", service1);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate that query with other `class` is ignored");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery("_r._sub._srv._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassNone);
+
+    AdvanceTime(2000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate that query for non-registered name is ignored");
+
+    sDnsMessages.Clear();
+    SendQuery("_u._sub._srv._udp.local.", ResourceRecord::kTypeAny);
+    SendQuery("host3.local.", ResourceRecord::kTypeAny);
+
+    AdvanceTime(2000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Query for SRV for `srv1._srv._udp` and validate answer and additional data");
+
+    sDnsMessages.Clear();
+
+    SendQuery("srv1._srv._udp.local.", ResourceRecord::kTypeSrv);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 4);
+
+    dnsMsg->Validate(service1, kInAnswerSection, kCheckSrv);
+    dnsMsg->Validate(host1, kInAdditionalSection);
+
+    //--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
+    // Query with multiple questions
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query with two questions (SRV for service1 and AAAA for host1). Validate response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQueryForTwo("srv1._srv._udp.local.", ResourceRecord::kTypeSrv, "host1.local.", ResourceRecord::kTypeAaaa);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    // Since AAAA record are already present in Answer they should not be appended
+    // in Additional anymore (for the SRV query).
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 4, /* Auth */ 0, /* Addnl */ 2);
+
+    dnsMsg->Validate(service1, kInAnswerSection, kCheckSrv);
+    dnsMsg->Validate(host1, kInAnswerSection);
+
+    //--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
+    // Known-answer suppression
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query for `_srv._udp` and include `srv1` as known-answer and validate response");
+
+    knownAnswers[0].mPtrAnswer = "srv1._srv._udp.local.";
+    knownAnswers[0].mTtl       = 1500;
+
+    AdvanceTime(1000);
+
+    sDnsMessages.Clear();
+    SendPtrQueryWithKnownAnswers("_srv._udp.local.", knownAnswers, 1);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    // Response should include `service3` only
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 4);
+    dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(host2, kInAdditionalSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query again with both services as known-answer, validate no response is emitted");
+
+    knownAnswers[1].mPtrAnswer = "srv3._srv._udp.local.";
+    knownAnswers[1].mTtl       = 1500;
+
+    AdvanceTime(1000);
+
+    sDnsMessages.Clear();
+    SendPtrQueryWithKnownAnswers("_srv._udp.local.", knownAnswers, 2);
+
+    AdvanceTime(2000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query for `_srv._udp` and include `srv1` as known-answer and validate response");
+
+    knownAnswers[0].mPtrAnswer = "srv1._srv._udp.local.";
+    knownAnswers[0].mTtl       = 1500;
+
+    AdvanceTime(1000);
+
+    sDnsMessages.Clear();
+    SendPtrQueryWithKnownAnswers("_srv._udp.local.", knownAnswers, 1);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    // Response should include `service3` only
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 4);
+    dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(host2, kInAdditionalSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Change the TTL for known-answer to less than half of record TTL and validate response");
+
+    knownAnswers[1].mTtl = 1500 / 2 - 1;
+
+    AdvanceTime(1000);
+
+    sDnsMessages.Clear();
+    SendPtrQueryWithKnownAnswers("_srv._udp.local.", knownAnswers, 2);
+
+    AdvanceTime(200);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    // Response should include `service3` only since anwer TTL
+    // is less than half of registered TTL
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 4);
+    dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt);
+    dnsMsg->Validate(host2, kInAdditionalSection);
+
+    //--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
+    // Query during Goodbye announcements
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Unregister `service1` and wait for its two announcements and validate them");
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->UnregisterService(service1));
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces - 1; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+
+        dnsMsg = sDnsMessages.GetHead();
+        VerifyOrQuit(dnsMsg != nullptr);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 5, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->Validate(service1, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr, kGoodBye);
+
+        for (uint16_t index = 0; index < service1.mSubTypeLabelsLength; index++)
+        {
+            dnsMsg->ValidateSubType(service1.mSubTypeLabels[index], service1, kGoodBye);
+        }
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for removed `service1` before its final announcement, validate no response");
+
+    sDnsMessages.Clear();
+
+    AdvanceTime(1100);
+    SendQuery("srv1._srv._udp.local.", ResourceRecord::kTypeSrv);
+
+    AdvanceTime(200);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    // Wait for final announcement and validate it
+
+    AdvanceTime(2000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 5, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->Validate(service1, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr, kGoodBye);
+
+    for (uint16_t index = 0; index < service1.mSubTypeLabelsLength; index++)
+    {
+        dnsMsg->ValidateSubType(service1.mSubTypeLabels[index], service1, kGoodBye);
+    }
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+
+void TestMultiPacket(void)
+{
+    static const char *const kSubTypes[] = {"_s1", "_r2", "vxy"};
+
+    Core             *mdns = InitTest();
+    Core::Service     service;
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     fullServiceName;
+    DnsNameString     fullServiceType;
+    KnownAnswer       knownAnswers[2];
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestMultiPacket");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    service.mHostName            = "myhost";
+    service.mServiceInstance     = "mysrv";
+    service.mServiceType         = "_tst._udp";
+    service.mSubTypeLabels       = kSubTypes;
+    service.mSubTypeLabelsLength = 3;
+    service.mTxtData             = kTxtData1;
+    service.mTxtDataLength       = sizeof(kTxtData1);
+    service.mPort                = 2222;
+    service.mPriority            = 3;
+    service.mWeight              = 4;
+    service.mTtl                 = 2000;
+
+    fullServiceName.Append("%s.%s.local.", service.mServiceInstance, service.mServiceType);
+    fullServiceType.Append("%s.local.", service.mServiceType);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a `ServiceEntry` with sub-types, check probes and announcements");
+
+    sDnsMessages.Clear();
+
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterService(service, 0, HandleSuccessCallback));
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ (probeCount == 0));
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 7, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+
+        for (uint16_t index = 0; index < service.mSubTypeLabelsLength; index++)
+        {
+            dnsMsg->ValidateSubType(service.mSubTypeLabels[index], service);
+        }
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a query for PTR record for service type and validate the response");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceType.AsCString(), ResourceRecord::kTypePtr);
+
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 2);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service, kInAdditionalSection, kCheckSrv | kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query again but mark it as truncated");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceType.AsCString(), ResourceRecord::kTypePtr, ResourceRecord::kClassInternet,
+              /* aTruncated */ true);
+
+    Log("Since message is marked as `truncated`, mDNS should wait at least 400 msec");
+
+    AdvanceTime(400);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(2000);
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 2);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service, kInAdditionalSection, kCheckSrv | kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query again as truncated followed-up by a non-matching answer");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceType.AsCString(), ResourceRecord::kTypePtr, ResourceRecord::kClassInternet,
+              /* aTruncated */ true);
+    AdvanceTime(10);
+
+    knownAnswers[0].mPtrAnswer = "other._tst._udp.local.";
+    knownAnswers[0].mTtl       = 1500;
+
+    SendEmtryPtrQueryWithKnownAnswers(fullServiceType.AsCString(), knownAnswers, 1);
+
+    AdvanceTime(1000);
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 2);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckPtr);
+    dnsMsg->Validate(service, kInAdditionalSection, kCheckSrv | kCheckTxt);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a PTR query again as truncated now followed-up by matching known-answer");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery(fullServiceType.AsCString(), ResourceRecord::kTypePtr, ResourceRecord::kClassInternet,
+              /* aTruncated */ true);
+    AdvanceTime(10);
+
+    knownAnswers[1].mPtrAnswer = "mysrv._tst._udp.local.";
+    knownAnswers[1].mTtl       = 1500;
+
+    SendEmtryPtrQueryWithKnownAnswers(fullServiceType.AsCString(), knownAnswers, 2);
+
+    Log("We expect no response since the followed-up message contains a matching known-answer");
+    AdvanceTime(5000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a truncated query for PTR record for `services._dns-sd`");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery("_services._dns-sd._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassInternet,
+              /* aTruncated */ true);
+
+    Log("Response should be sent after longer wait time");
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckServicesPtr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a truncated query for PTR record for `services._dns-sd` folloed by known-aswer");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery("_services._dns-sd._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassInternet,
+              /* aTruncated */ true);
+
+    AdvanceTime(20);
+    knownAnswers[0].mPtrAnswer = "_other._udp.local.";
+    knownAnswers[0].mTtl       = 4500;
+
+    SendEmtryPtrQueryWithKnownAnswers("_services._dns-sd._udp.local.", knownAnswers, 1);
+
+    Log("Response should be sent again due to answer not matching");
+    AdvanceTime(1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+    VerifyOrQuit(dnsMsg != nullptr);
+    dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->Validate(service, kInAnswerSection, kCheckServicesPtr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send the same truncated query again but follow-up with a matching known-answer message");
+
+    AdvanceTime(2000);
+
+    sDnsMessages.Clear();
+    SendQuery("_services._dns-sd._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassInternet,
+              /* aTruncated */ true);
+
+    AdvanceTime(20);
+    knownAnswers[1].mPtrAnswer = "_tst._udp.local.";
+    knownAnswers[1].mTtl       = 4500;
+
+    SendEmtryPtrQueryWithKnownAnswers("_services._dns-sd._udp.local.", knownAnswers, 2);
+
+    Log("We expect no response since the followed-up message contains a matching known-answer");
+    AdvanceTime(5000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestQuestionUnicastDisallowed(void)
+{
+    Core             *mdns = InitTest();
+    Core::Host        host;
+    Ip6::Address      hostAddresses[1];
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     hostFullName;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestQuestionUnicastDisallowed");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(hostAddresses[0].FromString("fd00::1234"));
+
+    host.mHostName        = "myhost";
+    host.mAddresses       = hostAddresses;
+    host.mAddressesLength = 1;
+    host.mTtl             = 1500;
+
+    mdns->SetQuestionUnicastAllowed(false);
+    VerifyOrQuit(!mdns->IsQuestionUnicastAllowed());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a `HostEntry`, check probes and announcements");
+
+    sDnsMessages.Clear();
+
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterHost(host, 0, HandleSuccessCallback));
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 1, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ false);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(host, kInAnswerSection);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    sDnsMessages.Clear();
+    AdvanceTime(15000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestTxMessageSizeLimit(void)
+{
+    Core             *mdns = InitTest();
+    Core::Host        host;
+    Core::Service     service;
+    Core::Key         hostKey;
+    Core::Key         serviceKey;
+    Ip6::Address      hostAddresses[3];
+    uint8_t           keyData[300];
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     hostFullName;
+    DnsNameString     serviceFullName;
+
+    memset(keyData, 1, sizeof(keyData));
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestTxMessageSizeLimit");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(hostAddresses[0].FromString("fd00::1:aaaa"));
+    SuccessOrQuit(hostAddresses[1].FromString("fd00::1:bbbb"));
+    SuccessOrQuit(hostAddresses[2].FromString("fd00::1:cccc"));
+    host.mHostName        = "myhost";
+    host.mAddresses       = hostAddresses;
+    host.mAddressesLength = 3;
+    host.mTtl             = 1500;
+    hostFullName.Append("%s.local.", host.mHostName);
+
+    service.mHostName            = host.mHostName;
+    service.mServiceInstance     = "mysrv";
+    service.mServiceType         = "_srv._udp";
+    service.mSubTypeLabels       = nullptr;
+    service.mSubTypeLabelsLength = 0;
+    service.mTxtData             = kTxtData1;
+    service.mTxtDataLength       = sizeof(kTxtData1);
+    service.mPort                = 1111;
+    service.mPriority            = 0;
+    service.mWeight              = 0;
+    service.mTtl                 = 1500;
+    serviceFullName.Append("%s.%s.local.", service.mServiceInstance, service.mServiceType);
+
+    hostKey.mName          = host.mHostName;
+    hostKey.mServiceType   = nullptr;
+    hostKey.mKeyData       = keyData;
+    hostKey.mKeyDataLength = 300;
+    hostKey.mTtl           = 8000;
+
+    serviceKey.mName          = service.mServiceInstance;
+    serviceKey.mServiceType   = service.mServiceType;
+    serviceKey.mKeyData       = keyData;
+    serviceKey.mKeyDataLength = 300;
+    serviceKey.mTtl           = 8000;
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Set `MaxMessageSize` to 340 and use large key record data to trigger size limit behavior");
+
+    mdns->SetMaxMessageSize(340);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register host and service and keys for each");
+
+    sDnsMessages.Clear();
+
+    for (RegCallback &regCallbck : sRegCallbacks)
+    {
+        regCallbck.Reset();
+    }
+
+    SuccessOrQuit(mdns->RegisterHost(host, 0, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterKey(hostKey, 2, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterKey(serviceKey, 3, HandleSuccessCallback));
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate probes for all entries");
+    Log("Probes for host and service should be broken into separate message due to size limit");
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+
+        for (uint16_t index = 0; index < 4; index++)
+        {
+            VerifyOrQuit(!sRegCallbacks[index].mWasCalled);
+        }
+
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 4, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(hostKey, /* aUnicastRequest */ (probeCount == 0));
+
+        dnsMsg = dnsMsg->GetNext();
+        VerifyOrQuit(dnsMsg != nullptr);
+
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 3, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ (probeCount == 0));
+        dnsMsg->ValidateAsProbeFor(serviceKey, /* aUnicastRequest */ (probeCount == 0));
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate announcements for all entries");
+    Log("Announces should also be broken into separate message due to size limit");
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+
+        for (uint16_t index = 0; index < 4; index++)
+        {
+            VerifyOrQuit(sRegCallbacks[index].mWasCalled);
+        }
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 4, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(host, kInAnswerSection);
+        dnsMsg->Validate(hostKey, kInAnswerSection);
+
+        dnsMsg = dnsMsg->GetNext();
+        VerifyOrQuit(dnsMsg != nullptr);
+
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 4, /* Auth */ 0, /* Addnl */ 4);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr);
+        dnsMsg->Validate(serviceKey, kInAnswerSection);
+
+        dnsMsg = dnsMsg->GetNext();
+        VerifyOrQuit(dnsMsg != nullptr);
+
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckServicesPtr);
+
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestHostConflict(void)
+{
+    Core             *mdns = InitTest();
+    Core::Host        host;
+    Ip6::Address      hostAddresses[2];
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     hostFullName;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestHostConflict");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(hostAddresses[0].FromString("fd00::1"));
+    SuccessOrQuit(hostAddresses[1].FromString("fd00::2"));
+
+    host.mHostName        = "myhost";
+    host.mAddresses       = hostAddresses;
+    host.mAddressesLength = 2;
+    host.mTtl             = 1500;
+
+    hostFullName.Append("%s.local.", host.mHostName);
+
+    // Run the test twice, first run send response with record in Answer section,
+    // section run in Additional Data section.
+
+    sConflictCallback.Reset();
+    mdns->SetConflictCallback(HandleConflict);
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register a `HostEntry`, wait for first probe");
+
+        sDnsMessages.Clear();
+
+        sRegCallbacks[0].Reset();
+        SuccessOrQuit(mdns->RegisterHost(host, 0, HandleCallback));
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ true);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Send a response claiming the name with record in %s section", (iter == 0) ? "answer" : "additional");
+
+        SendResponseWithEmptyKey(hostFullName.AsCString(), (iter == 0) ? kInAnswerSection : kInAdditionalSection);
+        AdvanceTime(1);
+
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+        VerifyOrQuit(sRegCallbacks[0].mError == kErrorDuplicated);
+
+        VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+        sDnsMessages.Clear();
+
+        SuccessOrQuit(mdns->UnregisterHost(host));
+
+        AdvanceTime(15000);
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a `HostEntry` and respond to probe to trigger conflict");
+
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterHost(host, 0, HandleCallback));
+
+    VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+
+    SendResponseWithEmptyKey(hostFullName.AsCString(), kInAnswerSection);
+    AdvanceTime(1);
+
+    VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+    VerifyOrQuit(sRegCallbacks[0].mError == kErrorDuplicated);
+    VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register the conflicted `HostEntry` again, and make sure no probes are sent");
+
+    sRegCallbacks[1].Reset();
+    sConflictCallback.Reset();
+    sDnsMessages.Clear();
+
+    SuccessOrQuit(mdns->RegisterHost(host, 1, HandleCallback));
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+    VerifyOrQuit(sRegCallbacks[1].mError == kErrorDuplicated);
+    VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Unregister the conflicted host and register it again immediately, make sure we see probes");
+
+    SuccessOrQuit(mdns->UnregisterHost(host));
+
+    sConflictCallback.Reset();
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterHost(host, 0, HandleSuccessCallback));
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(host, /* aUnicastRequest */ (probeCount == 0));
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(host, kInAnswerSection);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response for host name and validate that conflict is detected and callback is called");
+
+    SendResponseWithEmptyKey(hostFullName.AsCString(), kInAnswerSection);
+    AdvanceTime(1);
+
+    VerifyOrQuit(sConflictCallback.mWasCalled);
+    VerifyOrQuit(StringMatch(sConflictCallback.mName.AsCString(), host.mHostName, kStringCaseInsensitiveMatch));
+    VerifyOrQuit(!sConflictCallback.mHasServiceType);
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestServiceConflict(void)
+{
+    Core             *mdns = InitTest();
+    Core::Service     service;
+    const DnsMessage *dnsMsg;
+    uint16_t          heapAllocations;
+    DnsNameString     fullServiceName;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestServiceConflict");
+
+    service.mHostName            = "myhost";
+    service.mServiceInstance     = "myservice";
+    service.mServiceType         = "_srv._udp";
+    service.mSubTypeLabels       = nullptr;
+    service.mSubTypeLabelsLength = 0;
+    service.mTxtData             = kTxtData1;
+    service.mTxtDataLength       = sizeof(kTxtData1);
+    service.mPort                = 1234;
+    service.mPriority            = 1;
+    service.mWeight              = 2;
+    service.mTtl                 = 1000;
+
+    fullServiceName.Append("%s.%s.local.", service.mServiceInstance, service.mServiceType);
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    // Run the test twice, first run send response with record in Answer section,
+    // section run in Additional Data section.
+
+    sConflictCallback.Reset();
+    mdns->SetConflictCallback(HandleConflict);
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Register a `ServiceEntry`, wait for first probe");
+
+        sDnsMessages.Clear();
+
+        sRegCallbacks[0].Reset();
+        SuccessOrQuit(mdns->RegisterService(service, 0, HandleCallback));
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ true);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+        Log("Send a response claiming the name with record in %s section", (iter == 0) ? "answer" : "additional");
+
+        SendResponseWithEmptyKey(fullServiceName.AsCString(), (iter == 0) ? kInAnswerSection : kInAdditionalSection);
+        AdvanceTime(1);
+
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+        VerifyOrQuit(sRegCallbacks[0].mError == kErrorDuplicated);
+
+        VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+        sDnsMessages.Clear();
+
+        SuccessOrQuit(mdns->UnregisterService(service));
+
+        AdvanceTime(15000);
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register a `ServiceEntry` and respond to probe to trigger conflict");
+
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterService(service, 0, HandleCallback));
+
+    VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+
+    SendResponseWithEmptyKey(fullServiceName.AsCString(), kInAnswerSection);
+    AdvanceTime(1);
+
+    VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+    VerifyOrQuit(sRegCallbacks[0].mError == kErrorDuplicated);
+    VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register the conflicted `ServiceEntry` again, and make sure no probes are sent");
+
+    sRegCallbacks[1].Reset();
+    sConflictCallback.Reset();
+    sDnsMessages.Clear();
+
+    SuccessOrQuit(mdns->RegisterService(service, 1, HandleCallback));
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sRegCallbacks[1].mWasCalled);
+    VerifyOrQuit(sRegCallbacks[1].mError == kErrorDuplicated);
+    VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Unregister the conflicted host and register it again immediately, make sure we see probes");
+
+    SuccessOrQuit(mdns->UnregisterService(service));
+
+    sConflictCallback.Reset();
+    sRegCallbacks[0].Reset();
+    SuccessOrQuit(mdns->RegisterService(service, 0, HandleSuccessCallback));
+
+    for (uint8_t probeCount = 0; probeCount < 3; probeCount++)
+    {
+        sDnsMessages.Clear();
+
+        VerifyOrQuit(!sRegCallbacks[0].mWasCalled);
+        AdvanceTime(250);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 2, /* Addnl */ 0);
+        dnsMsg->ValidateAsProbeFor(service, /* aUnicastRequest */ (probeCount == 0));
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    for (uint8_t anncCount = 0; anncCount < kNumAnnounces; anncCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((anncCount == 0) ? 250 : (1U << (anncCount - 1)) * 1000);
+        VerifyOrQuit(sRegCallbacks[0].mWasCalled);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 4, /* Auth */ 0, /* Addnl */ 1);
+        dnsMsg->Validate(service, kInAnswerSection, kCheckSrv | kCheckTxt | kCheckPtr | kCheckServicesPtr);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    VerifyOrQuit(!sConflictCallback.mWasCalled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response for service name and validate that conflict is detected and callback is called");
+
+    SendResponseWithEmptyKey(fullServiceName.AsCString(), kInAnswerSection);
+    AdvanceTime(1);
+
+    VerifyOrQuit(sConflictCallback.mWasCalled);
+    VerifyOrQuit(
+        StringMatch(sConflictCallback.mName.AsCString(), service.mServiceInstance, kStringCaseInsensitiveMatch));
+    VerifyOrQuit(sConflictCallback.mHasServiceType);
+    VerifyOrQuit(
+        StringMatch(sConflictCallback.mServiceType.AsCString(), service.mServiceType, kStringCaseInsensitiveMatch));
+
+    sDnsMessages.Clear();
+    AdvanceTime(20000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+//=====================================================================================================================
+// Browser/Resolver tests
+
+struct BrowseCallback : public Allocatable<BrowseCallback>, public LinkedListEntry<BrowseCallback>
+{
+    BrowseCallback *mNext;
+    DnsName         mServiceType;
+    DnsName         mSubTypeLabel;
+    DnsName         mServiceInstance;
+    uint32_t        mTtl;
+    bool            mIsSubType;
+};
+
+struct SrvCallback : public Allocatable<SrvCallback>, public LinkedListEntry<SrvCallback>
+{
+    SrvCallback *mNext;
+    DnsName      mServiceInstance;
+    DnsName      mServiceType;
+    DnsName      mHostName;
+    uint16_t     mPort;
+    uint16_t     mPriority;
+    uint16_t     mWeight;
+    uint32_t     mTtl;
+};
+
+struct TxtCallback : public Allocatable<TxtCallback>, public LinkedListEntry<TxtCallback>
+{
+    static constexpr uint16_t kMaxTxtDataLength = 100;
+
+    template <uint16_t kSize> bool Matches(const uint8_t (&aData)[kSize]) const
+    {
+        return (mTxtDataLength == kSize) && (memcmp(mTxtData, aData, kSize) == 0);
+    }
+
+    TxtCallback *mNext;
+    DnsName      mServiceInstance;
+    DnsName      mServiceType;
+    uint8_t      mTxtData[kMaxTxtDataLength];
+    uint16_t     mTxtDataLength;
+    uint32_t     mTtl;
+};
+
+struct AddrCallback : public Allocatable<AddrCallback>, public LinkedListEntry<AddrCallback>
+{
+    static constexpr uint16_t kMaxNumAddrs = 16;
+
+    bool Contains(const AddrAndTtl &aAddrAndTtl) const
+    {
+        bool contains = false;
+
+        for (uint16_t index = 0; index < mNumAddrs; index++)
+        {
+            if (mAddrAndTtls[index] == aAddrAndTtl)
+            {
+                contains = true;
+                break;
+            }
+        }
+
+        return contains;
+    }
+
+    bool Matches(const AddrAndTtl *aAddrAndTtls, uint16_t aNumAddrs) const
+    {
+        bool matches = true;
+
+        VerifyOrExit(aNumAddrs == mNumAddrs, matches = false);
+
+        for (uint16_t index = 0; index < mNumAddrs; index++)
+        {
+            if (!Contains(aAddrAndTtls[index]))
+            {
+                ExitNow(matches = false);
+            }
+        }
+
+    exit:
+        return matches;
+    }
+
+    AddrCallback *mNext;
+    DnsName       mHostName;
+    AddrAndTtl    mAddrAndTtls[kMaxNumAddrs];
+    uint16_t      mNumAddrs;
+};
+
+OwningList<BrowseCallback> sBrowseCallbacks;
+OwningList<SrvCallback>    sSrvCallbacks;
+OwningList<TxtCallback>    sTxtCallbacks;
+OwningList<AddrCallback>   sAddrCallbacks;
+
+void HandleBrowseResult(otInstance *aInstance, const otMdnsBrowseResult *aResult)
+{
+    BrowseCallback *entry;
+
+    VerifyOrQuit(aInstance == sInstance);
+    VerifyOrQuit(aResult != nullptr);
+    VerifyOrQuit(aResult->mServiceType != nullptr);
+    VerifyOrQuit(aResult->mServiceInstance != nullptr);
+    VerifyOrQuit(aResult->mInfraIfIndex == kInfraIfIndex);
+
+    Log("Browse callback: %s (subtype:%s) -> %s ttl:%lu", aResult->mServiceType,
+        aResult->mSubTypeLabel == nullptr ? "(null)" : aResult->mSubTypeLabel, aResult->mServiceInstance,
+        ToUlong(aResult->mTtl));
+
+    entry = BrowseCallback::Allocate();
+    VerifyOrQuit(entry != nullptr);
+
+    entry->mServiceType.CopyFrom(aResult->mServiceType);
+    entry->mSubTypeLabel.CopyFrom(aResult->mSubTypeLabel);
+    entry->mServiceInstance.CopyFrom(aResult->mServiceInstance);
+    entry->mTtl       = aResult->mTtl;
+    entry->mIsSubType = (aResult->mSubTypeLabel != nullptr);
+
+    sBrowseCallbacks.PushAfterTail(*entry);
+}
+
+void HandleBrowseResultAlternate(otInstance *aInstance, const otMdnsBrowseResult *aResult)
+{
+    Log("Alternate browse callback is called");
+    HandleBrowseResult(aInstance, aResult);
+}
+
+void HandleSrvResult(otInstance *aInstance, const otMdnsSrvResult *aResult)
+{
+    SrvCallback *entry;
+
+    VerifyOrQuit(aInstance == sInstance);
+    VerifyOrQuit(aResult != nullptr);
+    VerifyOrQuit(aResult->mServiceInstance != nullptr);
+    VerifyOrQuit(aResult->mServiceType != nullptr);
+    VerifyOrQuit(aResult->mInfraIfIndex == kInfraIfIndex);
+
+    if (aResult->mTtl != 0)
+    {
+        VerifyOrQuit(aResult->mHostName != nullptr);
+
+        Log("SRV callback: %s %s, host:%s port:%u, prio:%u, weight:%u, ttl:%lu", aResult->mServiceInstance,
+            aResult->mServiceType, aResult->mHostName, aResult->mPort, aResult->mPriority, aResult->mWeight,
+            ToUlong(aResult->mTtl));
+    }
+    else
+    {
+        Log("SRV callback: %s %s, ttl:%lu", aResult->mServiceInstance, aResult->mServiceType, ToUlong(aResult->mTtl));
+    }
+
+    entry = SrvCallback::Allocate();
+    VerifyOrQuit(entry != nullptr);
+
+    entry->mServiceInstance.CopyFrom(aResult->mServiceInstance);
+    entry->mServiceType.CopyFrom(aResult->mServiceType);
+    entry->mHostName.CopyFrom(aResult->mHostName);
+    entry->mPort     = aResult->mPort;
+    entry->mPriority = aResult->mPriority;
+    entry->mWeight   = aResult->mWeight;
+    entry->mTtl      = aResult->mTtl;
+
+    sSrvCallbacks.PushAfterTail(*entry);
+}
+
+void HandleSrvResultAlternate(otInstance *aInstance, const otMdnsSrvResult *aResult)
+{
+    Log("Alternate SRV callback is called");
+    HandleSrvResult(aInstance, aResult);
+}
+
+void HandleTxtResult(otInstance *aInstance, const otMdnsTxtResult *aResult)
+{
+    TxtCallback *entry;
+
+    VerifyOrQuit(aInstance == sInstance);
+    VerifyOrQuit(aResult != nullptr);
+    VerifyOrQuit(aResult->mServiceInstance != nullptr);
+    VerifyOrQuit(aResult->mServiceType != nullptr);
+    VerifyOrQuit(aResult->mInfraIfIndex == kInfraIfIndex);
+
+    VerifyOrQuit(aResult->mTxtDataLength <= TxtCallback::kMaxTxtDataLength);
+
+    if (aResult->mTtl != 0)
+    {
+        VerifyOrQuit(aResult->mTxtData != nullptr);
+
+        Log("TXT callback: %s %s, len:%u, ttl:%lu", aResult->mServiceInstance, aResult->mServiceType,
+            aResult->mTxtDataLength, ToUlong(aResult->mTtl));
+    }
+    else
+    {
+        Log("TXT callback: %s %s, ttl:%lu", aResult->mServiceInstance, aResult->mServiceType, ToUlong(aResult->mTtl));
+    }
+
+    entry = TxtCallback::Allocate();
+    VerifyOrQuit(entry != nullptr);
+
+    entry->mServiceInstance.CopyFrom(aResult->mServiceInstance);
+    entry->mServiceType.CopyFrom(aResult->mServiceType);
+    entry->mTxtDataLength = aResult->mTxtDataLength;
+    memcpy(entry->mTxtData, aResult->mTxtData, aResult->mTxtDataLength);
+    entry->mTtl = aResult->mTtl;
+
+    sTxtCallbacks.PushAfterTail(*entry);
+}
+
+void HandleTxtResultAlternate(otInstance *aInstance, const otMdnsTxtResult *aResult)
+{
+    Log("Alternate TXT callback is called");
+    HandleTxtResult(aInstance, aResult);
+}
+
+void HandleAddrResult(otInstance *aInstance, const otMdnsAddressResult *aResult)
+{
+    AddrCallback *entry;
+
+    VerifyOrQuit(aInstance == sInstance);
+    VerifyOrQuit(aResult != nullptr);
+    VerifyOrQuit(aResult->mHostName != nullptr);
+    VerifyOrQuit(aResult->mInfraIfIndex == kInfraIfIndex);
+
+    VerifyOrQuit(aResult->mAddressesLength <= AddrCallback::kMaxNumAddrs);
+
+    entry = AddrCallback::Allocate();
+    VerifyOrQuit(entry != nullptr);
+
+    entry->mHostName.CopyFrom(aResult->mHostName);
+    entry->mNumAddrs = aResult->mAddressesLength;
+
+    Log("Addr callback: %s, num:%u", aResult->mHostName, aResult->mAddressesLength);
+
+    for (uint16_t index = 0; index < aResult->mAddressesLength; index++)
+    {
+        entry->mAddrAndTtls[index].mAddress = AsCoreType(&aResult->mAddresses[index].mAddress);
+        entry->mAddrAndTtls[index].mTtl     = aResult->mAddresses[index].mTtl;
+
+        Log(" - %s, ttl:%lu", entry->mAddrAndTtls[index].mAddress.ToString().AsCString(),
+            ToUlong(entry->mAddrAndTtls[index].mTtl));
+    }
+
+    sAddrCallbacks.PushAfterTail(*entry);
+}
+
+void HandleAddrResultAlternate(otInstance *aInstance, const otMdnsAddressResult *aResult)
+{
+    Log("Alternate addr callback is called");
+    HandleAddrResult(aInstance, aResult);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestBrowser(void)
+{
+    Core                 *mdns = InitTest();
+    Core::Browser         browser;
+    Core::Browser         browser2;
+    const DnsMessage     *dnsMsg;
+    const BrowseCallback *browseCallback;
+    uint16_t              heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestBrowser");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start a browser. Validate initial queries.");
+
+    ClearAllBytes(browser);
+
+    browser.mServiceType  = "_srv._udp";
+    browser.mSubTypeLabel = nullptr;
+    browser.mInfraIfIndex = kInfraIfIndex;
+    browser.mCallback     = HandleBrowseResult;
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->StartBrowser(browser));
+
+    for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((queryCount == 0) ? 125 : (1U << (queryCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(browser);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    sDnsMessages.Clear();
+
+    AdvanceTime(20000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response. Validate callback result.");
+
+    sBrowseCallbacks.Clear();
+
+    SendPtrResponse("_srv._udp.local.", "mysrv._srv._udp.local.", 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sBrowseCallbacks.IsEmpty());
+    browseCallback = sBrowseCallbacks.GetHead();
+    VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(!browseCallback->mIsSubType);
+    VerifyOrQuit(browseCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(browseCallback->mTtl == 120);
+    VerifyOrQuit(browseCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send another response. Validate callback result.");
+
+    AdvanceTime(10000);
+
+    sBrowseCallbacks.Clear();
+
+    SendPtrResponse("_srv._udp.local.", "awesome._srv._udp.local.", 500, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sBrowseCallbacks.IsEmpty());
+    browseCallback = sBrowseCallbacks.GetHead();
+    VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(!browseCallback->mIsSubType);
+    VerifyOrQuit(browseCallback->mServiceInstance.Matches("awesome"));
+    VerifyOrQuit(browseCallback->mTtl == 500);
+    VerifyOrQuit(browseCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start another browser for the same service and different callback. Validate results.");
+
+    AdvanceTime(5000);
+
+    browser2.mServiceType  = "_srv._udp";
+    browser2.mSubTypeLabel = nullptr;
+    browser2.mInfraIfIndex = kInfraIfIndex;
+    browser2.mCallback     = HandleBrowseResultAlternate;
+
+    sBrowseCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartBrowser(browser2));
+
+    browseCallback = sBrowseCallbacks.GetHead();
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        VerifyOrQuit(browseCallback != nullptr);
+
+        VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+        VerifyOrQuit(!browseCallback->mIsSubType);
+
+        if (browseCallback->mServiceInstance.Matches("awesome"))
+        {
+            VerifyOrQuit(browseCallback->mTtl == 500);
+        }
+        else if (browseCallback->mServiceInstance.Matches("mysrv"))
+        {
+            VerifyOrQuit(browseCallback->mTtl == 120);
+        }
+        else
+        {
+            VerifyOrQuit(false);
+        }
+
+        browseCallback = browseCallback->GetNext();
+    }
+
+    VerifyOrQuit(browseCallback == nullptr);
+
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start same browser again and check the returned error.");
+
+    sBrowseCallbacks.Clear();
+
+    VerifyOrQuit(mdns->StartBrowser(browser2) == kErrorAlready);
+
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a goodbye response. Validate result callback for both browsers.");
+
+    SendPtrResponse("_srv._udp.local.", "awesome._srv._udp.local.", 0, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    browseCallback = sBrowseCallbacks.GetHead();
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        VerifyOrQuit(browseCallback != nullptr);
+
+        VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+        VerifyOrQuit(!browseCallback->mIsSubType);
+        VerifyOrQuit(browseCallback->mServiceInstance.Matches("awesome"));
+        VerifyOrQuit(browseCallback->mTtl == 0);
+
+        browseCallback = browseCallback->GetNext();
+    }
+
+    VerifyOrQuit(browseCallback == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response with no changes, validate that no callback is invoked.");
+
+    sBrowseCallbacks.Clear();
+
+    SendPtrResponse("_srv._udp.local.", "mysrv._srv._udp.local.", 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the second browser.");
+
+    sBrowseCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopBrowser(browser2));
+
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check query is sent at 80 percentage of TTL and then respond to it.");
+
+    // First query should be sent at 80-82% of TTL of 120 second (96.0-98.4 sec).
+    // We wait for 100 second. Note that 5 seconds already passed in the
+    // previous step.
+
+    AdvanceTime(91 * 1000 - 1);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(4 * 1000 + 1);
+
+    VerifyOrQuit(!sDnsMessages.IsEmpty());
+    dnsMsg = sDnsMessages.GetHead();
+    dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->ValidateAsQueryFor(browser);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    sDnsMessages.Clear();
+    VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+
+    AdvanceTime(10);
+
+    SendPtrResponse("_srv._udp.local.", "mysrv._srv._udp.local.", 120, kInAnswerSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check queries are sent at 80, 85, 90, 95 percentages of TTL.");
+
+    for (uint8_t queryCount = 0; queryCount < kNumRefreshQueries; queryCount++)
+    {
+        if (queryCount == 0)
+        {
+            // First query is expected in 80-82% of TTL, so
+            // 80% of 120 = 96.0, 82% of 120 = 98.4
+
+            AdvanceTime(96 * 1000 - 1);
+        }
+        else
+        {
+            // Next query should happen within 3%-5% of TTL
+            // from previous query. We wait 3% of TTL here.
+            AdvanceTime(3600 - 1);
+        }
+
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+
+        // Wait for 2% of TTL of 120 which is 2.4 sec.
+
+        AdvanceTime(2400 + 1);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(browser);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        sDnsMessages.Clear();
+        VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check TTL timeout and callback result.");
+
+    AdvanceTime(6 * 1000);
+
+    VerifyOrQuit(!sBrowseCallbacks.IsEmpty());
+
+    browseCallback = sBrowseCallbacks.GetHead();
+    VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(!browseCallback->mIsSubType);
+    VerifyOrQuit(browseCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(browseCallback->mTtl == 0);
+    VerifyOrQuit(browseCallback->GetNext() == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    sBrowseCallbacks.Clear();
+    sDnsMessages.Clear();
+
+    AdvanceTime(200 * 1000);
+
+    VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a new response and make sure result callback is invoked");
+
+    SendPtrResponse("_srv._udp.local.", "great._srv._udp.local.", 200, kInAdditionalSection);
+
+    AdvanceTime(1);
+
+    browseCallback = sBrowseCallbacks.GetHead();
+
+    VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(!browseCallback->mIsSubType);
+    VerifyOrQuit(browseCallback->mServiceInstance.Matches("great"));
+    VerifyOrQuit(browseCallback->mTtl == 200);
+    VerifyOrQuit(browseCallback->GetNext() == nullptr);
+
+    sBrowseCallbacks.Clear();
+
+    AdvanceTime(150 * 1000);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+    VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the browser. There is no active browser for this service. Ensure no queries are sent");
+
+    sBrowseCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopBrowser(browser));
+
+    AdvanceTime(100 * 1000);
+
+    VerifyOrQuit(sBrowseCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start browser again. Validate that initial queries are sent again");
+
+    SuccessOrQuit(mdns->StartBrowser(browser));
+
+    AdvanceTime(125);
+
+    VerifyOrQuit(!sDnsMessages.IsEmpty());
+    dnsMsg = sDnsMessages.GetHead();
+    dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->ValidateAsQueryFor(browser);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response after the first initial query");
+
+    sDnsMessages.Clear();
+
+    SendPtrResponse("_srv._udp.local.", "mysrv._srv._udp.local.", 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    browseCallback = sBrowseCallbacks.GetHead();
+
+    VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(!browseCallback->mIsSubType);
+    VerifyOrQuit(browseCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(browseCallback->mTtl == 120);
+    VerifyOrQuit(browseCallback->GetNext() == nullptr);
+
+    sBrowseCallbacks.Clear();
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Validate initial esquires are still sent and include known-answer");
+
+    for (uint8_t queryCount = 1; queryCount < kNumInitalQueries; queryCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((1U << (queryCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 1, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(browser);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    sDnsMessages.Clear();
+    AdvanceTime(50 * 1000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+void TestSrvResolver(void)
+{
+    Core              *mdns = InitTest();
+    Core::SrvResolver  resolver;
+    Core::SrvResolver  resolver2;
+    const DnsMessage  *dnsMsg;
+    const SrvCallback *srvCallback;
+    uint16_t           heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestSrvResolver");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start a SRV resolver. Validate initial queries.");
+
+    ClearAllBytes(resolver);
+
+    resolver.mServiceInstance = "mysrv";
+    resolver.mServiceType     = "_srv._udp";
+    resolver.mInfraIfIndex    = kInfraIfIndex;
+    resolver.mCallback        = HandleSrvResult;
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->StartSrvResolver(resolver));
+
+    for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((queryCount == 0) ? 125 : (1U << (queryCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    sDnsMessages.Clear();
+
+    AdvanceTime(20 * 1000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response. Validate callback result.");
+
+    sSrvCallbacks.Clear();
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost.local.", 1234, 0, 1, 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(srvCallback->mPort == 1234);
+    VerifyOrQuit(srvCallback->mPriority == 0);
+    VerifyOrQuit(srvCallback->mWeight == 1);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response changing host name. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sSrvCallbacks.Clear();
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost2.local.", 1234, 0, 1, 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost2"));
+    VerifyOrQuit(srvCallback->mPort == 1234);
+    VerifyOrQuit(srvCallback->mPriority == 0);
+    VerifyOrQuit(srvCallback->mWeight == 1);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response changing port. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sSrvCallbacks.Clear();
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost2.local.", 4567, 0, 1, 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost2"));
+    VerifyOrQuit(srvCallback->mPort == 4567);
+    VerifyOrQuit(srvCallback->mPriority == 0);
+    VerifyOrQuit(srvCallback->mWeight == 1);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response changing TTL. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sSrvCallbacks.Clear();
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost2.local.", 4567, 0, 1, 0, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches(""));
+    VerifyOrQuit(srvCallback->mPort == 4567);
+    VerifyOrQuit(srvCallback->mPriority == 0);
+    VerifyOrQuit(srvCallback->mWeight == 1);
+    VerifyOrQuit(srvCallback->mTtl == 0);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response changing a bunch of things. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sSrvCallbacks.Clear();
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost.local.", 1234, 2, 3, 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(srvCallback->mPort == 1234);
+    VerifyOrQuit(srvCallback->mPriority == 2);
+    VerifyOrQuit(srvCallback->mWeight == 3);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response with no changes. Validate callback is not invoked.");
+
+    AdvanceTime(1000);
+
+    sSrvCallbacks.Clear();
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost.local.", 1234, 2, 3, 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(sSrvCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start another resolver for the same service and different callback. Validate results.");
+
+    ClearAllBytes(resolver2);
+
+    resolver2.mServiceInstance = "mysrv";
+    resolver2.mServiceType     = "_srv._udp";
+    resolver2.mInfraIfIndex    = kInfraIfIndex;
+    resolver2.mCallback        = HandleSrvResultAlternate;
+
+    sSrvCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartSrvResolver(resolver2));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(srvCallback->mPort == 1234);
+    VerifyOrQuit(srvCallback->mPriority == 2);
+    VerifyOrQuit(srvCallback->mWeight == 3);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start same resolver again and check the returned error.");
+
+    sSrvCallbacks.Clear();
+
+    VerifyOrQuit(mdns->StartSrvResolver(resolver2) == kErrorAlready);
+
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sSrvCallbacks.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check query is sent at 80 percentage of TTL and then respond to it.");
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost.local.", 1234, 2, 3, 120, kInAnswerSection);
+
+    // First query should be sent at 80-82% of TTL of 120 second (96.0-98.4 sec).
+    // We wait for 100 second. Note that 5 seconds already passed in the
+    // previous step.
+
+    AdvanceTime(96 * 1000 - 1);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(4 * 1000 + 1);
+
+    VerifyOrQuit(!sDnsMessages.IsEmpty());
+    dnsMsg = sDnsMessages.GetHead();
+    dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->ValidateAsQueryFor(resolver);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    sDnsMessages.Clear();
+    VerifyOrQuit(sSrvCallbacks.IsEmpty());
+
+    AdvanceTime(10);
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost.local.", 1234, 2, 3, 120, kInAnswerSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check queries are sent at 80, 85, 90, 95 percentages of TTL.");
+
+    for (uint8_t queryCount = 0; queryCount < kNumRefreshQueries; queryCount++)
+    {
+        if (queryCount == 0)
+        {
+            // First query is expected in 80-82% of TTL, so
+            // 80% of 120 = 96.0, 82% of 120 = 98.4
+
+            AdvanceTime(96 * 1000 - 1);
+        }
+        else
+        {
+            // Next query should happen within 3%-5% of TTL
+            // from previous query. We wait 3% of TTL here.
+            AdvanceTime(3600 - 1);
+        }
+
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+
+        // Wait for 2% of TTL of 120 which is 2.4 sec.
+
+        AdvanceTime(2400 + 1);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        sDnsMessages.Clear();
+        VerifyOrQuit(sSrvCallbacks.IsEmpty());
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check TTL timeout and callback result.");
+
+    AdvanceTime(6 * 1000);
+
+    srvCallback = sSrvCallbacks.GetHead();
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        VerifyOrQuit(srvCallback != nullptr);
+        VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+        VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+        VerifyOrQuit(srvCallback->mTtl == 0);
+        srvCallback = srvCallback->GetNext();
+    }
+
+    VerifyOrQuit(srvCallback == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    sSrvCallbacks.Clear();
+    sDnsMessages.Clear();
+
+    AdvanceTime(200 * 1000);
+
+    VerifyOrQuit(sSrvCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the second resolver");
+
+    sSrvCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopSrvResolver(resolver2));
+
+    AdvanceTime(100 * 1000);
+
+    VerifyOrQuit(sSrvCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a new response and make sure result callback is invoked");
+
+    SendSrvResponse("mysrv._srv._udp.local.", "myhost.local.", 1234, 2, 3, 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(srvCallback->mPort == 1234);
+    VerifyOrQuit(srvCallback->mPriority == 2);
+    VerifyOrQuit(srvCallback->mWeight == 3);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the resolver. There is no active resolver. Ensure no queries are sent");
+
+    sSrvCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopSrvResolver(resolver));
+
+    AdvanceTime(20 * 1000);
+
+    VerifyOrQuit(sSrvCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Restart the resolver with more than half of TTL remaining.");
+    Log("Ensure cached entry is reported in the result callback and no queries are sent.");
+
+    SuccessOrQuit(mdns->StartSrvResolver(resolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(srvCallback->mPort == 1234);
+    VerifyOrQuit(srvCallback->mPriority == 2);
+    VerifyOrQuit(srvCallback->mWeight == 3);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(20 * 1000);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop and start the resolver again after less than half TTL remaining.");
+    Log("Ensure cached entry is still reported in the result callback but queries should be sent");
+
+    sSrvCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopSrvResolver(resolver));
+
+    AdvanceTime(25 * 1000);
+
+    SuccessOrQuit(mdns->StartSrvResolver(resolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(srvCallback->mPort == 1234);
+    VerifyOrQuit(srvCallback->mPriority == 2);
+    VerifyOrQuit(srvCallback->mWeight == 3);
+    VerifyOrQuit(srvCallback->mTtl == 120);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    sSrvCallbacks.Clear();
+
+    AdvanceTime(15 * 1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+
+    for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++)
+    {
+        VerifyOrQuit(dnsMsg != nullptr);
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        dnsMsg = dnsMsg->GetNext();
+    }
+
+    VerifyOrQuit(dnsMsg == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+void TestTxtResolver(void)
+{
+    Core              *mdns = InitTest();
+    Core::TxtResolver  resolver;
+    Core::TxtResolver  resolver2;
+    const DnsMessage  *dnsMsg;
+    const TxtCallback *txtCallback;
+    uint16_t           heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestTxtResolver");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start a TXT resolver. Validate initial queries.");
+
+    ClearAllBytes(resolver);
+
+    resolver.mServiceInstance = "mysrv";
+    resolver.mServiceType     = "_srv._udp";
+    resolver.mInfraIfIndex    = kInfraIfIndex;
+    resolver.mCallback        = HandleTxtResult;
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->StartTxtResolver(resolver));
+
+    for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((queryCount == 0) ? 125 : (1U << (queryCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    sDnsMessages.Clear();
+
+    AdvanceTime(20 * 1000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response. Validate callback result.");
+
+    sTxtCallbacks.Clear();
+
+    SendTxtResponse("mysrv._srv._udp.local.", kTxtData1, sizeof(kTxtData1), 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData1));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response changing TXT data. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sTxtCallbacks.Clear();
+
+    SendTxtResponse("mysrv._srv._udp.local.", kTxtData2, sizeof(kTxtData2), 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData2));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response changing TXT data to empty. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sTxtCallbacks.Clear();
+
+    SendTxtResponse("mysrv._srv._udp.local.", kEmptyTxtData, sizeof(kEmptyTxtData), 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kEmptyTxtData));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response changing TTL. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sTxtCallbacks.Clear();
+
+    SendTxtResponse("mysrv._srv._udp.local.", kEmptyTxtData, sizeof(kEmptyTxtData), 500, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kEmptyTxtData));
+    VerifyOrQuit(txtCallback->mTtl == 500);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response with zero TTL. Validate callback result.");
+
+    AdvanceTime(1000);
+
+    sTxtCallbacks.Clear();
+
+    SendTxtResponse("mysrv._srv._udp.local.", kEmptyTxtData, sizeof(kEmptyTxtData), 0, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->mTtl == 0);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response. Validate callback result.");
+
+    sTxtCallbacks.Clear();
+    AdvanceTime(100 * 1000);
+
+    SendTxtResponse("mysrv._srv._udp.local.", kTxtData1, sizeof(kTxtData1), 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData1));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response with no changes. Validate callback is not invoked.");
+
+    AdvanceTime(1000);
+
+    sTxtCallbacks.Clear();
+
+    SendTxtResponse("mysrv._srv._udp.local.", kTxtData1, sizeof(kTxtData1), 120, kInAnswerSection);
+
+    AdvanceTime(100);
+
+    VerifyOrQuit(sTxtCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start another resolver for the same service and different callback. Validate results.");
+
+    resolver2.mServiceInstance = "mysrv";
+    resolver2.mServiceType     = "_srv._udp";
+    resolver2.mInfraIfIndex    = kInfraIfIndex;
+    resolver2.mCallback        = HandleTxtResultAlternate;
+
+    sTxtCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartTxtResolver(resolver2));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData1));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start same resolver again and check the returned error.");
+
+    sTxtCallbacks.Clear();
+
+    VerifyOrQuit(mdns->StartTxtResolver(resolver2) == kErrorAlready);
+
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sTxtCallbacks.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check query is sent at 80 percentage of TTL and then respond to it.");
+
+    SendTxtResponse("mysrv._srv._udp.local.", kTxtData1, sizeof(kTxtData1), 120, kInAnswerSection);
+
+    // First query should be sent at 80-82% of TTL of 120 second (96.0-98.4 sec).
+    // We wait for 100 second. Note that 5 seconds already passed in the
+    // previous step.
+
+    AdvanceTime(96 * 1000 - 1);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(4 * 1000 + 1);
+
+    VerifyOrQuit(!sDnsMessages.IsEmpty());
+    dnsMsg = sDnsMessages.GetHead();
+    dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->ValidateAsQueryFor(resolver);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    sDnsMessages.Clear();
+    VerifyOrQuit(sTxtCallbacks.IsEmpty());
+
+    AdvanceTime(10);
+
+    SendTxtResponse("mysrv._srv._udp.local.", kTxtData1, sizeof(kTxtData1), 120, kInAnswerSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check queries are sent at 80, 85, 90, 95 percentages of TTL.");
+
+    for (uint8_t queryCount = 0; queryCount < kNumRefreshQueries; queryCount++)
+    {
+        if (queryCount == 0)
+        {
+            // First query is expected in 80-82% of TTL, so
+            // 80% of 120 = 96.0, 82% of 120 = 98.4
+
+            AdvanceTime(96 * 1000 - 1);
+        }
+        else
+        {
+            // Next query should happen within 3%-5% of TTL
+            // from previous query. We wait 3% of TTL here.
+            AdvanceTime(3600 - 1);
+        }
+
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+
+        // Wait for 2% of TTL of 120 which is 2.4 sec.
+
+        AdvanceTime(2400 + 1);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        sDnsMessages.Clear();
+        VerifyOrQuit(sTxtCallbacks.IsEmpty());
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check TTL timeout and callback result.");
+
+    AdvanceTime(6 * 1000);
+
+    txtCallback = sTxtCallbacks.GetHead();
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        VerifyOrQuit(txtCallback != nullptr);
+        VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+        VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+        VerifyOrQuit(txtCallback->mTtl == 0);
+        txtCallback = txtCallback->GetNext();
+    }
+
+    VerifyOrQuit(txtCallback == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    sTxtCallbacks.Clear();
+    sDnsMessages.Clear();
+
+    AdvanceTime(200 * 1000);
+
+    VerifyOrQuit(sTxtCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the second resolver");
+
+    sTxtCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopTxtResolver(resolver2));
+
+    AdvanceTime(100 * 1000);
+
+    VerifyOrQuit(sTxtCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a new response and make sure result callback is invoked");
+
+    SendTxtResponse("mysrv._srv._udp.local.", kTxtData1, sizeof(kTxtData1), 120, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData1));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the resolver. There is no active resolver. Ensure no queries are sent");
+
+    sTxtCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopTxtResolver(resolver));
+
+    AdvanceTime(20 * 1000);
+
+    VerifyOrQuit(sTxtCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Restart the resolver with more than half of TTL remaining.");
+    Log("Ensure cached entry is reported in the result callback and no queries are sent.");
+
+    SuccessOrQuit(mdns->StartTxtResolver(resolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData1));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(20 * 1000);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop and start the resolver again after less than half TTL remaining.");
+    Log("Ensure cached entry is still reported in the result callback but queries should be sent");
+
+    sTxtCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopTxtResolver(resolver));
+
+    AdvanceTime(25 * 1000);
+
+    SuccessOrQuit(mdns->StartTxtResolver(resolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("mysrv"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData1));
+    VerifyOrQuit(txtCallback->mTtl == 120);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    sTxtCallbacks.Clear();
+
+    AdvanceTime(15 * 1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+
+    for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++)
+    {
+        VerifyOrQuit(dnsMsg != nullptr);
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        dnsMsg = dnsMsg->GetNext();
+    }
+
+    VerifyOrQuit(dnsMsg == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+void TestIp6AddrResolver(void)
+{
+    Core                 *mdns = InitTest();
+    Core::AddressResolver resolver;
+    Core::AddressResolver resolver2;
+    AddrAndTtl            addrs[5];
+    const DnsMessage     *dnsMsg;
+    const AddrCallback   *addrCallback;
+    uint16_t              heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestIp6AddrResolver");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start an IPv6 address resolver. Validate initial queries.");
+
+    ClearAllBytes(resolver);
+
+    resolver.mHostName     = "myhost";
+    resolver.mInfraIfIndex = kInfraIfIndex;
+    resolver.mCallback     = HandleAddrResult;
+
+    sDnsMessages.Clear();
+    SuccessOrQuit(mdns->StartIp6AddressResolver(resolver));
+
+    for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++)
+    {
+        sDnsMessages.Clear();
+
+        AdvanceTime((queryCount == 0) ? 125 : (1U << (queryCount - 1)) * 1000);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+    }
+
+    sDnsMessages.Clear();
+
+    AdvanceTime(20 * 1000);
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response. Validate callback result.");
+
+    sAddrCallbacks.Clear();
+
+    SuccessOrQuit(addrs[0].mAddress.FromString("fd00::1"));
+    addrs[0].mTtl = 120;
+
+    SendHostAddrResponse("myhost.local.", addrs, 1, /* aCachFlush */ true, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 1));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response adding a new address. Validate callback result.");
+
+    SuccessOrQuit(addrs[1].mAddress.FromString("fd00::2"));
+    addrs[1].mTtl = 120;
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", addrs, 2, /* aCachFlush */ true, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 2));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send an updated response adding and removing addresses. Validate callback result.");
+
+    SuccessOrQuit(addrs[0].mAddress.FromString("fd00::2"));
+    SuccessOrQuit(addrs[1].mAddress.FromString("fd00::aa"));
+    SuccessOrQuit(addrs[2].mAddress.FromString("fe80::bb"));
+    addrs[0].mTtl = 120;
+    addrs[1].mTtl = 120;
+    addrs[2].mTtl = 120;
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", addrs, 3, /* aCachFlush */ true, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 3));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response without cache flush adding an address. Validate callback result.");
+
+    SuccessOrQuit(addrs[3].mAddress.FromString("fd00::3"));
+    addrs[3].mTtl = 500;
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", &addrs[3], 1, /* aCachFlush */ false, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 4));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response without cache flush with existing addresses. Validate that callback is not called.");
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", &addrs[2], 2, /* aCachFlush */ false, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(sAddrCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response without no changes to the list. Validate that callback is not called");
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", addrs, 4, /* aCachFlush */ true, kInAdditionalSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(sAddrCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response without cache flush updating TTL of existing address. Validate callback result.");
+
+    addrs[3].mTtl = 200;
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", &addrs[3], 1, /* aCachFlush */ false, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 4));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response without cache flush removing an address (zero TTL). Validate callback result.");
+
+    addrs[3].mTtl = 0;
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", &addrs[3], 1, /* aCachFlush */ false, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 3));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response with cache flush removing all addresses. Validate callback result.");
+
+    addrs[0].mTtl = 0;
+
+    AdvanceTime(1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", addrs, 1, /* aCachFlush */ true, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 0));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a response with addresses with different TTL. Validate callback result");
+
+    SuccessOrQuit(addrs[0].mAddress.FromString("fd00::00"));
+    SuccessOrQuit(addrs[1].mAddress.FromString("fd00::11"));
+    SuccessOrQuit(addrs[2].mAddress.FromString("fe80::22"));
+    SuccessOrQuit(addrs[3].mAddress.FromString("fe80::33"));
+    addrs[0].mTtl = 120;
+    addrs[1].mTtl = 800;
+    addrs[2].mTtl = 2000;
+    addrs[3].mTtl = 8000;
+
+    AdvanceTime(5 * 1000);
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", addrs, 4, /* aCachFlush */ true, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 4));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start another resolver for the same host and different callback. Validate results.");
+
+    resolver2.mHostName     = "myhost";
+    resolver2.mInfraIfIndex = kInfraIfIndex;
+    resolver2.mCallback     = HandleAddrResultAlternate;
+
+    sAddrCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartIp6AddressResolver(resolver2));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 4));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start same resolver again and check the returned error.");
+
+    sAddrCallbacks.Clear();
+
+    VerifyOrQuit(mdns->StartIp6AddressResolver(resolver2) == kErrorAlready);
+
+    AdvanceTime(5000);
+
+    VerifyOrQuit(sAddrCallbacks.IsEmpty());
+    sDnsMessages.Clear();
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check query is sent at 80 percentage of TTL and then respond to it.");
+
+    SendHostAddrResponse("myhost.local.", addrs, 4, /* aCachFlush */ true, kInAnswerSection);
+
+    // First query should be sent at 80-82% of TTL of 120 second (96.0-98.4 sec).
+    // We wait for 100 second. Note that 5 seconds already passed in the
+    // previous step.
+
+    AdvanceTime(96 * 1000 - 1);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(4 * 1000 + 1);
+
+    VerifyOrQuit(!sDnsMessages.IsEmpty());
+    dnsMsg = sDnsMessages.GetHead();
+    dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+    dnsMsg->ValidateAsQueryFor(resolver);
+    VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+    sDnsMessages.Clear();
+    VerifyOrQuit(sAddrCallbacks.IsEmpty());
+
+    AdvanceTime(10);
+
+    SendHostAddrResponse("myhost.local.", addrs, 4, /* aCachFlush */ true, kInAnswerSection);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check queries are sent at 80, 85, 90, 95 percentages of TTL.");
+
+    for (uint8_t queryCount = 0; queryCount < kNumRefreshQueries; queryCount++)
+    {
+        if (queryCount == 0)
+        {
+            // First query is expected in 80-82% of TTL, so
+            // 80% of 120 = 96.0, 82% of 120 = 98.4
+
+            AdvanceTime(96 * 1000 - 1);
+        }
+        else
+        {
+            // Next query should happen within 3%-5% of TTL
+            // from previous query. We wait 3% of TTL here.
+            AdvanceTime(3600 - 1);
+        }
+
+        VerifyOrQuit(sDnsMessages.IsEmpty());
+
+        // Wait for 2% of TTL of 120 which is 2.4 sec.
+
+        AdvanceTime(2400 + 1);
+
+        VerifyOrQuit(!sDnsMessages.IsEmpty());
+        dnsMsg = sDnsMessages.GetHead();
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        VerifyOrQuit(dnsMsg->GetNext() == nullptr);
+
+        sDnsMessages.Clear();
+        VerifyOrQuit(sAddrCallbacks.IsEmpty());
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check TTL timeout of first address (TTL 120) and callback result.");
+
+    AdvanceTime(6 * 1000);
+
+    addrCallback = sAddrCallbacks.GetHead();
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        VerifyOrQuit(addrCallback != nullptr);
+        VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+        VerifyOrQuit(addrCallback->Matches(&addrs[1], 3));
+        addrCallback = addrCallback->GetNext();
+    }
+
+    VerifyOrQuit(addrCallback == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Check TTL timeout of next address (TTL 800) and callback result.");
+
+    sAddrCallbacks.Clear();
+
+    AdvanceTime((800 - 120) * 1000);
+
+    addrCallback = sAddrCallbacks.GetHead();
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        VerifyOrQuit(addrCallback != nullptr);
+        VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+        VerifyOrQuit(addrCallback->Matches(&addrs[2], 2));
+        addrCallback = addrCallback->GetNext();
+    }
+
+    VerifyOrQuit(addrCallback == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    sAddrCallbacks.Clear();
+    sDnsMessages.Clear();
+
+    AdvanceTime(200 * 1000);
+
+    VerifyOrQuit(sAddrCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the second resolver");
+
+    sAddrCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopIp6AddressResolver(resolver2));
+
+    AdvanceTime(100 * 1000);
+
+    VerifyOrQuit(sAddrCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Send a new response and make sure result callback is invoked");
+
+    sAddrCallbacks.Clear();
+
+    SendHostAddrResponse("myhost.local.", addrs, 1, /* aCachFlush */ true, kInAnswerSection);
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 1));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop the resolver. There is no active resolver. Ensure no queries are sent");
+
+    sAddrCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopIp6AddressResolver(resolver));
+
+    AdvanceTime(20 * 1000);
+
+    VerifyOrQuit(sAddrCallbacks.IsEmpty());
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Restart the resolver with more than half of TTL remaining.");
+    Log("Ensure cached entry is reported in the result callback and no queries are sent.");
+
+    SuccessOrQuit(mdns->StartIp6AddressResolver(resolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 1));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    AdvanceTime(20 * 1000);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Stop and start the resolver again after less than half TTL remaining.");
+    Log("Ensure cached entry is still reported in the result callback but queries should be sent");
+
+    sAddrCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StopIp6AddressResolver(resolver));
+
+    AdvanceTime(25 * 1000);
+
+    SuccessOrQuit(mdns->StartIp6AddressResolver(resolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("myhost"));
+    VerifyOrQuit(addrCallback->Matches(addrs, 1));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    sAddrCallbacks.Clear();
+
+    AdvanceTime(15 * 1000);
+
+    dnsMsg = sDnsMessages.GetHead();
+
+    for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++)
+    {
+        VerifyOrQuit(dnsMsg != nullptr);
+        dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
+        dnsMsg->ValidateAsQueryFor(resolver);
+        dnsMsg = dnsMsg->GetNext();
+    }
+
+    VerifyOrQuit(dnsMsg == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+void TestPassiveCache(void)
+{
+    static const char *const kSubTypes[] = {"_sub1", "_xyzw"};
+
+    Core                 *mdns = InitTest();
+    Core::Browser         browser;
+    Core::SrvResolver     srvResolver;
+    Core::TxtResolver     txtResolver;
+    Core::AddressResolver addrResolver;
+    Core::Host            host1;
+    Core::Host            host2;
+    Core::Service         service1;
+    Core::Service         service2;
+    Core::Service         service3;
+    Ip6::Address          host1Addresses[3];
+    Ip6::Address          host2Addresses[2];
+    AddrAndTtl            host1AddrTtls[3];
+    AddrAndTtl            host2AddrTtls[2];
+    const DnsMessage     *dnsMsg;
+    BrowseCallback       *browseCallback;
+    SrvCallback          *srvCallback;
+    TxtCallback          *txtCallback;
+    AddrCallback         *addrCallback;
+    uint16_t              heapAllocations;
+
+    Log("-------------------------------------------------------------------------------------------");
+    Log("TestPassiveCache");
+
+    AdvanceTime(1);
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
+
+    SuccessOrQuit(host1Addresses[0].FromString("fd00::1:aaaa"));
+    SuccessOrQuit(host1Addresses[1].FromString("fd00::1:bbbb"));
+    SuccessOrQuit(host1Addresses[2].FromString("fd00::1:cccc"));
+    host1.mHostName        = "host1";
+    host1.mAddresses       = host1Addresses;
+    host1.mAddressesLength = 3;
+    host1.mTtl             = 1500;
+
+    host1AddrTtls[0].mAddress = host1Addresses[0];
+    host1AddrTtls[1].mAddress = host1Addresses[1];
+    host1AddrTtls[2].mAddress = host1Addresses[2];
+    host1AddrTtls[0].mTtl     = host1.mTtl;
+    host1AddrTtls[1].mTtl     = host1.mTtl;
+    host1AddrTtls[2].mTtl     = host1.mTtl;
+
+    SuccessOrQuit(host2Addresses[0].FromString("fd00::2:eeee"));
+    SuccessOrQuit(host2Addresses[1].FromString("fd00::2:ffff"));
+    host2.mHostName        = "host2";
+    host2.mAddresses       = host2Addresses;
+    host2.mAddressesLength = 2;
+    host2.mTtl             = 1500;
+
+    host2AddrTtls[0].mAddress = host2Addresses[0];
+    host2AddrTtls[1].mAddress = host2Addresses[1];
+    host2AddrTtls[0].mTtl     = host2.mTtl;
+    host2AddrTtls[1].mTtl     = host2.mTtl;
+
+    service1.mHostName            = host1.mHostName;
+    service1.mServiceInstance     = "srv1";
+    service1.mServiceType         = "_srv._udp";
+    service1.mSubTypeLabels       = kSubTypes;
+    service1.mSubTypeLabelsLength = 2;
+    service1.mTxtData             = kTxtData1;
+    service1.mTxtDataLength       = sizeof(kTxtData1);
+    service1.mPort                = 1111;
+    service1.mPriority            = 0;
+    service1.mWeight              = 0;
+    service1.mTtl                 = 1500;
+
+    service2.mHostName            = host1.mHostName;
+    service2.mServiceInstance     = "srv2";
+    service2.mServiceType         = "_tst._tcp";
+    service2.mSubTypeLabels       = nullptr;
+    service2.mSubTypeLabelsLength = 0;
+    service2.mTxtData             = nullptr;
+    service2.mTxtDataLength       = 0;
+    service2.mPort                = 2222;
+    service2.mPriority            = 2;
+    service2.mWeight              = 2;
+    service2.mTtl                 = 1500;
+
+    service3.mHostName            = host2.mHostName;
+    service3.mServiceInstance     = "srv3";
+    service3.mServiceType         = "_srv._udp";
+    service3.mSubTypeLabels       = kSubTypes;
+    service3.mSubTypeLabelsLength = 1;
+    service3.mTxtData             = kTxtData2;
+    service3.mTxtDataLength       = sizeof(kTxtData2);
+    service3.mPort                = 3333;
+    service3.mPriority            = 3;
+    service3.mWeight              = 3;
+    service3.mTtl                 = 1500;
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Register 2 hosts and 3 services");
+
+    SuccessOrQuit(mdns->RegisterHost(host1, 0, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterHost(host2, 1, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterService(service1, 2, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterService(service2, 3, HandleSuccessCallback));
+    SuccessOrQuit(mdns->RegisterService(service3, 4, HandleSuccessCallback));
+
+    AdvanceTime(10 * 1000);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start a browser for `_srv._udp`, validate callback result");
+
+    browser.mServiceType  = "_srv._udp";
+    browser.mSubTypeLabel = nullptr;
+    browser.mInfraIfIndex = kInfraIfIndex;
+    browser.mCallback     = HandleBrowseResult;
+
+    sBrowseCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartBrowser(browser));
+
+    AdvanceTime(350);
+
+    browseCallback = sBrowseCallbacks.GetHead();
+
+    for (uint8_t iter = 0; iter < 2; iter++)
+    {
+        VerifyOrQuit(browseCallback != nullptr);
+
+        VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+        VerifyOrQuit(!browseCallback->mIsSubType);
+        VerifyOrQuit(browseCallback->mServiceInstance.Matches("srv1") ||
+                     browseCallback->mServiceInstance.Matches("srv3"));
+        VerifyOrQuit(browseCallback->mTtl == 1500);
+
+        browseCallback = browseCallback->GetNext();
+    }
+
+    VerifyOrQuit(browseCallback == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start SRV and TXT resolvers for the srv1 and for its host name.");
+    Log("Ensure all results are immediately provided from cache.");
+
+    srvResolver.mServiceInstance = "srv1";
+    srvResolver.mServiceType     = "_srv._udp";
+    srvResolver.mInfraIfIndex    = kInfraIfIndex;
+    srvResolver.mCallback        = HandleSrvResult;
+
+    txtResolver.mServiceInstance = "srv1";
+    txtResolver.mServiceType     = "_srv._udp";
+    txtResolver.mInfraIfIndex    = kInfraIfIndex;
+    txtResolver.mCallback        = HandleTxtResult;
+
+    addrResolver.mHostName     = "host1";
+    addrResolver.mInfraIfIndex = kInfraIfIndex;
+    addrResolver.mCallback     = HandleAddrResult;
+
+    sSrvCallbacks.Clear();
+    sTxtCallbacks.Clear();
+    sAddrCallbacks.Clear();
+    sDnsMessages.Clear();
+
+    SuccessOrQuit(mdns->StartSrvResolver(srvResolver));
+    SuccessOrQuit(mdns->StartTxtResolver(txtResolver));
+    SuccessOrQuit(mdns->StartIp6AddressResolver(addrResolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("srv1"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("host1"));
+    VerifyOrQuit(srvCallback->mPort == 1111);
+    VerifyOrQuit(srvCallback->mPriority == 0);
+    VerifyOrQuit(srvCallback->mWeight == 0);
+    VerifyOrQuit(srvCallback->mTtl == 1500);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("srv1"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(txtCallback->Matches(kTxtData1));
+    VerifyOrQuit(txtCallback->mTtl == 1500);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("host1"));
+    VerifyOrQuit(addrCallback->Matches(host1AddrTtls, 3));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    AdvanceTime(400);
+
+    VerifyOrQuit(sDnsMessages.IsEmpty());
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start a browser for sub-type service, validate callback result");
+
+    browser.mServiceType  = "_srv._udp";
+    browser.mSubTypeLabel = "_xyzw";
+    browser.mInfraIfIndex = kInfraIfIndex;
+    browser.mCallback     = HandleBrowseResult;
+
+    sBrowseCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartBrowser(browser));
+
+    AdvanceTime(350);
+
+    browseCallback = sBrowseCallbacks.GetHead();
+    VerifyOrQuit(browseCallback != nullptr);
+
+    VerifyOrQuit(browseCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(browseCallback->mIsSubType);
+    VerifyOrQuit(browseCallback->mSubTypeLabel.Matches("_xyzw"));
+    VerifyOrQuit(browseCallback->mServiceInstance.Matches("srv1"));
+    VerifyOrQuit(browseCallback->mTtl == 1500);
+    VerifyOrQuit(browseCallback->GetNext() == nullptr);
+
+    AdvanceTime(5 * 1000);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start SRV and TXT resolvers for `srv2._tst._tcp` service and validate callback result");
+
+    srvResolver.mServiceInstance = "srv2";
+    srvResolver.mServiceType     = "_tst._tcp";
+    srvResolver.mInfraIfIndex    = kInfraIfIndex;
+    srvResolver.mCallback        = HandleSrvResult;
+
+    txtResolver.mServiceInstance = "srv2";
+    txtResolver.mServiceType     = "_tst._tcp";
+    txtResolver.mInfraIfIndex    = kInfraIfIndex;
+    txtResolver.mCallback        = HandleTxtResult;
+
+    sSrvCallbacks.Clear();
+    sTxtCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartSrvResolver(srvResolver));
+    SuccessOrQuit(mdns->StartTxtResolver(txtResolver));
+
+    AdvanceTime(350);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("srv2"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_tst._tcp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("host1"));
+    VerifyOrQuit(srvCallback->mPort == 2222);
+    VerifyOrQuit(srvCallback->mPriority == 2);
+    VerifyOrQuit(srvCallback->mWeight == 2);
+    VerifyOrQuit(srvCallback->mTtl == 1500);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("srv2"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_tst._tcp"));
+    VerifyOrQuit(txtCallback->Matches(kEmptyTxtData));
+    VerifyOrQuit(txtCallback->mTtl == 1500);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Unregister `srv2._tst._tcp` and validate callback results");
+
+    sSrvCallbacks.Clear();
+    sTxtCallbacks.Clear();
+
+    SuccessOrQuit(mdns->UnregisterService(service2));
+
+    AdvanceTime(350);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("srv2"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_tst._tcp"));
+    VerifyOrQuit(srvCallback->mTtl == 0);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    VerifyOrQuit(!sTxtCallbacks.IsEmpty());
+    txtCallback = sTxtCallbacks.GetHead();
+    VerifyOrQuit(txtCallback->mServiceInstance.Matches("srv2"));
+    VerifyOrQuit(txtCallback->mServiceType.Matches("_tst._tcp"));
+    VerifyOrQuit(txtCallback->mTtl == 0);
+    VerifyOrQuit(txtCallback->GetNext() == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start an SRV resolver for `srv3._srv._udp` service and validate callback result");
+
+    srvResolver.mServiceInstance = "srv3";
+    srvResolver.mServiceType     = "_srv._udp";
+    srvResolver.mInfraIfIndex    = kInfraIfIndex;
+    srvResolver.mCallback        = HandleSrvResult;
+
+    sSrvCallbacks.Clear();
+
+    SuccessOrQuit(mdns->StartSrvResolver(srvResolver));
+
+    AdvanceTime(350);
+
+    VerifyOrQuit(!sSrvCallbacks.IsEmpty());
+    srvCallback = sSrvCallbacks.GetHead();
+    VerifyOrQuit(srvCallback->mServiceInstance.Matches("srv3"));
+    VerifyOrQuit(srvCallback->mServiceType.Matches("_srv._udp"));
+    VerifyOrQuit(srvCallback->mHostName.Matches("host2"));
+    VerifyOrQuit(srvCallback->mPort == 3333);
+    VerifyOrQuit(srvCallback->mPriority == 3);
+    VerifyOrQuit(srvCallback->mWeight == 3);
+    VerifyOrQuit(srvCallback->mTtl == 1500);
+    VerifyOrQuit(srvCallback->GetNext() == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+    Log("Start an address resolver for host2 and validate result is immediately reported from cache");
+
+    addrResolver.mHostName     = "host2";
+    addrResolver.mInfraIfIndex = kInfraIfIndex;
+    addrResolver.mCallback     = HandleAddrResult;
+
+    sAddrCallbacks.Clear();
+    SuccessOrQuit(mdns->StartIp6AddressResolver(addrResolver));
+
+    AdvanceTime(1);
+
+    VerifyOrQuit(!sAddrCallbacks.IsEmpty());
+    addrCallback = sAddrCallbacks.GetHead();
+    VerifyOrQuit(addrCallback->mHostName.Matches("host2"));
+    VerifyOrQuit(addrCallback->Matches(host2AddrTtls, 2));
+    VerifyOrQuit(addrCallback->GetNext() == nullptr);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
+
+    SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
+    VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
+
+    Log("End of test");
+
+    testFreeInstance(sInstance);
+}
+
+} // namespace Multicast
+} // namespace Dns
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+int main(void)
+{
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+    ot::Dns::Multicast::TestHostReg();
+    ot::Dns::Multicast::TestKeyReg();
+    ot::Dns::Multicast::TestServiceReg();
+    ot::Dns::Multicast::TestUnregisterBeforeProbeFinished();
+    ot::Dns::Multicast::TestServiceSubTypeReg();
+    ot::Dns::Multicast::TestHostOrServiceAndKeyReg();
+    ot::Dns::Multicast::TestQuery();
+    ot::Dns::Multicast::TestMultiPacket();
+    ot::Dns::Multicast::TestQuestionUnicastDisallowed();
+    ot::Dns::Multicast::TestTxMessageSizeLimit();
+    ot::Dns::Multicast::TestHostConflict();
+    ot::Dns::Multicast::TestServiceConflict();
+
+    ot::Dns::Multicast::TestBrowser();
+    ot::Dns::Multicast::TestSrvResolver();
+    ot::Dns::Multicast::TestTxtResolver();
+    ot::Dns::Multicast::TestIp6AddrResolver();
+    ot::Dns::Multicast::TestPassiveCache();
+
+    printf("All tests passed\n");
+#else
+    printf("mDNS feature is not enabled\n");
+#endif
+
+    return 0;
+}
diff --git a/tests/unit/test_network_data.cpp b/tests/unit/test_network_data.cpp
index 2868709..4740a2f 100644
--- a/tests/unit/test_network_data.cpp
+++ b/tests/unit/test_network_data.cpp
@@ -86,32 +86,32 @@
            (aConfig1.mDefaultRoute == aConfig2.mDefaultRoute) && (aConfig1.mOnMesh == aConfig2.mOnMesh);
 }
 
-template <uint8_t kLength>
-void VerifyRlocsArray(const uint16_t *aRlocs, uint16_t aRlocsLength, const uint16_t (&aExpectedRlocs)[kLength])
+template <uint8_t kLength> void VerifyRlocsArray(const Rlocs &aRlocs, const uint16_t (&aExpectedRlocs)[kLength])
 {
-    VerifyOrQuit(aRlocsLength == kLength);
+    VerifyOrQuit(aRlocs.GetLength() == kLength);
 
     printf("\nRLOCs: { ");
 
-    for (uint16_t index = 0; index < aRlocsLength; index++)
+    for (uint16_t rloc : aRlocs)
     {
-        VerifyOrQuit(aRlocs[index] == aExpectedRlocs[index]);
-        printf("0x%04x ", aRlocs[index]);
+        printf("0x%04x ", rloc);
     }
 
     printf("}");
+
+    for (uint16_t index = 0; index < kLength; index++)
+    {
+        VerifyOrQuit(aRlocs.Contains(aExpectedRlocs[index]));
+    }
 }
 
 void TestNetworkDataIterator(void)
 {
-    static constexpr uint8_t kMaxRlocsArray = 10;
-
     Instance           *instance;
     Iterator            iter = kIteratorInit;
     ExternalRouteConfig rconfig;
     OnMeshPrefixConfig  pconfig;
-    uint16_t            rlocs[kMaxRlocsArray];
-    uint8_t             rlocsLength;
+    Rlocs               rlocs;
 
     instance = testInitInstance();
     VerifyOrQuit(instance != nullptr);
@@ -168,19 +168,25 @@
             VerifyOrQuit(CompareExternalRouteConfig(rconfig, route));
         }
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kAnyRole, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocs);
+        netData.FindRlocs(kAnyBrOrServer, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kRlocs);
+
+        netData.FindRlocs(kAnyBrOrServer, kRouterRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocs);
+
+        netData.FindRlocs(kAnyBrOrServer, kChildRoleOnly, rlocs);
+        VerifyOrQuit(rlocs.GetLength() == 0);
+
+        netData.FindRlocs(kBrProvidingExternalIpConn, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kRlocs);
         VerifyOrQuit(netData.CountBorderRouters(kAnyRole) == GetArrayLength(kRlocs));
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kRouterRoleOnly, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocs);
+        netData.FindRlocs(kBrProvidingExternalIpConn, kRouterRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocs);
         VerifyOrQuit(netData.CountBorderRouters(kRouterRoleOnly) == GetArrayLength(kRlocs));
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kChildRoleOnly, rlocs, rlocsLength));
-        VerifyOrQuit(rlocsLength == 0);
+        netData.FindRlocs(kBrProvidingExternalIpConn, kChildRoleOnly, rlocs);
+        VerifyOrQuit(rlocs.GetLength() == 0);
         VerifyOrQuit(netData.CountBorderRouters(kChildRoleOnly) == 0);
 
         for (uint16_t rloc16 : kRlocs)
@@ -284,33 +290,29 @@
             VerifyOrQuit(CompareExternalRouteConfig(rconfig, route));
         }
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kAnyRole, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocsAnyRole);
+        netData.FindRlocs(kAnyBrOrServer, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsAnyRole);
+
+        netData.FindRlocs(kAnyBrOrServer, kRouterRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsRouterRole);
+
+        netData.FindRlocs(kAnyBrOrServer, kChildRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsChildRole);
+
+        netData.FindRlocs(kBrProvidingExternalIpConn, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsAnyRole);
         VerifyOrQuit(netData.CountBorderRouters(kAnyRole) == GetArrayLength(kRlocsAnyRole));
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kRouterRoleOnly, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocsRouterRole);
+        netData.FindRlocs(kBrProvidingExternalIpConn, kRouterRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsRouterRole);
         VerifyOrQuit(netData.CountBorderRouters(kRouterRoleOnly) == GetArrayLength(kRlocsRouterRole));
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kChildRoleOnly, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocsChildRole);
+        netData.FindRlocs(kBrProvidingExternalIpConn, kChildRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsChildRole);
         VerifyOrQuit(netData.CountBorderRouters(kChildRoleOnly) == GetArrayLength(kRlocsChildRole));
 
-        // Test failure case when given array is smaller than number of RLOCs.
-        rlocsLength = GetArrayLength(kRlocsAnyRole) - 1;
-        VerifyOrQuit(netData.FindBorderRouters(kAnyRole, rlocs, rlocsLength) == kErrorNoBufs);
-        VerifyOrQuit(rlocsLength == GetArrayLength(kRlocsAnyRole) - 1);
-        for (uint8_t index = 0; index < rlocsLength; index++)
-        {
-            VerifyOrQuit(rlocs[index] == kRlocsAnyRole[index]);
-        }
-
-        rlocsLength = GetArrayLength(kRlocsAnyRole);
-        SuccessOrQuit(netData.FindBorderRouters(kAnyRole, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocsAnyRole);
+        netData.FindRlocs(kBrProvidingExternalIpConn, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsAnyRole);
 
         for (uint16_t rloc16 : kRlocsAnyRole)
         {
@@ -434,10 +436,13 @@
             },
         };
 
-        const uint16_t kRlocsAnyRole[]     = {0xec00, 0x2801, 0x2800};
-        const uint16_t kRlocsRouterRole[]  = {0xec00, 0x2800};
-        const uint16_t kRlocsChildRole[]   = {0x2801};
-        const uint16_t kNonExistingRlocs[] = {0x6000, 0x0000, 0x2806, 0x4c00};
+        const uint16_t kRlocsAnyRole[]      = {0xec00, 0x2801, 0x2800, 0x4c00};
+        const uint16_t kRlocsRouterRole[]   = {0xec00, 0x2800, 0x4c00};
+        const uint16_t kRlocsChildRole[]    = {0x2801};
+        const uint16_t kBrRlocsAnyRole[]    = {0xec00, 0x2801, 0x2800};
+        const uint16_t kBrRlocsRouterRole[] = {0xec00, 0x2800};
+        const uint16_t kBrRlocsChildRole[]  = {0x2801};
+        const uint16_t kNonExistingRlocs[]  = {0x6000, 0x0000, 0x2806, 0x4c00};
 
         NetworkData netData(*instance, kNetworkData, sizeof(kNetworkData));
 
@@ -462,22 +467,28 @@
             VerifyOrQuit(CompareOnMeshPrefixConfig(pconfig, prefix));
         }
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kAnyRole, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocsAnyRole);
-        VerifyOrQuit(netData.CountBorderRouters(kAnyRole) == GetArrayLength(kRlocsAnyRole));
+        netData.FindRlocs(kAnyBrOrServer, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsAnyRole);
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kRouterRoleOnly, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocsRouterRole);
-        VerifyOrQuit(netData.CountBorderRouters(kRouterRoleOnly) == GetArrayLength(kRlocsRouterRole));
+        netData.FindRlocs(kAnyBrOrServer, kRouterRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsRouterRole);
 
-        rlocsLength = GetArrayLength(rlocs);
-        SuccessOrQuit(netData.FindBorderRouters(kChildRoleOnly, rlocs, rlocsLength));
-        VerifyRlocsArray(rlocs, rlocsLength, kRlocsChildRole);
-        VerifyOrQuit(netData.CountBorderRouters(kChildRoleOnly) == GetArrayLength(kRlocsChildRole));
+        netData.FindRlocs(kAnyBrOrServer, kChildRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kRlocsChildRole);
 
-        for (uint16_t rloc16 : kRlocsAnyRole)
+        netData.FindRlocs(kBrProvidingExternalIpConn, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kBrRlocsAnyRole);
+        VerifyOrQuit(netData.CountBorderRouters(kAnyRole) == GetArrayLength(kBrRlocsAnyRole));
+
+        netData.FindRlocs(kBrProvidingExternalIpConn, kRouterRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kBrRlocsRouterRole);
+        VerifyOrQuit(netData.CountBorderRouters(kRouterRoleOnly) == GetArrayLength(kBrRlocsRouterRole));
+
+        netData.FindRlocs(kBrProvidingExternalIpConn, kChildRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kBrRlocsChildRole);
+        VerifyOrQuit(netData.CountBorderRouters(kChildRoleOnly) == GetArrayLength(kBrRlocsChildRole));
+
+        for (uint16_t rloc16 : kBrRlocsAnyRole)
         {
             VerifyOrQuit(netData.ContainsBorderRouterWithRloc(rloc16));
         }
@@ -681,17 +692,34 @@
             {"fdde:ad00:beef:0:0:ff:fe00:6c00", 0xcd12, Service::DnsSrpUnicast::kFromServerData, 0x6c00},
         };
 
+        const uint16_t kExpectedRlocs[] = {0x6c00, 0x2800, 0x4c00, 0x0000};
+
         const uint8_t kPreferredAnycastEntryIndex = 2;
 
         Service::Manager            &manager = instance->Get<Service::Manager>();
         Service::Manager::Iterator   iterator;
         Service::DnsSrpAnycast::Info anycastInfo;
         Service::DnsSrpUnicast::Info unicastInfo;
+        Rlocs                        rlocs;
 
         reinterpret_cast<TestLeader &>(instance->Get<Leader>()).Populate(kNetworkData, sizeof(kNetworkData));
 
         DumpBuffer("netdata", kNetworkData, sizeof(kNetworkData));
 
+        // Verify `FindRlocs()`
+
+        instance->Get<Leader>().FindRlocs(kAnyBrOrServer, kAnyRole, rlocs);
+        VerifyRlocsArray(rlocs, kExpectedRlocs);
+
+        instance->Get<Leader>().FindRlocs(kAnyBrOrServer, kRouterRoleOnly, rlocs);
+        VerifyRlocsArray(rlocs, kExpectedRlocs);
+
+        instance->Get<Leader>().FindRlocs(kAnyBrOrServer, kChildRoleOnly, rlocs);
+        VerifyOrQuit(rlocs.GetLength() == 0);
+
+        instance->Get<Leader>().FindRlocs(kBrProvidingExternalIpConn, kAnyRole, rlocs);
+        VerifyOrQuit(rlocs.GetLength() == 0);
+
         // Verify all the "DNS/SRP Anycast Service" entries in Network Data
 
         printf("\n- - - - - - - - - - - - - - - - - - - -");
diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp
index 40e23ce..496dc49 100644
--- a/tests/unit/test_platform.cpp
+++ b/tests/unit/test_platform.cpp
@@ -555,6 +555,35 @@
     return OT_ERROR_NONE;
 }
 
+#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
+OT_TOOL_WEAK otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+}
+
+OT_TOOL_WEAK void otPlatMdnsSendUnicast(otInstance                  *aInstance,
+                                        otMessage                   *aMessage,
+                                        const otPlatMdnsAddressInfo *aAddress)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aAddress);
+}
+
+#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE
+
 #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE
 
 OT_TOOL_WEAK void otPlatDsoEnableListening(otInstance *aInstance, bool aEnable)
@@ -694,39 +723,39 @@
 otError otPlatBleEnable(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleDisable(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGapAdvStart(otInstance *aInstance, uint16_t aInterval)
 {
     OT_UNUSED_VARIABLE(aInstance);
     OT_UNUSED_VARIABLE(aInterval);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGapAdvStop(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGapDisconnect(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGattMtuGet(otInstance *aInstance, uint16_t *aMtu)
 {
     OT_UNUSED_VARIABLE(aInstance);
     OT_UNUSED_VARIABLE(aMtu);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 
 otError otPlatBleGattServerIndicate(otInstance *aInstance, uint16_t aHandle, const otBleRadioPacket *aPacket)
@@ -734,7 +763,7 @@
     OT_UNUSED_VARIABLE(aInstance);
     OT_UNUSED_VARIABLE(aHandle);
     OT_UNUSED_VARIABLE(aPacket);
-    return OT_ERROR_NOT_IMPLEMENTED;
+    return OT_ERROR_NONE;
 }
 #endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
 
diff --git a/tests/unit/test_platform.h b/tests/unit/test_platform.h
index f1c7eb4..8f9d291 100644
--- a/tests/unit/test_platform.h
+++ b/tests/unit/test_platform.h
@@ -38,6 +38,7 @@
 #include <openthread/platform/dso_transport.h>
 #include <openthread/platform/entropy.h>
 #include <openthread/platform/logging.h>
+#include <openthread/platform/mdns_socket.h>
 #include <openthread/platform/misc.h>
 #include <openthread/platform/multipan.h>
 #include <openthread/platform/radio.h>
diff --git a/tests/unit/test_pskc.cpp b/tests/unit/test_pskc.cpp
index 2515637..b1a3877 100644
--- a/tests/unit/test_pskc.cpp
+++ b/tests/unit/test_pskc.cpp
@@ -34,71 +34,81 @@
 #include "test_util.h"
 
 namespace ot {
+namespace MeshCoP {
 
 #if OPENTHREAD_FTD
 
 void TestMinimumPassphrase(void)
 {
-    ot::Pskc              pskc;
-    const uint8_t         expectedPskc[] = {0x44, 0x98, 0x8e, 0x22, 0xcf, 0x65, 0x2e, 0xee,
-                                            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();
-    SuccessOrQuit(ot::MeshCoP::GeneratePskc(passphrase,
-                                            *reinterpret_cast<const ot::MeshCoP::NetworkName *>("OpenThread"),
-                                            static_cast<const ot::MeshCoP::ExtendedPanId &>(xpanid), pskc));
-    VerifyOrQuit(memcmp(pskc.m8, expectedPskc, OT_PSKC_MAX_SIZE) == 0);
+    static const otExtendedPanId kExtPanId     = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}};
+    static const otNetworkName   kNetworkName  = {{'O', 'p', 'e', 'n', 'T', 'h', 'r', 'e', 'a', 'd', '\0'}};
+    static const char            kPassphrase[] = "123456";
+
+    static const otPskc kExpectedPskc = {
+        {0x44, 0x98, 0x8e, 0x22, 0xcf, 0x65, 0x2e, 0xee, 0xcc, 0xd1, 0xe4, 0xc0, 0x1d, 0x01, 0x54, 0xf8}};
+
+    Instance *instance = testInitInstance();
+    Pskc      pskc;
+
+    SuccessOrQuit(GeneratePskc(kPassphrase, AsCoreType(&kNetworkName), AsCoreType(&kExtPanId), pskc));
+    VerifyOrQuit(pskc == AsCoreType(&kExpectedPskc));
+
     testFreeInstance(instance);
 }
 
 void TestMaximumPassphrase(void)
 {
-    ot::Pskc              pskc;
-    const uint8_t         expectedPskc[] = {0x9e, 0x81, 0xbd, 0x35, 0xa2, 0x53, 0x76, 0x2f,
-                                            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";
+    static const otExtendedPanId kExtPanId    = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}};
+    static const otNetworkName   kNetworkName = {{'O', 'p', 'e', 'n', 'T', 'h', 'r', 'e', 'a', 'd', '\0'}};
 
-    otInstance *instance = testInitInstance();
-    SuccessOrQuit(ot::MeshCoP::GeneratePskc(passphrase,
-                                            *reinterpret_cast<const ot::MeshCoP::NetworkName *>("OpenThread"),
-                                            static_cast<const ot::MeshCoP::ExtendedPanId &>(xpanid), pskc));
-    VerifyOrQuit(memcmp(pskc.m8, expectedPskc, sizeof(pskc.m8)) == 0);
+    static const char kPassphrase[] = "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "1234567812345678"
+                                      "123456781234567";
+
+    static const otPskc kExpectedPskc = {
+        {0x9e, 0x81, 0xbd, 0x35, 0xa2, 0x53, 0x76, 0x2f, 0x80, 0xee, 0x04, 0xff, 0x2f, 0xa2, 0x85, 0xe9}};
+
+    Instance *instance = testInitInstance();
+    Pskc      pskc;
+
+    SuccessOrQuit(GeneratePskc(kPassphrase, AsCoreType(&kNetworkName), AsCoreType(&kExtPanId), pskc));
+    VerifyOrQuit(pskc == AsCoreType(&kExpectedPskc));
+
     testFreeInstance(instance);
 }
 
 void TestExampleInSpec(void)
 {
-    ot::Pskc              pskc;
-    const uint8_t         expectedPskc[] = {0xc3, 0xf5, 0x93, 0x68, 0x44, 0x5a, 0x1b, 0x61,
-                                            0x06, 0xbe, 0x42, 0x0a, 0x70, 0x6d, 0x4c, 0xc9};
-    const otExtendedPanId xpanid         = {{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}};
-    const char            passphrase[]   = "12SECRETPASSWORD34";
+    static const otExtendedPanId kExtPanId     = {{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}};
+    static const otNetworkName   kNetworkName  = {{'T', 'e', 's', 't', ' ', 'N', 'e', 't', 'w', 'o', 'r', 'k', '\0'}};
+    static const char            kPassphrase[] = "12SECRETPASSWORD34";
 
-    otInstance *instance = testInitInstance();
-    SuccessOrQuit(ot::MeshCoP::GeneratePskc(passphrase,
-                                            *reinterpret_cast<const ot::MeshCoP::NetworkName *>("Test Network"),
-                                            static_cast<const ot::MeshCoP::ExtendedPanId &>(xpanid), pskc));
-    VerifyOrQuit(memcmp(pskc.m8, expectedPskc, sizeof(pskc.m8)) == 0);
+    static const otPskc kExpectedPskc = {
+        {0xc3, 0xf5, 0x93, 0x68, 0x44, 0x5a, 0x1b, 0x61, 0x06, 0xbe, 0x42, 0x0a, 0x70, 0x6d, 0x4c, 0xc9}};
+
+    Instance *instance = testInitInstance();
+    Pskc      pskc;
+
+    SuccessOrQuit(GeneratePskc(kPassphrase, AsCoreType(&kNetworkName), AsCoreType(&kExtPanId), pskc));
+    VerifyOrQuit(pskc == AsCoreType(&kExpectedPskc));
+
     testFreeInstance(instance);
 }
 
+} // namespace MeshCoP
 } // namespace ot
 
 #endif // OPENTHREAD_FTD
@@ -106,9 +116,9 @@
 int main(void)
 {
 #if OPENTHREAD_FTD
-    ot::TestMinimumPassphrase();
-    ot::TestMaximumPassphrase();
-    ot::TestExampleInSpec();
+    ot::MeshCoP::TestMinimumPassphrase();
+    ot::MeshCoP::TestMaximumPassphrase();
+    ot::MeshCoP::TestExampleInSpec();
     printf("All tests passed\n");
 #else
     printf("PSKc generation is not supported on non-ftd build\n");
diff --git a/tests/unit/test_routing_manager.cpp b/tests/unit/test_routing_manager.cpp
index df7c2b2..a474de4 100644
--- a/tests/unit/test_routing_manager.cpp
+++ b/tests/unit/test_routing_manager.cpp
@@ -126,7 +126,7 @@
 static uint8_t      sRadioTxFramePsdu[OT_RADIO_FRAME_MAX_SIZE];
 static bool         sRadioTxOngoing = false;
 
-using Icmp6Packet = Ip6::Nd::RouterAdvertMessage::Icmp6Packet;
+using Icmp6Packet = Ip6::Nd::RouterAdvert::Icmp6Packet;
 
 enum ExpectedPio
 {
@@ -160,6 +160,12 @@
 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.
 
+// Indicate whether or not to check the emitted RA header (default route) lifetime
+bool sCheckRaHeaderLifetime;
+
+// Expected default route lifetime in emitted RA header by BR.
+uint32_t sExpectedRaHeaderLifetime;
+
 enum ExpectedRaHeaderFlags
 {
     kRaHeaderFlagsSkipChecking, // Skip checking the RA header flags.
@@ -414,7 +420,7 @@
 {
     constexpr uint8_t kMaxPrefixes = 16;
 
-    Ip6::Nd::RouterAdvertMessage     raMsg(aPacket);
+    Ip6::Nd::RouterAdvert::RxMessage raMsg(aPacket);
     bool                             sawExpectedPio = false;
     Array<Ip6::Prefix, kMaxPrefixes> pioPrefixes;
     Array<Ip6::Prefix, kMaxPrefixes> rioPrefixes;
@@ -424,7 +430,10 @@
 
     VerifyOrQuit(raMsg.IsValid());
 
-    VerifyOrQuit(raMsg.GetHeader().GetRouterLifetime() == 0);
+    if (sCheckRaHeaderLifetime)
+    {
+        VerifyOrQuit(raMsg.GetHeader().GetRouterLifetime() == sExpectedRaHeaderLifetime);
+    }
 
     switch (sExpectedRaHeaderFlags)
     {
@@ -570,7 +579,7 @@
 
 void LogRouterAdvert(const Icmp6Packet &aPacket)
 {
-    Ip6::Nd::RouterAdvertMessage raMsg(aPacket);
+    Ip6::Nd::RouterAdvert::RxMessage raMsg(aPacket);
 
     VerifyOrQuit(raMsg.IsValid());
 
@@ -854,17 +863,15 @@
     bool mStubRouterFlag;
 };
 
-template <size_t N>
-uint16_t BuildRouterAdvert(uint8_t (&aBuffer)[N],
-                           const Pio          *aPios,
-                           uint16_t            aNumPios,
-                           const Rio          *aRios,
-                           uint16_t            aNumRios,
-                           const DefaultRoute &aDefaultRoute,
-                           const RaFlags      &aRaFlags)
+void BuildRouterAdvert(Ip6::Nd::RouterAdvert::TxMessage &aRaMsg,
+                       const Pio                        *aPios,
+                       uint16_t                          aNumPios,
+                       const Rio                        *aRios,
+                       uint16_t                          aNumRios,
+                       const DefaultRoute               &aDefaultRoute,
+                       const RaFlags                    &aRaFlags)
 {
-    Ip6::Nd::RouterAdvertMessage::Header header;
-    uint16_t                             length;
+    Ip6::Nd::RouterAdvert::Header header;
 
     header.SetRouterLifetime(aDefaultRoute.mLifetime);
     header.SetDefaultRouterPreference(aDefaultRoute.mPreference);
@@ -879,29 +886,22 @@
         header.SetOtherConfigFlag();
     }
 
+    SuccessOrQuit(aRaMsg.AppendHeader(header));
+
+    if (aRaFlags.mStubRouterFlag)
     {
-        Ip6::Nd::RouterAdvertMessage raMsg(header, aBuffer);
-
-        if (aRaFlags.mStubRouterFlag)
-        {
-            SuccessOrQuit(raMsg.AppendFlagsExtensionOption(/* aStubRouterFlag */ true));
-        }
-
-        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));
-        }
-
-        length = raMsg.GetAsPacket().GetLength();
+        SuccessOrQuit(aRaMsg.AppendFlagsExtensionOption(/* aStubRouterFlag */ true));
     }
 
-    return length;
+    for (; aNumPios > 0; aPios++, aNumPios--)
+    {
+        SuccessOrQuit(aRaMsg.AppendPrefixInfoOption(aPios->mPrefix, aPios->mValidLifetime, aPios->mPreferredLifetime));
+    }
+
+    for (; aNumRios > 0; aRios++, aNumRios--)
+    {
+        SuccessOrQuit(aRaMsg.AppendRouteInfoOption(aRios->mPrefix, aRios->mValidLifetime, aRios->mPreference));
+    }
 }
 
 void SendRouterAdvert(const Ip6::Address &aRouterAddress,
@@ -912,12 +912,15 @@
                       const DefaultRoute &aDefaultRoute,
                       const RaFlags      &aRaFlags)
 {
-    uint8_t  buffer[kMaxRaSize];
-    uint16_t length = BuildRouterAdvert(buffer, aPios, aNumPios, aRios, aNumRios, aDefaultRoute, aRaFlags);
+    Ip6::Nd::RouterAdvert::TxMessage raMsg;
+    Icmp6Packet                      packet;
 
-    SendRouterAdvert(aRouterAddress, buffer, length);
+    BuildRouterAdvert(raMsg, aPios, aNumPios, aRios, aNumRios, aDefaultRoute, aRaFlags);
+    raMsg.GetAsPacket(packet);
+
+    SendRouterAdvert(aRouterAddress, packet);
     Log("Sending RA from router %s", aRouterAddress.ToString().AsCString());
-    LogRouterAdvert(buffer, length);
+    LogRouterAdvert(packet);
 }
 
 template <uint16_t kNumPios, uint16_t kNumRios>
@@ -963,13 +966,16 @@
 
 template <uint16_t kNumPios> void SendRouterAdvertToBorderRoutingProcessIcmp6Ra(const Pio (&aPios)[kNumPios])
 {
-    uint8_t  buffer[kMaxRaSize];
-    uint16_t length = BuildRouterAdvert(buffer, aPios, kNumPios, nullptr, 0,
-                                        DefaultRoute(0, NetworkData::kRoutePreferenceMedium), RaFlags());
+    Ip6::Nd::RouterAdvert::TxMessage raMsg;
+    Icmp6Packet                      packet;
 
-    otPlatBorderRoutingProcessIcmp6Ra(sInstance, buffer, length);
+    BuildRouterAdvert(raMsg, aPios, kNumPios, nullptr, 0, DefaultRoute(0, NetworkData::kRoutePreferenceMedium),
+                      RaFlags());
+    raMsg.GetAsPacket(packet);
+
+    otPlatBorderRoutingProcessIcmp6Ra(sInstance, packet.GetBytes(), packet.GetLength());
     Log("Passing RA to otPlatBorderRoutingProcessIcmp6Ra");
-    LogRouterAdvert(buffer, length);
+    LogRouterAdvert(packet);
 }
 
 struct OnLinkPrefix : public Pio
@@ -1191,8 +1197,10 @@
     sRaValidated = false;
     sExpectedPio = kNoPio;
     sExpectedRios.Clear();
-    sRespondToNs           = true;
-    sExpectedRaHeaderFlags = kRaHeaderFlagsNone;
+    sRespondToNs              = true;
+    sExpectedRaHeaderFlags    = kRaHeaderFlagsNone;
+    sCheckRaHeaderLifetime    = true;
+    sExpectedRaHeaderLifetime = 0;
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Ensure device starts as leader.
@@ -2953,6 +2961,93 @@
     FinalizeTest();
 }
 
+void TestLearnRaHeader(void)
+{
+    Ip6::Prefix localOnLink;
+    Ip6::Prefix localOmr;
+    Ip6::Prefix onLinkPrefix = PrefixFromString("2000:abba:baba::", 64);
+    uint16_t    heapAllocations;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestLearnRaHeader");
+
+    InitTest();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start Routing Manager. Check emitted RS and RA messages.
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+    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");
+
+    VerifyDiscoveredRoutersIsEmpty();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from the same address (another entity on the device)
+    // advertising a default route.
+
+    SendRouterAdvert(sInfraIfAddress, DefaultRoute(1000, NetworkData::kRoutePreferenceLow));
+
+    AdvanceTime(1);
+    VerifyDiscoveredRouters({InfraRouter(sInfraIfAddress, /* M */ false, /* O */ false, /* StubRouter */ false)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // RoutingManager should learn the header from the
+    // received RA (from same address) and start advertising
+    // the same default route lifetime in the emitted RAs.
+
+    sRaValidated              = false;
+    sCheckRaHeaderLifetime    = true;
+    sExpectedRaHeaderLifetime = 1000;
+
+    AdvanceTime(30 * 1000);
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for longer than entry lifetime (for it to expire) and
+    // make sure `RoutingManager` stops advertising default route.
+
+    sCheckRaHeaderLifetime = false;
+
+    AdvanceTime(1000 * 1000);
+
+    sRaValidated              = false;
+    sCheckRaHeaderLifetime    = true;
+    sExpectedRaHeaderLifetime = 0;
+
+    AdvanceTime(700 * 1000);
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(false));
+    VerifyDiscoveredRoutersIsEmpty();
+
+    VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength());
+
+    Log("End of TestLearnRaHeader");
+    FinalizeTest();
+}
+
 void TestConflictingPrefix(void)
 {
     static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}};
@@ -3924,6 +4019,7 @@
     ot::TestConflictingPrefix();
     ot::TestRouterNsProbe();
     ot::TestLearningAndCopyingOfFlags();
+    ot::TestLearnRaHeader();
 #if OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE
     ot::TestSavedOnLinkPrefixes();
 #endif
diff --git a/tests/unit/test_srp_adv_proxy.cpp b/tests/unit/test_srp_adv_proxy.cpp
index 9fe8809..ffd954a 100644
--- a/tests/unit/test_srp_adv_proxy.cpp
+++ b/tests/unit/test_srp_adv_proxy.cpp
@@ -44,7 +44,7 @@
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE &&                   \
     OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE && !OPENTHREAD_CONFIG_TIME_SYNC_ENABLE && \
-    !OPENTHREAD_PLATFORM_POSIX
+    !OPENTHREAD_PLATFORM_POSIX && OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION
 #define ENABLE_ADV_PROXY_TEST 1
 #else
 #define ENABLE_ADV_PROXY_TEST 0
@@ -543,6 +543,10 @@
     SuccessOrQuit(otIp6SetEnabled(sInstance, true));
     SuccessOrQuit(otThreadSetEnabled(sInstance, true));
 
+    // Configure the `Dnssd` module to use `otPlatDnssd` APIs.
+
+    sInstance->Get<Dnssd>().SetUseNativeMdns(false);
+
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Ensure device starts as leader.
 
diff --git a/tests/unit/test_tcat.cpp b/tests/unit/test_tcat.cpp
new file mode 100644
index 0000000..6767a0f
--- /dev/null
+++ b/tests/unit/test_tcat.cpp
@@ -0,0 +1,170 @@
+/*
+ *  Copyright (c) 2024, 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-core-config.h"
+
+#include "test_platform.h"
+#include "test_util.h"
+
+#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
+
+#include <openthread/ble_secure.h>
+
+#define OT_TCAT_X509_CERT                                                  \
+    "-----BEGIN CERTIFICATE-----\r\n"                                      \
+    "MIIBmDCCAT+gAwIBAgIEAQIDBDAKBggqhkjOPQQDAjBvMQswCQYDVQQGEwJYWDEQ\r\n" \
+    "MA4GA1UECBMHTXlTdGF0ZTEPMA0GA1UEBxMGTXlDaXR5MQ8wDQYDVQQLEwZNeVVu\r\n" \
+    "aXQxETAPBgNVBAoTCE15VmVuZG9yMRkwFwYDVQQDExB3d3cubXl2ZW5kb3IuY29t\r\n" \
+    "MB4XDTIzMTAxNjEwMzk1NFoXDTI0MTAxNjEwMzk1NFowIjEgMB4GA1UEAxMXbXl2\r\n" \
+    "ZW5kb3IuY29tL3RjYXQvbXlkZXYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQB\r\n" \
+    "aWwFDNj1bpQIdN+Kp2cHWw55U/+fa+OmZnoy1B4BOT+822jdwPBuyXWAQoBdYdQJ\r\n" \
+    "ff4RgmhczyV4PhArPIuAoxYwFDASBgkrBgEEAYLfKgMEBQABAQEBMAoGCCqGSM49\r\n" \
+    "BAMCA0cAMEQCIBEHxiEDij26y6V77Q311Gj4CZAuZuPGXZpnzL2BLk7bAiAlFk6G\r\n" \
+    "mYGzkcrYyssFI9HlPgrisWoMmgummaTtCuvrEw==\r\n"                         \
+    "-----END CERTIFICATE-----\r\n"
+
+#define OT_TCAT_PRIV_KEY                                                   \
+    "-----BEGIN EC PRIVATE KEY-----\r\n"                                   \
+    "MHcCAQEEIDeJ6lVQKiOIBxKwTZp6TkU5QVHt9pvXOR9CGpPBI3DhoAoGCCqGSM49\r\n" \
+    "AwEHoUQDQgAEAWlsBQzY9W6UCHTfiqdnB1sOeVP/n2vjpmZ6MtQeATk/vNto3cDw\r\n" \
+    "bsl1gEKAXWHUCX3+EYJoXM8leD4QKzyLgA==\r\n"                             \
+    "-----END EC PRIVATE KEY-----\r\n"
+
+#define OT_TCAT_TRUSTED_ROOT_CERTIFICATE                                   \
+    "-----BEGIN CERTIFICATE-----\r\n"                                      \
+    "MIICCDCCAa2gAwIBAgIJAIKxygBXoH+5MAoGCCqGSM49BAMCMG8xCzAJBgNVBAYT\r\n" \
+    "AlhYMRAwDgYDVQQIEwdNeVN0YXRlMQ8wDQYDVQQHEwZNeUNpdHkxDzANBgNVBAsT\r\n" \
+    "Bk15VW5pdDERMA8GA1UEChMITXlWZW5kb3IxGTAXBgNVBAMTEHd3dy5teXZlbmRv\r\n" \
+    "ci5jb20wHhcNMjMxMDE2MTAzMzE1WhcNMjYxMDE2MTAzMzE1WjBvMQswCQYDVQQG\r\n" \
+    "EwJYWDEQMA4GA1UECBMHTXlTdGF0ZTEPMA0GA1UEBxMGTXlDaXR5MQ8wDQYDVQQL\r\n" \
+    "EwZNeVVuaXQxETAPBgNVBAoTCE15VmVuZG9yMRkwFwYDVQQDExB3d3cubXl2ZW5k\r\n" \
+    "b3IuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWdyzPAXGKeZY94OhHAWX\r\n" \
+    "HzJfQIjGSyaOzlgL9OEFw2SoUDncLKPGwfPAUSfuMyEkzszNDM0HHkBsDLqu4n25\r\n" \
+    "/6MyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU4EynoSw9eDKZEVPkums2\r\n" \
+    "IWLAJCowCgYIKoZIzj0EAwIDSQAwRgIhAMYGGL9xShyE6P9wEU+MAYF6W3CzdrwV\r\n" \
+    "kuerX1encIH2AiEA5rq490NUobM1Au43roxJq1T6Z43LscPVbGZfULD1Jq0=\r\n"     \
+    "-----END CERTIFICATE-----\r\n"
+
+namespace ot {
+
+class TestBleSecure
+{
+public:
+    TestBleSecure(void)
+        : mIsConnected(false)
+        , mIsBleConnectionOpen(false)
+    {
+    }
+
+    void HandleBleSecureConnect(bool aConnected, bool aBleConnectionOpen)
+    {
+        mIsConnected         = aConnected;
+        mIsBleConnectionOpen = aBleConnectionOpen;
+    }
+
+    bool IsConnected(void) const { return mIsConnected; }
+    bool IsBleConnectionOpen(void) const { return mIsBleConnectionOpen; }
+
+private:
+    bool mIsConnected;
+    bool mIsBleConnectionOpen;
+};
+
+static void HandleBleSecureConnect(otInstance *aInstance, bool aConnected, bool aBleConnectionOpen, void *aContext)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    static_cast<TestBleSecure *>(aContext)->HandleBleSecureConnect(aConnected, aBleConnectionOpen);
+}
+
+void TestTcat(void)
+{
+    const char         kPskdVendor[] = "J01NM3";
+    const char         kUrl[]        = "dummy_url";
+    constexpr uint16_t kConnectionId = 0;
+
+    TestBleSecure ble;
+    Instance     *instance = testInitInstance();
+
+    otTcatVendorInfo vendorInfo = {.mProvisioningUrl = kUrl, .mPskdString = kPskdVendor};
+
+    otBleSecureSetCertificate(instance, reinterpret_cast<const uint8_t *>(OT_TCAT_X509_CERT), sizeof(OT_TCAT_X509_CERT),
+                              reinterpret_cast<const uint8_t *>(OT_TCAT_PRIV_KEY), sizeof(OT_TCAT_PRIV_KEY));
+
+    otBleSecureSetCaCertificateChain(instance, reinterpret_cast<const uint8_t *>(OT_TCAT_TRUSTED_ROOT_CERTIFICATE),
+                                     sizeof(OT_TCAT_TRUSTED_ROOT_CERTIFICATE));
+
+    otBleSecureSetSslAuthMode(instance, true);
+
+    // Validate BLE secure and Tcat start APIs
+    VerifyOrQuit(otBleSecureTcatStart(instance, &vendorInfo, nullptr) == kErrorInvalidState);
+    SuccessOrQuit(otBleSecureStart(instance, HandleBleSecureConnect, nullptr, true, &ble));
+    VerifyOrQuit(otBleSecureStart(instance, HandleBleSecureConnect, nullptr, true, nullptr) == kErrorAlready);
+    SuccessOrQuit(otBleSecureTcatStart(instance, &vendorInfo, nullptr));
+
+    // Validate connection callbacks when platform informs that peer has connected/disconnected
+    otPlatBleGapOnConnected(instance, kConnectionId);
+    VerifyOrQuit(!ble.IsConnected() && ble.IsBleConnectionOpen());
+    otPlatBleGapOnDisconnected(instance, kConnectionId);
+    VerifyOrQuit(!ble.IsConnected() && !ble.IsBleConnectionOpen());
+
+    // Validate connection callbacks when calling `otBleSecureDisconnect()`
+    otPlatBleGapOnConnected(instance, kConnectionId);
+    VerifyOrQuit(!ble.IsConnected() && ble.IsBleConnectionOpen());
+    otBleSecureDisconnect(instance);
+    VerifyOrQuit(!ble.IsConnected() && !ble.IsBleConnectionOpen());
+
+    // Validate TLS connection can be started only when peer is connected
+    otPlatBleGapOnConnected(instance, kConnectionId);
+    SuccessOrQuit(otBleSecureConnect(instance));
+    otBleSecureDisconnect(instance);
+    VerifyOrQuit(otBleSecureConnect(instance) == kErrorInvalidState);
+
+    // Validate Tcat state changes after stopping BLE secure
+    VerifyOrQuit(otBleSecureIsTcatEnabled(instance));
+    otBleSecureStop(instance);
+    VerifyOrQuit(!otBleSecureIsTcatEnabled(instance));
+
+    testFreeInstance(instance);
+}
+
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
+
+int main(void)
+{
+#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
+    ot::TestTcat();
+    printf("All tests passed\n");
+#else
+    printf("Tcat is not enabled\n");
+    return -1;
+#endif
+    return 0;
+}
diff --git a/third_party/mbedtls/mbedtls-config.h b/third_party/mbedtls/mbedtls-config.h
index a3e06ac..29aa49e 100644
--- a/third_party/mbedtls/mbedtls-config.h
+++ b/third_party/mbedtls/mbedtls-config.h
@@ -94,6 +94,7 @@
 
 #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
 #define MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
+#define MBEDTLS_GCM_C
 #endif
 
 #ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
@@ -132,7 +133,9 @@
 #define MBEDTLS_MEMORY_BUFFER_ALLOC_C
 #endif
 
-#if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE
+#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
+#define MBEDTLS_SSL_MAX_CONTENT_LEN      2000 /**< Maxium fragment length in bytes */
+#elif OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE
 #define MBEDTLS_SSL_MAX_CONTENT_LEN      900 /**< Maxium fragment length in bytes */
 #else
 #define MBEDTLS_SSL_MAX_CONTENT_LEN      768 /**< Maxium fragment length in bytes */
diff --git a/tools/tcat_ble_client/bbtc.py b/tools/tcat_ble_client/bbtc.py
old mode 100644
new mode 100755
index fa2ebbb..92539c1
--- a/tools/tcat_ble_client/bbtc.py
+++ b/tools/tcat_ble_client/bbtc.py
@@ -34,6 +34,7 @@
 from ble.ble_connection_constants import BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, \
     BBTC_RX_CHAR_UUID, SERVER_COMMON_NAME
 from ble.ble_stream import BleStream
+from ble.udp_stream import UdpStream
 from ble.ble_stream_secure import BleStreamSecure
 from ble import ble_scanner
 from cli.cli import CLI
@@ -47,10 +48,12 @@
 
     parser = argparse.ArgumentParser(description='Device parameters')
     parser.add_argument('--debug', help='Enable debug logs', action='store_true')
+    parser.add_argument('--cert_path', help='Path to certificate chain and key', action='store', default='auth')
     group = parser.add_mutually_exclusive_group()
     group.add_argument('--mac', type=str, help='Device MAC address', action='store')
     group.add_argument('--name', type=str, help='Device name', action='store')
     group.add_argument('--scan', help='Scan all available devices', action='store_true')
+    group.add_argument('--simulation', help='Connect to simulation node id', action='store')
     args = parser.parse_args()
 
     if args.debug:
@@ -63,12 +66,11 @@
 
     if not (device is None):
         print(f'Connecting to {device}')
-        ble_stream = await BleStream.create(device.address, BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, BBTC_RX_CHAR_UUID)
-        ble_sstream = BleStreamSecure(ble_stream)
+        ble_sstream = BleStreamSecure(device)
         ble_sstream.load_cert(
-            certfile=path.join('auth', 'commissioner_cert.pem'),
-            keyfile=path.join('auth', 'commissioner_key.pem'),
-            cafile=path.join('auth', 'ca_cert.pem'),
+            certfile=path.join(args.cert_path, 'commissioner_cert.pem'),
+            keyfile=path.join(args.cert_path, 'commissioner_key.pem'),
+            cafile=path.join(args.cert_path, 'ca_cert.pem'),
         )
 
         print('Setting up secure channel...')
@@ -96,11 +98,16 @@
     device = None
     if args.mac:
         device = await ble_scanner.find_first_by_mac(args.mac)
+        device = await BleStream.create(device.address, BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, BBTC_RX_CHAR_UUID)
     elif args.name:
         device = await ble_scanner.find_first_by_name(args.name)
+        device = await BleStream.create(device.address, BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, BBTC_RX_CHAR_UUID)
     elif args.scan:
         tcat_devices = await ble_scanner.scan_tcat_devices()
         device = select_device_by_user_input(tcat_devices)
+        device = await BleStream.create(device.address, BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, BBTC_RX_CHAR_UUID)
+    elif args.simulation:
+        device = UdpStream("127.0.0.1", int(args.simulation))
 
     return device
 
diff --git a/tools/tcat_ble_client/ble/ble_stream_secure.py b/tools/tcat_ble_client/ble/ble_stream_secure.py
index 9d15b79..4731c18 100644
--- a/tools/tcat_ble_client/ble/ble_stream_secure.py
+++ b/tools/tcat_ble_client/ble/ble_stream_secure.py
@@ -30,15 +30,13 @@
 import ssl
 import logging
 
-from .ble_stream import BleStream
-
 logger = logging.getLogger(__name__)
 
 
 class BleStreamSecure:
 
-    def __init__(self, ble_stream: BleStream):
-        self.ble_stream = ble_stream
+    def __init__(self, stream):
+        self.stream = stream
         self.ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
         self.incoming = ssl.MemoryBIO()
         self.outgoing = ssl.MemoryBIO()
@@ -67,12 +65,12 @@
             # SSLWantWrite means ssl wants to send data over the link,
             # but might need a receive first
             except ssl.SSLWantWriteError:
-                output = await self.ble_stream.recv(4096)
+                output = await self.stream.recv(4096)
                 if output:
                     self.incoming.write(output)
                 data = self.outgoing.read()
                 if data:
-                    await self.ble_stream.send(data)
+                    await self.stream.send(data)
                 await asyncio.sleep(0.1)
 
             # SSLWantRead means ssl wants to receive data from the link,
@@ -80,8 +78,8 @@
             except ssl.SSLWantReadError:
                 data = self.outgoing.read()
                 if data:
-                    await self.ble_stream.send(data)
-                output = await self.ble_stream.recv(4096)
+                    await self.stream.send(data)
+                output = await self.stream.recv(4096)
                 if output:
                     self.incoming.write(output)
                 await asyncio.sleep(0.1)
@@ -89,14 +87,14 @@
     async def send(self, bytes):
         self.ssl_object.write(bytes)
         encode = self.outgoing.read(4096)
-        await self.ble_stream.send(encode)
+        await self.stream.send(encode)
 
     async def recv(self, buffersize, timeout=1):
         end_time = asyncio.get_event_loop().time() + timeout
-        data = await self.ble_stream.recv(buffersize)
+        data = await self.stream.recv(buffersize)
         while not data and asyncio.get_event_loop().time() < end_time:
             await asyncio.sleep(0.1)
-            data = await self.ble_stream.recv(buffersize)
+            data = await self.stream.recv(buffersize)
         if not data:
             logger.warning('No response when response expected.')
             return b''
@@ -108,10 +106,10 @@
                 break
             # if recv called before entire message was received from the link
             except ssl.SSLWantReadError:
-                more = await self.ble_stream.recv(buffersize)
+                more = await self.stream.recv(buffersize)
                 while not more:
                     await asyncio.sleep(0.1)
-                    more = await self.ble_stream.recv(buffersize)
+                    more = await self.stream.recv(buffersize)
                 self.incoming.write(more)
         return decode
 
diff --git a/tools/tcat_ble_client/ble/udp_stream.py b/tools/tcat_ble_client/ble/udp_stream.py
new file mode 100644
index 0000000..5421940
--- /dev/null
+++ b/tools/tcat_ble_client/ble/udp_stream.py
@@ -0,0 +1,56 @@
+"""
+  Copyright (c) 2024, 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 itertools import count, takewhile
+from typing import Iterator
+import logging
+import time
+from asyncio import sleep
+import socket
+
+logger = logging.getLogger(__name__)
+
+
+class UdpStream:
+    BASE_PORT = 10000
+
+    def __init__(self, address, node_id):
+        self.__receive_buffer = b''
+        self.__last_recv_time = None
+        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        self.address = (address, self.BASE_PORT + node_id)
+
+    async def send(self, data):
+        logger.debug(f'sending {data}')
+        self.socket.sendto(data, self.address)
+        return len(data)
+
+    async def recv(self, bufsize):
+        message = self.socket.recv(bufsize)
+        logger.debug(f'retrieved {message}')
+        return message
diff --git a/tools/tcat_ble_client/tlv/tlv.py b/tools/tcat_ble_client/tlv/tlv.py
new file mode 100644
index 0000000..75657c4
--- /dev/null
+++ b/tools/tcat_ble_client/tlv/tlv.py
@@ -0,0 +1,71 @@
+"""
+  Copyright (c) 2024, 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 __future__ import annotations
+from typing import List
+
+
+class TLV():
+
+    def __init__(self, type: int = None, value: bytes = None):
+        self.type: int = type
+        self.value: bytes = value
+
+    def __str__(self):
+        return f'TLV\n\tTYPE:\t0x{self.type:02x}\n\tVALUE:\t{self.value}'
+
+    @staticmethod
+    def parse_tlvs(data: bytes) -> List[TLV]:
+        res: List[TLV] = []
+        while data:
+            next_tlv = TLV.from_bytes(data)
+            next_tlv_size = len(next_tlv.to_bytes())
+            data = data[next_tlv_size:]
+            res.append(next_tlv)
+        return res
+
+    @staticmethod
+    def from_bytes(data: bytes) -> TLV:
+        res = TLV()
+        res.set_from_bytes(data)
+        return res
+
+    def set_from_bytes(self, data: bytes):
+        self.type = data[0]
+        header_len = 2
+        if data[1] == 0xFF:
+            header_len = 4
+        length = int.from_bytes(data[1:header_len], byteorder='big')
+        self.value = data[header_len:header_len + length]
+
+    def to_bytes(self) -> bytes:
+        has_long_header = len(self.value) >= 255
+        header_len = 4 if has_long_header else 2
+        len_bytes = len(self.value).to_bytes(header_len - 1, byteorder='big')
+        header = bytes([self.type]) + len_bytes
+        return header + self.value
diff --git a/tools/tcat_ble_client/utils/__init__.py b/tools/tcat_ble_client/utils/__init__.py
new file mode 100644
index 0000000..d6d40ba
--- /dev/null
+++ b/tools/tcat_ble_client/utils/__init__.py
@@ -0,0 +1,63 @@
+"""
+  Copyright (c) 2024, 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.
+"""
+
+
+def get_int_in_range(min_value, max_value):
+    while True:
+        try:
+            user_input = int(input('> '))
+            if min_value <= user_input <= max_value:
+                return user_input
+            else:
+                print('The value is out of range. Try again.')
+        except ValueError:
+            print('The value is not an integer. Try again.')
+        except KeyboardInterrupt:
+            quit_with_reason('Program interrupted by user. Quitting.')
+
+
+def quit_with_reason(reason):
+    print(reason)
+    exit(1)
+
+
+def select_device_by_user_input(tcat_devices):
+    if tcat_devices:
+        print('Found devices:\n')
+        for i, device in enumerate(tcat_devices):
+            print(f'{i + 1}: {device.name} - {device.address}')
+    else:
+        print('\nNo devices found.')
+        return None
+
+    print('\nSelect the target number to connect to it.')
+    selected = get_int_in_range(1, len(tcat_devices))
+    device = tcat_devices[selected - 1]
+    print('Selected ', device)
+
+    return device
diff --git a/zephyr/module.yml b/zephyr/module.yml
index 75f79ec..50c6605 100644
--- a/zephyr/module.yml
+++ b/zephyr/module.yml
@@ -26,6 +26,7 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
+name: openthread
 build:
   cmake-ext: True
   kconfig-ext: True