[Github Sync] Merge github/main to aosp/main

Bug: 333301206

* github/main:
  [mle] MTD to handle udp time sync message (#9978)
  [plat-utils] enhance `otMacFrameDoesAddrMatch()` (#9997)
  [netdata-publisher] distinguish SRP/DNS unicast entries (#9937)
  [logging] introduce `LogWarnOnError()` for standardized error logging (#9996)
  [test] add tests for multi-BR network resilience during BR removal (#9990)
  [srp-sever] add `LogError()` (#9992)
  [dataset] define `Dataset::Tlvs` type (#9991)
  [ip6] allow delivering RLOC/ALOC traffic to host (#9987)
  [mle] simplify `CheckReachablity()` (#9989)
  [srp-server] retry other ports when failing to `prepareSocket` (#9981)
  [link-metrics] fix race condition (#9986)
  [doc] use space char instead of tabs in `ot_config_doc.h` (#9983)
  github-actions: bump actions/checkout from 4.1.1 to 4.1.2 (#9982)
  [mesh-forwarder] update `CheckReachablity()` to handle malformed messages (#9976)
  [netata] simplify `RemoveTemporaryData()` (#9975)
  [cli] `netdata show` to support filtering by RLOC16 (#9969)
  [mle] improve MLE module comments (#9972)
  [cli] fix CoAP response code for POST 2.03 -> 2.04 per RFC 7252 (#9971)
  [mle] include child RLOC16 in the logs from `HandleTimeTick()` (#9968)
  [netdata] refactor `MutableNetworkData` methods for code organization (#9970)
  [routing-manager] set extra options to append to emitted RAs (#9945)
  [test] add `tcat` unit tests (#9953)
  [cli] add `OutputNetworkData()` (#9965)
  [routing-manager] employ RA hash tracking to detect self-originating RAs (#9939)
  [channel-manager] add local csl channel selection on SSED (#9641)
  [netdata] add `FindRlocs()` to retrieve RLOC16 of entries (#9961)
  [version] introduce `OT_THREAD_VERSION_1_4` (#9946)
  github-actions: bump actions/download-artifact from 4.1.1 to 4.1.4 (#9963)
  [config] update docs for `SED_BUFFER_SIZE` & `SED_DATAGRAM_COUNT` (#9962)
  [spinel] extract log module from radio spinel (#9957)
  [mle] simplify `HandleTimeTick()` code related to aging routers (#9955)
  [mle] retain direct child cache entries on Addr Solicit Response TX (#9956)
  [test] simplify `test_pskc` unit test (#9952)
  [cli] move `Process{Get/Set/Enable/Disable}` methods to Utils class (#9951)
  [simulation] add simulation tests framework for tcat (#9724)
  [coap] fix copying option (#9894)
  github-actions: bump docker/setup-buildx-action from 3.0.0 to 3.2.0 (#9941)
  [routing-manager] simplify `Ip6::Nd` type usage (#9938)
  [thread-cert] use `unittest` assert methods (#9936)
  [lib] include `(void)` in function prototype in `reset_util.h` (#9934)
  [routing-manager] update use of `Ip6::Nd::Option` enumerator (#9931)
  [posix] remove use of core-internal types in `netif.cpp` platform (#9928)
  [test] enhance `test_netdata_publisher` robustness (#9932)
  [thread-cert] use `ROUTER_STARTUP_DELAY` in `test_detach` (#9933)
  [mle] disable MLE retransmissions when detaching (#9929)
  [simulation] allow specify local host (#9925)
  [routing-manager] construct RA dynamically using `Heap::Array` (#9924)
  [posix] ensure module names are included in platform logs (#9920)
  [tests] fix `test_manual_maddress.py` (#9923)
  [thread-cert] set device mode in `test_mle_msg_key_seq_jump` (#9921)
  [posix] add IPv6 address helper functions in `Ip6Utils` (#9917)
  [daemon] always initialize the OT CLI when setting up the daemon (#9919)
  [misc] remove stale forward declaration (#9911)
  github-actions: bump actions/upload-artifact from 4.2.0 to 4.3.1 (#9914)
  [docs] add documentation for CLI `dataset updater` command (#9905)
  [test] add `test_key_rotation_and_key_guard_time` (#9906)
  [srp-server] add snoop cache entries for registered host addresses (#9881)
  [key-manager] update how key guard time is determined and applied (#9871)
  [toranj-config] disable `OT_BORDER_ROUTING` on NCP builds (#9907)
  [simulation] implement `otPlatInfraIf` APIs in simulation platform (#9895)
  [posix] add otSysCliInitUsingDaemon API (#9897)
  [routing-manager] delay sending RAs until after initial policy evaluation (#9896)
  [posix] add missing header guards, style fixes (#9892)
  [border-agent] mechanism to use ephemeral key (#9435)
  [posix] enable building `infra_if.cpp` on macOS (#9891)
  [string] return `0` if `nullptr` is passed to `StringLength` (#9886)
  [posix] fix member var access from static `CreateIcmp6Socket()` (#9887)
  github-actions: bump github/codeql-action from 3.23.2 to 3.24.6 (#9890)
  github-actions: bump codecov/codecov-action from 3.1.4 to 4.0.2 (#9877)
  [cli] add documentation for `nexthop` command in `README.md` (#9882)
  [simulation] add `simul_utils.h` for socket operation helpers (#9879)
  [message] track received `ThreadLinkInfo` in `Message` metadata (#9878)
  [spinel] drop received frames between RCP disable and reset (#9793)
  [link-quality] prevent overflow in `LqiAverager` calculation (#9876)
  [cli] add `@moreinfo` Doxygen tags to reference CoAPS Concepts Guide (#9867)
  [core] use `NumericLimits<>` constants (#9875)

Change-Id: I65a1c577c22335916c3ba68f164a76b6d1409831
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..9738b85 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: |
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..7abbc91 100644
--- a/.github/workflows/otns.yml
+++ b/.github/workflows/otns.yml
@@ -62,7 +62,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: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
       with:
         go-version: "1.20"
@@ -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,7 +102,7 @@
     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"
@@ -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,7 +164,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: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
         with:
           go-version: "1.20"
@@ -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..26153f4 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@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+      if: ${{ failure() }}
+      with:
+        name: channel-manager-csl
+        path: ot_testing
+    - name: Generate Coverage
+      run: |
+        ./script/test generate_coverage gcc
+    - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+      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_config_doc.h b/doc/ot_config_doc.h
index 564950b..87af229 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
@@ -83,7 +83,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..da77ef0 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -180,6 +180,7 @@
 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)")
@@ -278,7 +279,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 +287,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..e01bc9c 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
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/examples/platforms/simulation/CMakeLists.txt b/examples/platforms/simulation/CMakeLists.txt
index 41a1dbf..12580e2 100644
--- a/examples/platforms/simulation/CMakeLists.txt
+++ b/examples/platforms/simulation/CMakeLists.txt
@@ -72,6 +72,7 @@
     misc.c
     multipan.c
     radio.c
+    simul_utils.c
     spi-stubs.c
     system.c
     trel.c
diff --git a/examples/platforms/simulation/ble.c b/examples/platforms/simulation/ble.c
index 2fe3c64..d48922a 100644
--- a/examples/platforms/simulation/ble.c
+++ b/examples/platforms/simulation/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/examples/platforms/simulation/infra_if.c b/examples/platforms/simulation/infra_if.c
index 9596406..ccb5e9b 100644
--- a/examples/platforms/simulation/infra_if.c
+++ b/examples/platforms/simulation/infra_if.c
@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2021, The OpenThread Authors.
+ *  Copyright (c) 2024, The OpenThread Authors.
  *  All rights reserved.
  *
  *  Redistribution and use in source and binary forms, with or without
@@ -28,15 +28,154 @@
 
 #include "platform-simulation.h"
 
+#include <openthread/icmp6.h>
+#include <openthread/ip6.h>
+#include <openthread/logging.h>
 #include <openthread/platform/infra_if.h>
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#include "simul_utils.h"
+#include "utils/code_utils.h"
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && !OPENTHREAD_RADIO
+
+#define DEBUG_LOG 0
+
+#if DEBUG_LOG
+#define LOG(...) otLogNotePlat("[infra-if] "__VA_ARGS__)
+#else
+#define LOG(...) \
+    do           \
+    {            \
+    } while (0)
+#endif
+
+#define INFRA_IF_SIM_PORT 9800
+#define INFRA_IF_MAX_PACKET_SIZE 1800
+#define INFRA_IF_MAX_PENDING_TX 64
+#define INFRA_IF_NEIGHBOR_ADVERT_SIZE 24
+
+typedef struct Message
+{
+    uint32_t     mIfIndex;
+    otIp6Address mSrc;
+    otIp6Address mDst;
+    uint16_t     mDataLength;
+    uint8_t      mData[INFRA_IF_MAX_PACKET_SIZE];
+} Message;
+
+static bool         sInitialized = false;
+static otIp6Address sIp6Address;
+static otIp6Address sLinkLocalAllNodes;
+static otIp6Address sLinkLocalAllRouters;
+static utilsSocket  sSocket;
+static uint16_t     sPortOffset   = 0;
+static uint8_t      sNumPendingTx = 0;
+static Message      sPendingTx[INFRA_IF_MAX_PENDING_TX];
+
+//---------------------------------------------------------------------------------------------------------------------
+
+static bool addressesMatch(const otIp6Address *aFirstAddr, const otIp6Address *aSecondAddr)
+{
+    return memcmp(aFirstAddr, aSecondAddr, sizeof(otIp6Address)) == 0;
+}
+
+static uint16_t getMessageSize(const Message *aMessage)
+{
+    return (uint16_t)(&aMessage->mData[aMessage->mDataLength] - (const uint8_t *)aMessage);
+}
+
+static void sendPendingTxMessages(void)
+{
+    for (uint8_t i = 0; i < sNumPendingTx; i++)
+    {
+        utilsSendOverSocket(&sSocket, &sPendingTx[i], getMessageSize(&sPendingTx[i]));
+    }
+
+    sNumPendingTx = 0;
+}
+
+static void sendNeighborAdvert(const Message *aNsMessage)
+{
+    Message *message;
+    uint8_t  index;
+
+    assert(sNumPendingTx < INFRA_IF_MAX_PENDING_TX);
+
+    message = &sPendingTx[sNumPendingTx++];
+
+    message->mIfIndex = aNsMessage->mIfIndex;
+    message->mSrc     = sIp6Address;
+    message->mDst     = aNsMessage->mSrc;
+
+    // Neighbor Advertisement Message (RFC 4861)
+    //
+    //   0                   1                   2                   3
+    //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |     Type      |     Code      |          Checksum             |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |R|S|O|                     Reserved                            |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |                                                               |
+    //  +                                                               +
+    //  |                                                               |
+    //  +                       Target Address                          +
+    //  |                                                               |
+    //  +                                                               +
+    //  |                                                               |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+    index = 0;
+    memset(message->mData, 0, INFRA_IF_NEIGHBOR_ADVERT_SIZE);
+
+    message->mData[index++] = OT_ICMP6_TYPE_NEIGHBOR_ADVERT;           // Type.
+    index += 3;                                                        // Code is zero. Checksum (uint16) as zero.
+    message->mData[index++] = 0xd0;                                    // Flags, set R and S bits.
+    index += 3;                                                        // Skip over the reserved bytes.
+    memcpy(&message->mData[index], &sIp6Address, sizeof(sIp6Address)); // Set the target address field.
+    index += sizeof(sIp6Address);
+
+    assert(index == INFRA_IF_NEIGHBOR_ADVERT_SIZE);
+
+    message->mDataLength = INFRA_IF_NEIGHBOR_ADVERT_SIZE;
+}
+
+static void processMessage(otInstance *aInstance, Message *aMessage, uint16_t aLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    otEXPECT(aLength > 0);
+    otEXPECT(getMessageSize(aMessage) == aLength);
+    otEXPECT(aMessage->mDataLength > 0);
+
+    // Validate the dest address.
+    otEXPECT(addressesMatch(&aMessage->mDst, &sIp6Address) || addressesMatch(&aMessage->mDst, &sLinkLocalAllNodes) ||
+             addressesMatch(&aMessage->mDst, &sLinkLocalAllRouters));
+
+    if (aMessage->mData[0] == OT_ICMP6_TYPE_NEIGHBOR_SOLICIT)
+    {
+        LOG("Received NS, responding with NA");
+        sendNeighborAdvert(aMessage);
+    }
+    else
+    {
+        LOG("Received msg, len:%u", aMessage->mDataLength);
+        otPlatInfraIfRecvIcmp6Nd(aInstance, aMessage->mIfIndex, &aMessage->mSrc, aMessage->mData,
+                                 aMessage->mDataLength);
+    }
+
+exit:
+    return;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// otPlatInfraIf
+
 bool otPlatInfraIfHasAddress(uint32_t aInfraIfIndex, const otIp6Address *aAddress)
 {
     OT_UNUSED_VARIABLE(aInfraIfIndex);
-    OT_UNUSED_VARIABLE(aAddress);
 
-    return false;
+    return addressesMatch(aAddress, &sIp6Address);
 }
 
 otError otPlatInfraIfSendIcmp6Nd(uint32_t            aInfraIfIndex,
@@ -44,12 +183,27 @@
                                  const uint8_t      *aBuffer,
                                  uint16_t            aBufferLength)
 {
-    OT_UNUSED_VARIABLE(aInfraIfIndex);
-    OT_UNUSED_VARIABLE(aDestAddress);
-    OT_UNUSED_VARIABLE(aBuffer);
-    OT_UNUSED_VARIABLE(aBufferLength);
+    otError  error = OT_ERROR_FAILED;
+    Message *message;
 
-    return OT_ERROR_NONE;
+    otEXPECT(sInitialized);
+    otEXPECT(sNumPendingTx < INFRA_IF_MAX_PENDING_TX);
+
+    message = &sPendingTx[sNumPendingTx++];
+
+    message->mIfIndex = aInfraIfIndex;
+    message->mSrc     = sIp6Address;
+    message->mDst     = *aDestAddress;
+
+    assert(aBufferLength <= INFRA_IF_MAX_PACKET_SIZE);
+    message->mDataLength = aBufferLength;
+    memcpy(message->mData, aBuffer, aBufferLength);
+    error = OT_ERROR_NONE;
+
+    LOG("otPlatInfraIfSendIcmp6Nd() msg-len:%u", aBufferLength);
+
+exit:
+    return error;
 }
 
 otError otPlatInfraIfDiscoverNat64Prefix(uint32_t aInfraIfIndex)
@@ -58,4 +212,127 @@
 
     return OT_ERROR_NONE;
 }
-#endif
+
+//---------------------------------------------------------------------------------------------------------------------
+// platformInfraIf
+
+void platformInfraIfInit(void)
+{
+    char *str;
+
+    otEXPECT(!sInitialized);
+
+    sInitialized = true;
+
+    memset(&sIp6Address, 0, sizeof(sIp6Address));
+    sIp6Address.mFields.m8[0]  = 0xfe;
+    sIp6Address.mFields.m8[1]  = 0x80;
+    sIp6Address.mFields.m8[15] = (uint8_t)(gNodeId & 0xff);
+
+    // "ff02::01"
+    memset(&sLinkLocalAllNodes, 0, sizeof(sLinkLocalAllNodes));
+    sLinkLocalAllNodes.mFields.m8[0]  = 0xff;
+    sLinkLocalAllNodes.mFields.m8[1]  = 0x02;
+    sLinkLocalAllNodes.mFields.m8[15] = 0x01;
+
+    // "ff02::02"
+    memset(&sLinkLocalAllRouters, 0, sizeof(sLinkLocalAllRouters));
+    sLinkLocalAllRouters.mFields.m8[0]  = 0xff;
+    sLinkLocalAllRouters.mFields.m8[1]  = 0x02;
+    sLinkLocalAllRouters.mFields.m8[15] = 0x02;
+
+    str = getenv("PORT_OFFSET");
+
+    if (str != NULL)
+    {
+        char *endptr;
+
+        sPortOffset = (uint16_t)strtol(str, &endptr, 0);
+
+        if (*endptr != '\0')
+        {
+            fprintf(stderr, "\r\nInvalid PORT_OFFSET: %s\r\n", str);
+            exit(EXIT_FAILURE);
+        }
+
+        sPortOffset *= (MAX_NETWORK_SIZE + 1);
+    }
+
+    utilsInitSocket(&sSocket, INFRA_IF_SIM_PORT + sPortOffset);
+
+exit:
+    return;
+}
+
+void platformInfraIfDeinit(void)
+{
+    otEXPECT(sInitialized);
+    sInitialized = false;
+    utilsDeinitSocket(&sSocket);
+
+exit:
+    return;
+}
+
+void platformInfraIfUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, int *aMaxFd)
+{
+    otEXPECT(sInitialized);
+
+    utilsAddSocketRxFd(&sSocket, aReadFdSet, aMaxFd);
+
+    if (sNumPendingTx > 0)
+    {
+        utilsAddSocketTxFd(&sSocket, aWriteFdSet, aMaxFd);
+    }
+
+exit:
+    return;
+}
+
+void platformInfraIfProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    otEXPECT(sInitialized);
+
+    if ((sNumPendingTx > 0) && utilsCanSocketSend(&sSocket, aWriteFdSet))
+    {
+        sendPendingTxMessages();
+    }
+
+    if (utilsCanSocketReceive(&sSocket, aReadFdSet))
+    {
+        Message  message;
+        uint16_t len;
+
+        message.mDataLength = 0;
+
+        len = utilsReceiveFromSocket(&sSocket, &message, sizeof(message), NULL);
+        processMessage(aInstance, &message, len);
+    }
+
+exit:
+    return;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// Provide weak implementation (used for RCP builds).
+// `OPENTHREAD_RADIO` is not available in simulation platform
+
+OT_TOOL_WEAK void otPlatInfraIfRecvIcmp6Nd(otInstance         *aInstance,
+                                           uint32_t            aInfraIfIndex,
+                                           const otIp6Address *aSrcAddress,
+                                           const uint8_t      *aBuffer,
+                                           uint16_t            aBufferLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+    OT_UNUSED_VARIABLE(aSrcAddress);
+    OT_UNUSED_VARIABLE(aBuffer);
+    OT_UNUSED_VARIABLE(aBufferLength);
+
+    fprintf(stderr, "\n\r Weak otPlatInfraIfRecvIcmp6Nd is being used\n\r");
+    exit(1);
+}
+
+#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
diff --git a/examples/platforms/simulation/platform-simulation.h b/examples/platforms/simulation/platform-simulation.h
index 0592b14..d440768 100644
--- a/examples/platforms/simulation/platform-simulation.h
+++ b/examples/platforms/simulation/platform-simulation.h
@@ -159,7 +159,7 @@
 void platformRadioReceive(otInstance *aInstance, uint8_t *aBuf, uint16_t aBufLength);
 
 /**
- * Updates the file descriptor sets with file descriptors used by the radio driver.
+ * Updates the file descriptor sets with file descriptors used by the BLE radio driver.
  *
  * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
  * @param[in,out]  aWriteFdSet  A pointer to the write file descriptors.
@@ -305,4 +305,67 @@
 
 #endif // OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
+/**
+ * Initializes the platform infra-if module.
+ *
+ */
+void platformInfraIfInit(void);
+
+/**
+ * Shuts down the platform infra-if module.
+ *
+ */
+void platformInfraIfDeinit(void);
+
+/**
+ * Updates the file descriptor sets with file descriptors used by the infra-if module
+ *
+ * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
+ * @param[in,out]  aWriteFdSet  A pointer to the write file descriptors.
+ * @param[in,out]  aMaxFd       A pointer to the max file descriptor.
+ *
+ */
+void platformInfraIfUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, int *aMaxFd);
+
+/**
+ * Performs infra-if module processing.
+ *
+ * @param[in]  aInstance    The OpenThread instance structure.
+ * @param[in]  aReadFdSet   A pointer to the read file descriptors.
+ * @param[in]  aWriteFdSet  A pointer to the write file descriptors.
+ *
+ */
+void platformInfraIfProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet);
+
+#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
+/**
+ * Shuts down the BLE service used by OpenThread.
+ *
+ */
+void platformBleDeinit(void);
+
+/**
+ * Updates the file descriptor sets with file descriptors used by the radio driver.
+ *
+ * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
+ * @param[in,out]  aWriteFdSet  A pointer to the write file descriptors.
+ * @param[in,out]  aTimeout     A pointer to the timeout.
+ * @param[in,out]  aMaxFd       A pointer to the max file descriptor.
+ *
+ */
+void platformBleUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, struct timeval *aTimeout, int *aMaxFd);
+
+/**
+ * Performs BLE driver processing.
+ *
+ * @param[in]  aInstance    The OpenThread instance structure.
+ * @param[in]  aReadFdSet   A pointer to the read file descriptors.
+ * @param[in]  aWriteFdSet  A pointer to the write file descriptors.
+ *
+ */
+void platformBleProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet);
+
 #endif // PLATFORM_SIMULATION_H_
diff --git a/examples/platforms/simulation/radio.c b/examples/platforms/simulation/radio.c
index 03808b5..960d8c6 100644
--- a/examples/platforms/simulation/radio.c
+++ b/examples/platforms/simulation/radio.c
@@ -41,14 +41,12 @@
 #include <openthread/platform/radio.h>
 #include <openthread/platform/time.h>
 
+#include "simul_utils.h"
 #include "utils/code_utils.h"
 #include "utils/link_metrics.h"
 #include "utils/mac_frame.h"
 #include "utils/soft_source_match_table.h"
 
-// The IPv4 group for receiving packets of radio simulation
-#define OT_RADIO_GROUP "224.0.0.116"
-
 #define MS_PER_S 1000
 #define US_PER_MS 1000
 
@@ -75,11 +73,9 @@
 extern uint16_t sPortBase;
 extern uint16_t sPortOffset;
 #else
-static int      sTxFd       = -1;
-static int      sRxFd       = -1;
-static uint16_t sPortBase   = 9000;
-static uint16_t sPortOffset = 0;
-static uint16_t sPort       = 0;
+static utilsSocket sSocket;
+static uint16_t    sPortBase   = 9000;
+static uint16_t    sPortOffset = 0;
 #endif
 
 static int8_t   sEnergyScanResult  = OT_RADIO_RSSI_INVALID;
@@ -190,6 +186,8 @@
 {
     bool isConnectable = true;
 
+    otEXPECT_ACTION(aNodeId != gNodeId, isConnectable = false);
+
     switch (sFilterMode)
     {
     case kFilterOff:
@@ -202,6 +200,7 @@
         break;
     }
 
+exit:
     return isConnectable;
 }
 
@@ -407,82 +406,15 @@
     sPromiscuous = aEnable;
 }
 
-#if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
-static void initFds(void)
-{
-    int                fd;
-    int                one = 1;
-    struct sockaddr_in sockaddr;
-
-    memset(&sockaddr, 0, sizeof(sockaddr));
-
-    otEXPECT_ACTION((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1, perror("socket(sTxFd)"));
-
-    sPort                    = (uint16_t)(sPortBase + sPortOffset + gNodeId);
-    sockaddr.sin_family      = AF_INET;
-    sockaddr.sin_port        = htons(sPort);
-    sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
-
-    otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &sockaddr.sin_addr, sizeof(sockaddr.sin_addr)) != -1,
-                    perror("setsockopt(sTxFd, IP_MULTICAST_IF)"));
-
-    otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &one, sizeof(one)) != -1,
-                    perror("setsockopt(sRxFd, IP_MULTICAST_LOOP)"));
-
-    otEXPECT_ACTION(bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != -1, perror("bind(sTxFd)"));
-
-    // Tx fd is successfully initialized.
-    sTxFd = fd;
-
-    otEXPECT_ACTION((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1, perror("socket(sRxFd)"));
-
-    otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) != -1,
-                    perror("setsockopt(sRxFd, SO_REUSEADDR)"));
-    otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) != -1,
-                    perror("setsockopt(sRxFd, SO_REUSEPORT)"));
-
-    {
-        struct ip_mreqn mreq;
-
-        memset(&mreq, 0, sizeof(mreq));
-        inet_pton(AF_INET, OT_RADIO_GROUP, &mreq.imr_multiaddr);
-
-        // Always use loopback device to send simulation packets.
-        mreq.imr_address.s_addr = inet_addr("127.0.0.1");
-
-        otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &mreq.imr_address, sizeof(mreq.imr_address)) != -1,
-                        perror("setsockopt(sRxFd, IP_MULTICAST_IF)"));
-        otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) != -1,
-                        perror("setsockopt(sRxFd, IP_ADD_MEMBERSHIP)"));
-    }
-
-    sockaddr.sin_family      = AF_INET;
-    sockaddr.sin_port        = htons((uint16_t)(sPortBase + sPortOffset));
-    sockaddr.sin_addr.s_addr = inet_addr(OT_RADIO_GROUP);
-
-    otEXPECT_ACTION(bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != -1, perror("bind(sRxFd)"));
-
-    // Rx fd is successfully initialized.
-    sRxFd = fd;
-
-exit:
-    if (sRxFd == -1 || sTxFd == -1)
-    {
-        exit(EXIT_FAILURE);
-    }
-}
-#endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
-
 void platformRadioInit(void)
 {
-#if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
+#if !OPENTHREAD_SIMULATION_VIRTUAL_TIME
     parseFromEnvAsUint16("PORT_BASE", &sPortBase);
-
     parseFromEnvAsUint16("PORT_OFFSET", &sPortOffset);
     sPortOffset *= (MAX_NETWORK_SIZE + 1);
 
-    initFds();
-#endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
+    utilsInitSocket(&sSocket, sPortBase + sPortOffset);
+#endif
 
     sReceiveFrame.mPsdu  = sReceiveMessage.mPsdu;
     sTransmitFrame.mPsdu = sTransmitMessage.mPsdu;
@@ -859,24 +791,14 @@
 #else
 void platformRadioUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, struct timeval *aTimeout, int *aMaxFd)
 {
-    if (aReadFdSet != NULL && (sState != OT_RADIO_STATE_TRANSMIT || sTxWait))
+    if (sState != OT_RADIO_STATE_TRANSMIT || sTxWait)
     {
-        FD_SET(sRxFd, aReadFdSet);
-
-        if (aMaxFd != NULL && *aMaxFd < sRxFd)
-        {
-            *aMaxFd = sRxFd;
-        }
+        utilsAddSocketRxFd(&sSocket, aReadFdSet, aMaxFd);
     }
 
-    if (aWriteFdSet != NULL && platformRadioIsTransmitPending())
+    if (platformRadioIsTransmitPending())
     {
-        FD_SET(sTxFd, aWriteFdSet);
-
-        if (aMaxFd != NULL && *aMaxFd < sTxFd)
-        {
-            *aMaxFd = sTxFd;
-        }
+        utilsAddSocketTxFd(&sSocket, aWriteFdSet, aMaxFd);
     }
 
     if (sEnergyScanning)
@@ -900,18 +822,7 @@
 }
 
 // no need to close in virtual time mode.
-void platformRadioDeinit(void)
-{
-    if (sRxFd != -1)
-    {
-        close(sRxFd);
-    }
-
-    if (sTxFd != -1)
-    {
-        close(sTxFd);
-    }
-}
+void platformRadioDeinit(void) { utilsDeinitSocket(&sSocket); }
 #endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME
 
 void platformRadioProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
@@ -919,41 +830,22 @@
     OT_UNUSED_VARIABLE(aReadFdSet);
     OT_UNUSED_VARIABLE(aWriteFdSet);
 
-#if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
-    if (FD_ISSET(sRxFd, aReadFdSet))
+#if !OPENTHREAD_SIMULATION_VIRTUAL_TIME
+    if (utilsCanSocketReceive(&sSocket, aReadFdSet))
     {
-        struct sockaddr_in sockaddr;
-        socklen_t          len = sizeof(sockaddr);
-        ssize_t            rval;
+        uint16_t senderNodeId;
+        uint16_t len;
 
-        memset(&sockaddr, 0, sizeof(sockaddr));
-        rval =
-            recvfrom(sRxFd, (char *)&sReceiveMessage, sizeof(sReceiveMessage), 0, (struct sockaddr *)&sockaddr, &len);
+        len = utilsReceiveFromSocket(&sSocket, &sReceiveMessage, sizeof(sReceiveMessage), &senderNodeId);
 
-        if (rval > 0)
+        if (NodeIdFilterIsConnectable(senderNodeId))
         {
-            uint16_t srcPort   = ntohs(sockaddr.sin_port);
-            uint16_t srcNodeId = srcPort - sPortOffset - sPortBase;
-
-            if (NodeIdFilterIsConnectable(srcNodeId) && srcPort != sPort)
-            {
-                sReceiveFrame.mLength = (uint16_t)(rval - 1);
-
-                radioReceive(aInstance);
-            }
-        }
-        else if (rval == 0)
-        {
-            // socket is closed, which should not happen
-            assert(false);
-        }
-        else if (errno != EINTR && errno != EAGAIN)
-        {
-            perror("recvfrom(sRxFd)");
-            exit(EXIT_FAILURE);
+            sReceiveFrame.mLength = len - 1;
+            radioReceive(aInstance);
         }
     }
-#endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
+#endif
+
     if (platformRadioIsTransmitPending())
     {
         radioSendMessage(aInstance);
@@ -968,33 +860,17 @@
 
 void radioTransmit(struct RadioMessage *aMessage, const struct otRadioFrame *aFrame)
 {
-#if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
-    ssize_t            rval;
-    struct sockaddr_in sockaddr;
-
-    memset(&sockaddr, 0, sizeof(sockaddr));
-    sockaddr.sin_family = AF_INET;
-    inet_pton(AF_INET, OT_RADIO_GROUP, &sockaddr.sin_addr);
-
-    sockaddr.sin_port = htons((uint16_t)(sPortBase + sPortOffset));
-    rval =
-        sendto(sTxFd, (const char *)aMessage, 1 + aFrame->mLength, 0, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
-
-    if (rval < 0)
-    {
-        perror("sendto(sTxFd)");
-        exit(EXIT_FAILURE);
-    }
-#else  // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
+#if !OPENTHREAD_SIMULATION_VIRTUAL_TIME
+    utilsSendOverSocket(&sSocket, aMessage, aFrame->mLength + 1); // + 1 is for `mChannel`
+#else
     struct Event event;
 
     event.mDelay      = 1; // 1us for now
     event.mEvent      = OT_SIM_EVENT_RADIO_RECEIVED;
     event.mDataLength = 1 + aFrame->mLength; // include channel in first byte
     memcpy(event.mData, aMessage, event.mDataLength);
-
     otSimSendEvent(&event);
-#endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
+#endif
 }
 
 void radioSendAck(void)
diff --git a/examples/platforms/simulation/simul_utils.c b/examples/platforms/simulation/simul_utils.c
new file mode 100644
index 0000000..36b69af
--- /dev/null
+++ b/examples/platforms/simulation/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/examples/platforms/simulation/simul_utils.h b/examples/platforms/simulation/simul_utils.h
new file mode 100644
index 0000000..0d70f2d
--- /dev/null
+++ b/examples/platforms/simulation/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/examples/platforms/simulation/system.c b/examples/platforms/simulation/system.c
index 535cb1c..2abe892 100644
--- a/examples/platforms/simulation/system.c
+++ b/examples/platforms/simulation/system.c
@@ -36,19 +36,27 @@
 
 #if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
 
+#include <arpa/inet.h>
 #include <assert.h>
 #include <errno.h>
 #include <getopt.h>
+#include <ifaddrs.h>
 #include <libgen.h>
+#include <netinet/in.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
 
 #include <openthread/tasklet.h>
 #include <openthread/platform/alarm-milli.h>
 #include <openthread/platform/radio.h>
 
+#include "simul_utils.h"
+#include "utils/code_utils.h"
+
 uint32_t gNodeId = 1;
 
 extern bool        gPlatformPseudoResetWasRequested;
@@ -71,6 +79,7 @@
 {
     OT_SIM_OPT_HELP               = 'h',
     OT_SIM_OPT_ENABLE_ENERGY_SCAN = 'E',
+    OT_SIM_OPT_LOCAL_HOST         = 'L',
     OT_SIM_OPT_SLEEP_TO_TX        = 't',
     OT_SIM_OPT_TIME_SPEED         = 's',
     OT_SIM_OPT_LOG_FILE           = 'l',
@@ -96,6 +105,56 @@
     exit(aExitCode);
 }
 
+static const char *GetLocalHostAddress(const char *aLocalHost)
+{
+    struct ifaddrs *ifaddr;
+    static char     ipstr[INET_ADDRSTRLEN] = {0};
+    const char     *rval                   = NULL;
+
+    {
+        struct in_addr addr;
+
+        otEXPECT_ACTION(inet_aton(aLocalHost, &addr) == 0, rval = aLocalHost);
+    }
+
+    if (getifaddrs(&ifaddr) == -1)
+    {
+        perror("getifaddrs");
+        exit(EXIT_FAILURE);
+    }
+
+    for (struct ifaddrs *ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next)
+    {
+        if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET)
+        {
+            continue;
+        }
+
+        if (strcmp(ifa->ifa_name, aLocalHost) == 0)
+        {
+            struct sockaddr_in *addr = (struct sockaddr_in *)ifa->ifa_addr;
+
+            if (inet_ntop(AF_INET, &addr->sin_addr, ipstr, sizeof(ipstr)))
+            {
+                break;
+            }
+        }
+    }
+
+    freeifaddrs(ifaddr);
+
+    if (ipstr[0] == '\0')
+    {
+        fprintf(stderr, "Local host address not found!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    rval = ipstr;
+
+exit:
+    return rval;
+}
+
 void otSysInit(int aArgCount, char *aArgVector[])
 {
     char    *endptr;
@@ -106,6 +165,7 @@
         {"enable-energy-scan", no_argument, 0, OT_SIM_OPT_ENABLE_ENERGY_SCAN},
         {"sleep-to-tx", no_argument, 0, OT_SIM_OPT_SLEEP_TO_TX},
         {"time-speed", required_argument, 0, OT_SIM_OPT_TIME_SPEED},
+        {"local-host", required_argument, 0, OT_SIM_OPT_LOCAL_HOST},
 #if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
         {"log-file", required_argument, 0, OT_SIM_OPT_LOG_FILE},
 #endif
@@ -113,9 +173,9 @@
     };
 
 #if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
-    static const char options[] = "Ehts:l:";
+    static const char options[] = "Ehts:L:l:";
 #else
-    static const char options[] = "Ehts:";
+    static const char options[] = "Ehts:L:";
 #endif
 
     if (gPlatformPseudoResetWasRequested)
@@ -149,6 +209,10 @@
         case OT_SIM_OPT_SLEEP_TO_TX:
             gRadioCaps |= OT_RADIO_CAPS_SLEEP_TO_TX;
             break;
+        case OT_SIM_OPT_LOCAL_HOST:
+            gLocalHost = GetLocalHostAddress(optarg);
+            fprintf(stderr, "Simulate on %s\n", gLocalHost);
+            break;
         case OT_SIM_OPT_TIME_SPEED:
             speedUpFactor = (uint32_t)strtol(optarg, &endptr, 10);
             if (*endptr != '\0' || speedUpFactor == 0)
@@ -189,6 +253,9 @@
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
     platformTrelInit(speedUpFactor);
 #endif
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    platformInfraIfInit();
+#endif
     platformRandomInit();
 }
 
@@ -200,6 +267,9 @@
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
     platformTrelDeinit();
 #endif
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    //    platformInfrIfDeinit();
+#endif
     platformLoggingDeinit();
 }
 
@@ -222,6 +292,13 @@
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
     platformTrelUpdateFdSet(&read_fds, &write_fds, &timeout, &max_fd);
 #endif
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    platformInfraIfUpdateFdSet(&read_fds, &write_fds, &max_fd);
+#endif
+
+#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
+    platformBleUpdateFdSet(&read_fds, &write_fds, &timeout, &max_fd);
+#endif
 
     if (otTaskletsArePending(aInstance))
     {
@@ -235,6 +312,9 @@
     {
         platformUartProcess();
         platformRadioProcess(aInstance, &read_fds, &write_fds);
+#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
+        platformBleProcess(aInstance, &read_fds, &write_fds);
+#endif
     }
     else if (errno != EINTR)
     {
@@ -246,6 +326,9 @@
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
     platformTrelProcess(aInstance, &read_fds, &write_fds);
 #endif
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    platformInfraIfProcess(aInstance, &read_fds, &write_fds);
+#endif
 
     if (gTerminate)
     {
diff --git a/examples/platforms/simulation/trel.c b/examples/platforms/simulation/trel.c
index cfc34ac..696b1a5 100644
--- a/examples/platforms/simulation/trel.c
+++ b/examples/platforms/simulation/trel.c
@@ -31,6 +31,7 @@
 #include <openthread/random_noncrypto.h>
 #include <openthread/platform/trel.h>
 
+#include "simul_utils.h"
 #include "utils/code_utils.h"
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
@@ -38,8 +39,6 @@
 // Change DEBUG_LOG to all extra logging
 #define DEBUG_LOG 0
 
-// The IPv4 group for receiving
-#define TREL_SIM_GROUP "224.0.0.116"
 #define TREL_SIM_PORT 9200
 
 #define TREL_MAX_PACKET_SIZE 1800
@@ -67,11 +66,9 @@
 static uint8_t sNumPendingTx = 0;
 static Message sPendingTx[TREL_MAX_PENDING_TX];
 
-static int      sTxFd       = -1;
-static int      sRxFd       = -1;
-static uint16_t sPortOffset = 0;
-static bool     sEnabled    = false;
-static uint16_t sUdpPort;
+static utilsSocket sSocket;
+static uint16_t    sPortOffset = 0;
+static bool        sEnabled    = false;
 
 static bool               sServiceRegistered = false;
 static uint16_t           sServicePort;
@@ -117,80 +114,6 @@
 }
 #endif
 
-static void initFds(void)
-{
-    int                fd;
-    int                one = 1;
-    struct sockaddr_in sockaddr;
-    struct ip_mreqn    mreq;
-
-    memset(&sockaddr, 0, sizeof(sockaddr));
-
-    otEXPECT_ACTION((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1, perror("socket(sTxFd)"));
-
-    sUdpPort                 = (uint16_t)(TREL_SIM_PORT + sPortOffset + gNodeId);
-    sockaddr.sin_family      = AF_INET;
-    sockaddr.sin_port        = htons(sUdpPort);
-    sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
-
-    otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &sockaddr.sin_addr, sizeof(sockaddr.sin_addr)) != -1,
-                    perror("setsockopt(sTxFd, IP_MULTICAST_IF)"));
-
-    otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &one, sizeof(one)) != -1,
-                    perror("setsockopt(sTxFd, IP_MULTICAST_LOOP)"));
-
-    otEXPECT_ACTION(bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != -1, perror("bind(sTxFd)"));
-
-    // Tx fd is successfully initialized.
-    sTxFd = fd;
-
-    otEXPECT_ACTION((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1, perror("socket(sRxFd)"));
-
-    otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) != -1,
-                    perror("setsockopt(sRxFd, SO_REUSEADDR)"));
-    otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) != -1,
-                    perror("setsockopt(sRxFd, SO_REUSEPORT)"));
-
-    memset(&mreq, 0, sizeof(mreq));
-    inet_pton(AF_INET, TREL_SIM_GROUP, &mreq.imr_multiaddr);
-
-    // Always use loopback device to send simulation packets.
-    mreq.imr_address.s_addr = inet_addr("127.0.0.1");
-
-    otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &mreq.imr_address, sizeof(mreq.imr_address)) != -1,
-                    perror("setsockopt(sRxFd, IP_MULTICAST_IF)"));
-    otEXPECT_ACTION(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) != -1,
-                    perror("setsockopt(sRxFd, IP_ADD_MEMBERSHIP)"));
-
-    sockaddr.sin_family      = AF_INET;
-    sockaddr.sin_port        = htons((uint16_t)(TREL_SIM_PORT + sPortOffset));
-    sockaddr.sin_addr.s_addr = inet_addr(TREL_SIM_GROUP);
-
-    otEXPECT_ACTION(bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != -1, perror("bind(sRxFd)"));
-
-    // Rx fd is successfully initialized.
-    sRxFd = fd;
-
-exit:
-    if (sRxFd == -1 || sTxFd == -1)
-    {
-        exit(EXIT_FAILURE);
-    }
-}
-
-static void deinitFds(void)
-{
-    if (sRxFd != -1)
-    {
-        close(sRxFd);
-    }
-
-    if (sTxFd != -1)
-    {
-        close(sTxFd);
-    }
-}
-
 static uint16_t getMessageSize(const Message *aMessage)
 {
     return (uint16_t)(&aMessage->mData[aMessage->mDataLength] - (const uint8_t *)aMessage);
@@ -198,31 +121,13 @@
 
 static void sendPendingTxMessages(void)
 {
-    ssize_t            rval;
-    struct sockaddr_in sockaddr;
-
-    memset(&sockaddr, 0, sizeof(sockaddr));
-    sockaddr.sin_family = AF_INET;
-    inet_pton(AF_INET, TREL_SIM_GROUP, &sockaddr.sin_addr);
-
-    sockaddr.sin_port = htons((uint16_t)(TREL_SIM_PORT + sPortOffset));
-
     for (uint8_t i = 0; i < sNumPendingTx; i++)
     {
-        uint16_t size = getMessageSize(&sPendingTx[i]);
-
 #if DEBUG_LOG
         fprintf(stderr, "\r\n[trel-sim] Sending message (num:%d, type:%s, port:%u)\r\n", i,
                 messageTypeToString(sPendingTx[i].mType), sPendingTx[i].mSockAddr.mPort);
 #endif
-
-        rval = sendto(sTxFd, &sPendingTx[i], size, 0, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
-
-        if (rval < 0)
-        {
-            perror("sendto(sTxFd)");
-            exit(EXIT_FAILURE);
-        }
+        utilsSendOverSocket(&sSocket, &sPendingTx[i], getMessageSize(&sPendingTx[i]));
     }
 
     sNumPendingTx = 0;
@@ -279,7 +184,7 @@
     switch (aMessage->mType)
     {
     case TREL_DATA_MESSAGE:
-        otEXPECT(aMessage->mSockAddr.mPort == sUdpPort);
+        otEXPECT(aMessage->mSockAddr.mPort == sSocket.mPort);
         otPlatTrelHandleReceived(aInstance, aMessage->mData, aMessage->mDataLength);
         break;
 
@@ -309,7 +214,7 @@
 {
     OT_UNUSED_VARIABLE(aInstance);
 
-    *aUdpPort = sUdpPort;
+    *aUdpPort = sSocket.mPort;
 
 #if DEBUG_LOG
     fprintf(stderr, "\r\n[trel-sim] otPlatTrelEnable() *aUdpPort=%u\r\n", *aUdpPort);
@@ -417,62 +322,46 @@
         sPortOffset *= (MAX_NETWORK_SIZE + 1);
     }
 
-    initFds();
+    utilsInitSocket(&sSocket, TREL_SIM_PORT + sPortOffset);
 
     OT_UNUSED_VARIABLE(aSpeedUpFactor);
 }
 
-void platformTrelDeinit(void) { deinitFds(); }
+void platformTrelDeinit(void) { utilsDeinitSocket(&sSocket); }
 
 void platformTrelUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, struct timeval *aTimeout, int *aMaxFd)
 {
     OT_UNUSED_VARIABLE(aTimeout);
 
     // Always ready to receive
-    if (aReadFdSet != NULL)
+    utilsAddSocketRxFd(&sSocket, aReadFdSet, aMaxFd);
+
+    if (sNumPendingTx > 0)
     {
-        FD_SET(sRxFd, aReadFdSet);
-
-        if (aMaxFd != NULL && *aMaxFd < sRxFd)
-        {
-            *aMaxFd = sRxFd;
-        }
-    }
-
-    if ((aWriteFdSet != NULL) && (sNumPendingTx > 0))
-    {
-        FD_SET(sTxFd, aWriteFdSet);
-
-        if (aMaxFd != NULL && *aMaxFd < sTxFd)
-        {
-            *aMaxFd = sTxFd;
-        }
+        utilsAddSocketTxFd(&sSocket, aWriteFdSet, aMaxFd);
     }
 }
 
 void platformTrelProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
 {
-    if (FD_ISSET(sTxFd, aWriteFdSet) && (sNumPendingTx > 0))
+    if ((sNumPendingTx > 0) && utilsCanSocketSend(&sSocket, aWriteFdSet))
     {
         sendPendingTxMessages();
     }
 
-    if (FD_ISSET(sRxFd, aReadFdSet))
+    if (utilsCanSocketReceive(&sSocket, aReadFdSet))
     {
-        Message message;
-        ssize_t rval;
+        Message  message;
+        uint16_t len;
 
         message.mDataLength = 0;
 
-        rval = recvfrom(sRxFd, (char *)&message, sizeof(message), 0, NULL, NULL);
+        len = utilsReceiveFromSocket(&sSocket, &message, sizeof(message), NULL);
 
-        if (rval < 0)
+        if (len > 0)
         {
-            perror("recvfrom(sRxFd)");
-            exit(EXIT_FAILURE);
+            processMessage(aInstance, &message, len);
         }
-
-        processMessage(aInstance, &message, (uint16_t)(rval));
     }
 }
 
diff --git a/examples/platforms/simulation/uart.c b/examples/platforms/simulation/uart.c
index fe3fceb..70d3871 100644
--- a/examples/platforms/simulation/uart.c
+++ b/examples/platforms/simulation/uart.c
@@ -40,6 +40,7 @@
 
 #include <openthread/platform/debug_uart.h>
 
+#include "simul_utils.h"
 #include "utils/code_utils.h"
 #include "utils/uart.h"
 
@@ -172,34 +173,13 @@
 
 void platformUartUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, fd_set *aErrorFdSet, int *aMaxFd)
 {
-    if (aReadFdSet != NULL)
+    utilsAddFdToFdSet(s_in_fd, aReadFdSet, aMaxFd);
+    utilsAddFdToFdSet(s_in_fd, aErrorFdSet, aMaxFd);
+
+    if ((s_write_length > 0))
     {
-        FD_SET(s_in_fd, aReadFdSet);
-
-        if (aErrorFdSet != NULL)
-        {
-            FD_SET(s_in_fd, aErrorFdSet);
-        }
-
-        if (aMaxFd != NULL && *aMaxFd < s_in_fd)
-        {
-            *aMaxFd = s_in_fd;
-        }
-    }
-
-    if ((aWriteFdSet != NULL) && (s_write_length > 0))
-    {
-        FD_SET(s_out_fd, aWriteFdSet);
-
-        if (aErrorFdSet != NULL)
-        {
-            FD_SET(s_out_fd, aErrorFdSet);
-        }
-
-        if (aMaxFd != NULL && *aMaxFd < s_out_fd)
-        {
-            *aMaxFd = s_out_fd;
-        }
+        utilsAddFdToFdSet(s_out_fd, aWriteFdSet, aMaxFd);
+        utilsAddFdToFdSet(s_out_fd, aErrorFdSet, aMaxFd);
     }
 }
 
diff --git a/examples/platforms/utils/mac_frame.cpp b/examples/platforms/utils/mac_frame.cpp
index 0ae88db..36a1c97 100644
--- a/examples/platforms/utils/mac_frame.cpp
+++ b/examples/platforms/utils/mac_frame.cpp
@@ -43,7 +43,7 @@
     Mac::Address      dst;
     Mac::PanId        panid;
 
-    SuccessOrExit(frame.GetDstAddr(dst));
+    VerifyOrExit(frame.GetDstAddr(dst) == kErrorNone, rval = false);
 
     switch (dst.GetType())
     {
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..1e5ff61 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:
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/instance.h b/include/openthread/instance.h
index 546b5d1..1ce766c 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 (397)
+#define OPENTHREAD_API_VERSION (402)
 
 /**
  * @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/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/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/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..a0921af 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'
diff --git a/src/cli/BUILD.gn b/src/cli/BUILD.gn
index 318a4d0..b74577a 100644
--- a/src/cli/BUILD.gn
+++ b/src/cli/BUILD.gn
@@ -55,8 +55,6 @@
   "cli_mac_filter.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 +65,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..9dfee40 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -45,13 +45,13 @@
     cli_link_metrics.cpp
     cli_mac_filter.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..c8dfe27 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)
@@ -357,12 +358,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 +1114,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 +1826,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 +1845,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 +2764,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.
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_DATASET.md b/src/cli/README_DATASET.md
index 54f4c1e..1f32cce 100644
--- a/src/cli/README_DATASET.md
+++ b/src/cli/README_DATASET.md
@@ -107,6 +107,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 +293,7 @@
 - [pskc](#pskc)
 - [securitypolicy](#securitypolicy)
 - [tlvs](#tlvs)
+- [updater](#updater)
 
 ## Command Details
 
@@ -695,3 +748,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 4d1ab02..33d1123 100644
--- a/src/cli/cli.cpp
+++ b/src/cli/cli.cpp
@@ -68,7 +68,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 +101,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)
@@ -337,7 +339,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 +410,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 +423,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);
@@ -610,6 +592,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);
@@ -618,6 +692,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
@@ -1330,7 +1426,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")
     {
         /**
@@ -1347,26 +1445,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
@@ -1395,7 +1509,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
          */
@@ -1406,7 +1522,7 @@
             SuccessOrExit(error = aArgs[2].ParseAsBool(enable));
             error = otChannelManagerRequestChannelSelect(GetInstancePtr(), enable);
         }
-#endif
+#endif // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
         /**
          * @cli channel manager auto
          * @code
@@ -1417,7 +1533,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
          */
@@ -1428,6 +1546,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
@@ -1445,6 +1589,7 @@
         {
             error = ProcessGetSet(aArgs + 2, otChannelManagerGetDelay, otChannelManagerSetDelay);
         }
+#endif
         /**
          * @cli channel manager interval
          * @code
@@ -1454,7 +1599,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
          */
@@ -1471,7 +1618,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
          */
@@ -1488,7 +1637,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
          */
@@ -1506,7 +1657,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
          */
@@ -2371,9 +2524,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
@@ -8211,76 +8364,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
diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp
index 142f02c..75ea629 100644
--- a/src/cli/cli.hpp
+++ b/src/cli/cli.hpp
@@ -68,13 +68,13 @@
 #include "cli/cli_link_metrics.hpp"
 #include "cli/cli_mac_filter.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 +108,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;
@@ -128,8 +128,6 @@
     friend void otCliOutputFormat(const char *aFmt, ...);
 
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -179,19 +177,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.
@@ -289,94 +274,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 +392,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);
 
@@ -599,46 +501,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 9464dff..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
@@ -949,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 e5dff9a..5148bb3 100644
--- a/src/cli/cli_dns.cpp
+++ b/src/cli/cli_dns.cpp
@@ -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_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 7d88259..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)
 {
 }
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 d90947b..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);
@@ -453,7 +453,7 @@
  */
 template <> otError SrpClient::Process<Cmd("leaseinterval")>(Arg aArgs[])
 {
-    return Interpreter::GetInterpreter().ProcessGetSet(aArgs, otSrpClientGetLeaseInterval, otSrpClientSetLeaseInterval);
+    return ProcessGetSet(aArgs, otSrpClientGetLeaseInterval, otSrpClientSetLeaseInterval);
 }
 
 /**
@@ -475,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[])
@@ -577,9 +576,9 @@
      * * -->                          [@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}.
@@ -664,8 +663,8 @@
     {
         // `key [enable/disable]`
 
-        error = Interpreter::GetInterpreter().ProcessEnableDisable(aArgs + 1, otSrpClientIsServiceKeyRecordEnabled,
-                                                                   otSrpClientSetServiceKeyRecordEnabled);
+        error = ProcessEnableDisable(aArgs + 1, otSrpClientIsServiceKeyRecordEnabled,
+                                     otSrpClientSetServiceKeyRecordEnabled);
     }
 #endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     else
@@ -929,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 ee61a33..5e71a4a 100644
--- a/src/cli/cli_srp_server.cpp
+++ b/src/cli/cli_srp_server.cpp
@@ -121,8 +121,7 @@
  */
 template <> otError SrpServer::Process<Cmd("auto")>(Arg aArgs[])
 {
-    return Interpreter::GetInterpreter().ProcessEnableDisable(aArgs, otSrpServerIsAutoEnableMode,
-                                                              otSrpServerSetAutoEnableMode);
+    return ProcessEnableDisable(aArgs, otSrpServerIsAutoEnableMode, otSrpServerSetAutoEnableMode);
 }
 #endif
 
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 7845be8..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"
 
diff --git a/src/cli/cli_tcat.hpp b/src/cli/cli_tcat.hpp
index 91b36cc..697cba1 100644
--- a/src/cli/cli_tcat.hpp
+++ b/src/cli/cli_tcat.hpp
@@ -33,7 +33,7 @@
 
 #include <openthread/tcat.h>
 
-#include "cli/cli_output.hpp"
+#include "cli/cli_utils.hpp"
 
 #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE && OPENTHREAD_CONFIG_CLI_BLE_SECURE_ENABLE
 
@@ -45,11 +45,9 @@
  * Implements the Tcat CLI interpreter.
  *
  */
-class Tcat : private Output
+class Tcat : private Utils
 {
 public:
-    typedef Utils::CmdLineParser::Arg Arg;
-
     /**
      * Constructor
      *
@@ -58,7 +56,7 @@
      *
      */
     Tcat(otInstance *aInstance, OutputImplementer &aOutputImplementer)
-        : Output(aInstance, aOutputImplementer)
+        : Utils(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_tcp.cpp b/src/cli/cli_tcp.cpp
index 90ce1aa..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)
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 9f8afdf..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);
@@ -408,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 71%
rename from src/cli/cli_output.cpp
rename to src/cli/cli_utils.cpp
index c70eacd..f2f5359 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)
 {
@@ -370,7 +370,7 @@
 }
 
 #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
-void Output::LogInput(const Arg *aArgs)
+void Utils::LogInput(const Arg *aArgs)
 {
     String<kInputOutputLogStringSize> inputString;
 
@@ -383,7 +383,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++)
     {
@@ -412,7 +412,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++)
     {
@@ -427,5 +427,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 a866cd0..715741d 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}")
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..7c8002d 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>(
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/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/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/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/routing_manager.cpp b/src/core/border_router/routing_manager.cpp
index 81f2d9a..bebd795 100644
--- a/src/core/border_router/routing_manager.cpp
+++ b/src/core/border_router/routing_manager.cpp
@@ -363,6 +363,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 +494,10 @@
     mNat64PrefixManager.Evaluate();
 #endif
 
-    SendRouterAdvertisement(kAdvPrefixesFromNetData);
+    if (IsInitalPolicyEvaluationDone())
+    {
+        SendRouterAdvertisement(kAdvPrefixesFromNetData);
+    }
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
     if (Get<Srp::Server>().IsAutoEnableMode() && IsInitalPolicyEvaluationDone())
@@ -568,35 +587,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 +612,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 +667,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 +722,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 +735,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 +759,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 +785,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 +884,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 +998,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 +1029,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 +1058,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 +1100,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 +1145,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 +1187,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 +1498,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 +1577,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 +1594,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 +1620,12 @@
 
         if (router.mManagedAddressConfigFlag)
         {
-            aRaMessage.GetHeader().SetManagedAddressConfigFlag();
+            aHeader.SetManagedAddressConfigFlag();
         }
 
         if (router.mOtherConfigFlag)
         {
-            aRaMessage.GetHeader().SetOtherConfigFlag();
+            aHeader.SetOtherConfigFlag();
         }
     }
 }
@@ -1775,7 +1712,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 +1721,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 +1730,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;
@@ -2525,13 +2462,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 +2482,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 +2504,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 +2526,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 +2768,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 +2783,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 +2878,7 @@
         {
             if (now >= prefix.mExpirationTime)
             {
-                AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage);
+                SuccessOrExit(error = AppendRio(prefix.mPrefix, /* aRouteLifetime */ 0, aRaMessage));
                 continue;
             }
         }
@@ -2938,7 +2891,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 +2908,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 +3406,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 +3497,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();
@@ -3652,12 +3673,12 @@
 
 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));
+    error = Process(RouterAdvert::RxMessage(packet));
     mNumPlatformRaReceived++;
     mLastPlatformRaTime = TimerMilli::GetNow();
 
@@ -3668,7 +3689,7 @@
     }
 }
 
-Error RoutingManager::PdPrefixManager::Process(const Ip6::Nd::RouterAdvertMessage &aMessage)
+Error RoutingManager::PdPrefixManager::Process(const RouterAdvert::RxMessage &aMessage)
 {
     Error                        error = kErrorNone;
     DiscoveredPrefixTable::Entry favoredEntry;
@@ -3677,17 +3698,17 @@
     VerifyOrExit(aMessage.IsValid(), error = kErrorParse);
     favoredEntry.Clear();
 
-    for (const Ip6::Nd::Option &option : aMessage)
+    for (const Option &option : aMessage)
     {
         DiscoveredPrefixTable::Entry entry;
 
-        if (option.GetType() != Ip6::Nd::Option::Type::kTypePrefixInfo ||
-            !static_cast<const Ip6::Nd::PrefixInfoOption &>(option).IsValid())
+        if (option.GetType() != Option::kTypePrefixInfo || !static_cast<const PrefixInfoOption &>(option).IsValid())
         {
             continue;
         }
+
         mNumPlatformPioProcessed++;
-        entry.SetFrom(static_cast<const Ip6::Nd::PrefixInfoOption &>(option));
+        entry.SetFrom(static_cast<const PrefixInfoOption &>(option));
 
         if (!IsValidPdPrefix(entry.GetPrefix()))
         {
diff --git a/src/core/border_router/routing_manager.hpp b/src/core/border_router/routing_manager.hpp
index 4aef01d..37de6ec 100644
--- a/src/core/border_router/routing_manager.hpp
+++ b/src/core/border_router/routing_manager.hpp
@@ -55,6 +55,7 @@
 #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 +63,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"
@@ -233,6 +235,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:
@@ -581,6 +599,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 +657,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 +674,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 +750,9 @@
                 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);
             Type               GetType(void) const { return mType; }
             bool               IsOnLinkPrefix(void) const { return (mType == kTypeOnLink); }
             bool               IsRoutePrefix(void) const { return (mType == kTypeRoute); }
@@ -824,10 +850,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 +977,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 +1006,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 +1039,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 +1067,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 +1178,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(); }
@@ -1246,7 +1290,7 @@
         }
 
     private:
-        Error Process(const Ip6::Nd::RouterAdvertMessage &aMessage);
+        Error Process(const RouterAdvert::RxMessage &aMessage);
         void  EvaluateStateChange(Dhcp6PdState aOldState);
         void  WithdrawPrefix(void);
         void  StartStop(bool aStart);
@@ -1282,17 +1326,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 +1377,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..4998de7 100644
--- a/src/core/coap/coap_message.cpp
+++ b/src/core/coap/coap_message.cpp
@@ -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..3a4c342 100644
--- a/src/core/common/message.cpp
+++ b/src/core/common/message.cpp
@@ -770,12 +770,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 +802,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..f3dfd54 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.
@@ -1015,11 +1015,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 +1039,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 +1270,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 +1296,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 +1322,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 +1521,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/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/misc.h b/src/core/config/misc.h
index b04e32e..70e0c12 100644
--- a/src/core/config/misc.h
+++ b/src/core/config/misc.h
@@ -424,9 +424,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 +441,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/instance/instance.cpp b/src/core/instance/instance.cpp
index 4735ce3..3076110 100644
--- a/src/core/instance/instance.cpp
+++ b/src/core/instance/instance.cpp
@@ -230,7 +230,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
diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp
index 1ebc01c..30b443b 100644
--- a/src/core/instance/instance.hpp
+++ b/src/core/instance/instance.hpp
@@ -645,7 +645,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
 
@@ -946,7 +948,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/mac_frame.cpp b/src/core/mac/mac_frame.cpp
index 3e84465..9946bb2 100644
--- a/src/core/mac/mac_frame.cpp
+++ b/src/core/mac/mac_frame.cpp
@@ -1220,9 +1220,11 @@
 {
     uint8_t *cur = GetThreadIe(ThreadIe::kEnhAckProbingIe);
 
-    OT_ASSERT(cur != nullptr);
-
+    VerifyOrExit(cur != nullptr);
     memcpy(cur + sizeof(HeaderIe) + sizeof(VendorIeHeader), aValue, aLen);
+
+exit:
+    return;
 }
 #endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 
diff --git a/src/core/meshcop/border_agent.cpp b/src/core/meshcop/border_agent.cpp
index 142c606..24e7331 100644
--- a/src/core/meshcop/border_agent.cpp
+++ b/src/core/meshcop/border_agent.cpp
@@ -128,7 +128,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)
@@ -158,7 +158,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send error CoAP message", error);
+    LogWarnOnError(error, "send error CoAP message");
 }
 
 Error BorderAgent::SendMessage(Coap::Message &aMessage)
@@ -239,6 +239,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();
 }
@@ -341,7 +347,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)
@@ -392,7 +398,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;
@@ -430,7 +436,7 @@
     LogInfo("Sent to commissioner");
 
 exit:
-    LogError("send to commissioner", error);
+    LogWarnOnError(error, "send to commissioner");
     return error;
 }
 
@@ -514,7 +520,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)
@@ -570,7 +576,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 9f721d6..933a95c 100644
--- a/src/core/meshcop/border_agent.hpp
+++ b/src/core/meshcop/border_agent.hpp
@@ -45,6 +45,8 @@
 #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"
@@ -60,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.
 
     /**
@@ -125,7 +151,7 @@
      * Starts the Border Agent service.
      *
      */
-    void Start(void);
+    void Start(void) { IgnoreError(Start(kUdpPort)); }
 
     /**
      * Stops the Border Agent service.
@@ -141,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.
@@ -151,9 +246,16 @@
     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)
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
+    static constexpr uint16_t kMaxEphemeralKeyConnectionAttempts = 10;
+#endif
+
     class ForwardContext : public InstanceLocatorInit, public Heap::Allocatable<ForwardContext>
     {
     public:
@@ -171,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);
@@ -185,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,
@@ -195,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;
@@ -212,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..0285956 100644
--- a/src/core/meshcop/dataset.cpp
+++ b/src/core/meshcop/dataset.cpp
@@ -251,10 +251,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,10 +271,10 @@
     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)
diff --git a/src/core/meshcop/dataset.hpp b/src/core/meshcop/dataset.hpp
index 131a271..5fbc799 100644
--- a/src/core/meshcop/dataset.hpp
+++ b/src/core/meshcop/dataset.hpp
@@ -76,6 +76,12 @@
     };
 
     /**
+     * Represents a Dataset as a sequence of TLVs.
+     *
+     */
+    typedef otOperationalDatasetTlvs Tlvs;
+
+    /**
      * Represents presence of different components in Active or Pending Operational Dataset.
      *
      */
@@ -770,10 +776,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.
@@ -859,10 +865,10 @@
     /**
      * 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);
+    void SetFrom(const Tlvs &aTlvs);
 
     /**
      * Appends the MLE Dataset TLV but excluding MeshCoP Sub Timestamp TLV.
diff --git a/src/core/meshcop/dataset_local.cpp b/src/core/meshcop/dataset_local.cpp
index edb8108..ba4c5fc 100644
--- a/src/core/meshcop/dataset_local.cpp
+++ b/src/core/meshcop/dataset_local.cpp
@@ -147,15 +147,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 +173,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);
 }
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..6c49a23 100644
--- a/src/core/meshcop/dataset_manager.cpp
+++ b/src/core/meshcop/dataset_manager.cpp
@@ -160,11 +160,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:
@@ -292,7 +292,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;
     }
@@ -697,11 +701,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:
diff --git a/src/core/meshcop/dataset_manager.hpp b/src/core/meshcop/dataset_manager.hpp
index 36162a3..c0d90fe 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.
@@ -261,13 +261,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.
@@ -459,13 +459,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 +558,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/joiner.cpp b/src/core/meshcop/joiner.cpp
index fb0f3e6..4df5f96 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;
 }
 
@@ -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..4fc8385 100644
--- a/src/core/meshcop/meshcop_leader.cpp
+++ b/src/core/meshcop/meshcop_leader.cpp
@@ -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,7 +211,7 @@
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("send dataset changed", error);
+    LogWarnOnError(error, "send dataset changed");
 }
 
 Error Leader::SetDelayTimerMinimal(uint32_t aDelayTimerMinimal)
diff --git a/src/core/meshcop/tcat_agent.cpp b/src/core/meshcop/tcat_agent.cpp
index f8df773..f8e4596 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;
 }
 
@@ -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));
 
@@ -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/dnssd_server.cpp b/src/core/net/dnssd_server.cpp
index 385eb88..e05a7d9 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;
 
diff --git a/src/core/net/ip6.cpp b/src/core/net/ip6.cpp
index 0c0bf5a..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:
@@ -1094,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;
@@ -1115,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.
@@ -1203,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/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_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/openthread-core-config.h b/src/core/openthread-core-config.h
index 498eb62..009b671 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
 
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/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 df1aa25..4b5b4b2 100644
--- a/src/core/thread/mesh_forwarder.cpp
+++ b/src/core/thread/mesh_forwarder.cpp
@@ -1477,7 +1477,7 @@
 
         message->SetDatagramTag(fragmentHeader.GetDatagramTag());
         message->SetTimestampToNow();
-        message->SetLinkInfo(aLinkInfo);
+        message->UpdateLinkInfoFrom(aLinkInfo);
 
         VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);
 
@@ -1530,9 +1530,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();
     }
 
@@ -1543,7 +1541,7 @@
         if (message->GetOffset() >= message->GetLength())
         {
             mReassemblyList.Dequeue(*message);
-            IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
+            IgnoreError(HandleDatagram(*message, aMacAddrs.mSource));
         }
     }
     else
@@ -1643,7 +1641,7 @@
 
     SuccessOrExit(error = FrameToMessage(aFrameData, 0, aMacAddrs, message));
 
-    message->SetLinkInfo(aLinkInfo);
+    message->UpdateLinkInfoFrom(aLinkInfo);
 
     VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);
 
@@ -1655,7 +1653,7 @@
 
     if (error == kErrorNone)
     {
-        IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
+        IgnoreError(HandleDatagram(*message, aMacAddrs.mSource));
     }
     else
     {
@@ -1664,7 +1662,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);
@@ -1680,7 +1678,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..99e6809 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));
@@ -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
 
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 76b8dc3..b55341f 100644
--- a/src/core/thread/mle_router.cpp
+++ b/src/core/thread/mle_router.cpp
@@ -363,15 +363,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 +449,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 +528,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 +674,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 +690,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Source Address
     switch (Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress))
     {
     case kErrorNone:
@@ -711,7 +716,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 +727,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // TLV Request
     switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
     {
     case kErrorNone:
@@ -748,16 +753,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 +780,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 +826,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 +877,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 +888,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 +915,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 +938,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 +959,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 +976,6 @@
     case kRoleLeader:
         VerifyOrExit(router != nullptr);
 
-        // Leader Data
         SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
         VerifyOrExit(leaderData.GetPartitionId() == mLeaderData.GetPartitionId());
 
@@ -992,7 +986,6 @@
             IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
         }
 
-        // Route (optional)
         switch (aRxInfo.mMessage.ReadRouteTlv(routeTlv))
         {
         case kErrorNone:
@@ -1026,7 +1019,6 @@
         OT_ASSERT(false);
     }
 
-    // finish link synchronization
     InitNeighbor(*router, aRxInfo);
     router->SetRloc16(sourceAddress);
     router->GetLinkFrameCounters().SetAll(linkFrameCounter);
@@ -1049,10 +1041,8 @@
         RxChallenge challenge;
         TlvList     requestedTlvList;
 
-        // Challenge
         SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(challenge));
 
-        // TLV Request
         switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
         {
         case kErrorNone:
@@ -1062,7 +1052,7 @@
             ExitNow(error = kErrorParse);
         }
 
-        SuccessOrExit(error = SendLinkAccept(aRxInfo.mMessageInfo, router, requestedTlvList, challenge));
+        SuccessOrExit(error = SendLinkAccept(aRxInfo, router, requestedTlvList, challenge));
     }
 
 exit:
@@ -1178,7 +1168,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 +1208,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 +1323,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 +1342,6 @@
 exit:
     if (aRxInfo.mNeighbor && aRxInfo.mNeighbor->GetRloc16() != aSourceAddress)
     {
-        // Remove stale neighbors
         RemoveNeighbor(*aRxInfo.mNeighbor);
     }
 
@@ -1395,11 +1386,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 +1408,6 @@
         break;
     }
 
-    // Challenge
     SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(challenge));
 
     child = mChildTable.FindChild(extAddr, Child::kInStateAnyExceptInvalid);
@@ -1428,7 +1416,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 +1497,9 @@
         mPreviousPartitionIdTimeout--;
     }
 
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Role transitions
+
     roleTransitionTimeoutExpired = mRouterRoleTransition.HandleTimeTick();
 
     switch (mRole)
@@ -1532,7 +1522,6 @@
             }
             else
             {
-                // send announce after decided to stay in REED if needed
                 InformPreviousChannel();
             }
 
@@ -1549,11 +1538,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 +1567,9 @@
         OT_ASSERT(false);
     }
 
-    // update children state
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Update `ChildTable`
+
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateAnyExceptInvalid))
     {
         uint32_t timeout = 0;
@@ -1605,7 +1596,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 +1604,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 +1613,9 @@
         }
     }
 
-    // update router state
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Update `RouterTable`
+
     for (Router &router : Get<RouterTable>())
     {
         uint32_t age;
@@ -1635,53 +1628,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()));
         }
     }
 
@@ -1807,7 +1782,6 @@
 #endif
 
 #if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
-    // Retrieve registered multicast addresses of the Child
     if (aChild.HasAnyMlrRegisteredAddress())
     {
         OT_ASSERT(aChild.IsStateValid());
@@ -1871,7 +1845,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
@@ -1917,7 +1892,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;
             }
         }
@@ -1999,43 +1974,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:
@@ -2048,7 +2013,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Active Timestamp
     switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
@@ -2067,7 +2031,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Pending Timestamp
     switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
@@ -2093,12 +2056,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);
     }
 
@@ -2118,7 +2079,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
@@ -2179,11 +2140,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:
@@ -2236,7 +2195,6 @@
         tlvList.Add(Tlv::kLinkFrameCounter);
     }
 
-    // IPv6 Address TLV
     switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
     {
     case kErrorNone:
@@ -2248,7 +2206,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Leader Data
     switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
     {
     case kErrorNone:
@@ -2260,7 +2217,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Timeout
     switch (Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout))
     {
     case kErrorNone:
@@ -2280,7 +2236,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Supervision interval
     switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
     {
     case kErrorNone:
@@ -2298,7 +2253,6 @@
 
     child->SetSupervisionInterval(supervisionInterval);
 
-    // TLV Request
     switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
     {
     case kErrorNone:
@@ -2408,7 +2362,6 @@
 
     child = static_cast<Child *>(aRxInfo.mNeighbor);
 
-    // Response
     switch (aRxInfo.mMessage.ReadResponseTlv(response))
     {
     case kErrorNone:
@@ -2424,7 +2377,6 @@
 
     Log(kMessageReceive, kTypeChildUpdateResponseOfChild, aRxInfo.mMessageInfo.GetPeerAddr(), child->GetRloc16());
 
-    // Source Address
     switch (Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress))
     {
     case kErrorNone:
@@ -2443,7 +2395,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Status
     switch (Tlv::Find<StatusTlv>(aRxInfo.mMessage, status))
     {
     case kErrorNone:
@@ -2455,8 +2406,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Link-Layer Frame Counter
-
     switch (Tlv::Find<LinkFrameCounterTlv>(aRxInfo.mMessage, linkFrameCounter))
     {
     case kErrorNone:
@@ -2469,7 +2418,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // MLE Frame Counter
     switch (Tlv::Find<MleFrameCounterTlv>(aRxInfo.mMessage, mleFrameCounter))
     {
     case kErrorNone:
@@ -2481,7 +2429,6 @@
         ExitNow(error = kErrorNone);
     }
 
-    // Timeout
     switch (Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout))
     {
     case kErrorNone:
@@ -2508,7 +2455,6 @@
         }
     }
 
-    // IPv6 Address
     switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
     {
     case kErrorNone:
@@ -2518,7 +2464,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Leader Data
     switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
     {
     case kErrorNone:
@@ -2533,7 +2478,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;
 
@@ -2551,10 +2496,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:
@@ -2573,7 +2516,6 @@
         ExitNow(error = kErrorParse);
     }
 
-    // Pending Timestamp
     switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
@@ -2685,7 +2627,6 @@
 
     discoveryRequestTlv.SetLength(0);
 
-    // only Routers and REEDs respond
     VerifyOrExit(IsRouterEligible(), error = kErrorInvalidState);
 
     SuccessOrExit(error = Tlv::FindTlvValueStartEndOffsets(aRxInfo.mMessage, Tlv::kDiscovery, offset, end));
@@ -2767,13 +2708,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);
 
@@ -2798,11 +2737,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));
@@ -2843,7 +2780,7 @@
     {
         uint16_t rloc16;
 
-        // pick next Child ID that is not being used
+        // Pick next Child ID that is not being used
         do
         {
             mNextChildId++;
@@ -2857,7 +2794,6 @@
 
         } while (mChildTable.FindChild(rloc16, Child::kInStateAnyExceptInvalid) != nullptr);
 
-        // allocate Child ID
         aChild.SetRloc16(rloc16);
     }
 
@@ -2937,14 +2873,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;
             }
@@ -3155,10 +3093,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));
@@ -3240,7 +3176,6 @@
 
         if (aNeighbor.IsFullThreadDevice())
         {
-            // Clear all EID-to-RLOC entries associated with the child.
             Get<AddressResolver>().RemoveEntriesForRloc16(aNeighbor.GetRloc16());
         }
 
@@ -3291,11 +3226,9 @@
         ExitNow();
     }
 
-    // loop exists
     router = mRouterTable.FindRouterByRloc16(aDestRloc16);
     VerifyOrExit(router != nullptr);
 
-    // invalidate next hop
     router->SetNextHopToInvalid();
     ResetAdvertiseInterval();
 
@@ -3305,46 +3238,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)
@@ -3458,7 +3384,6 @@
     SuccessOrExit(Tlv::FindTlv(*aMessage, routerMaskTlv));
     VerifyOrExit(routerMaskTlv.IsValid());
 
-    // assign short address
     SetRouterId(routerId);
 
     SetStateRouter(Rloc16FromRouterId(mRouterId));
@@ -3521,7 +3446,6 @@
     }
 
 exit:
-    // Send announce after received address solicit reply if needed
     InformPreviousChannel();
 }
 
@@ -3582,7 +3506,6 @@
     }
 #endif
 
-    // Check if allocation already exists
     router = mRouterTable.FindRouter(extAddress);
 
     if (router != nullptr)
@@ -3674,7 +3597,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))
     {
@@ -3684,6 +3610,7 @@
         oldRloc16 = aMessageInfo.GetPeerAddr().GetIid().GetLocator();
 
         VerifyOrExit(oldRloc16 != aRouter->GetRloc16());
+        VerifyOrExit(!RouterIdMatch(oldRloc16, GetRloc16()));
         Get<AddressResolver>().RemoveEntriesForRloc16(oldRloc16);
     }
 
@@ -3760,13 +3687,11 @@
     {
         if (router.GetRloc16() == GetRloc16())
         {
-            // skip self
             continue;
         }
 
         if (!router.IsStateValid())
         {
-            // skip non-neighbor routers
             continue;
         }
 
@@ -3966,20 +3891,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/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..788a077 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;
@@ -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/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/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 f03d945..e3911c0 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",
diff --git a/src/lib/spinel/CMakeLists.txt b/src/lib/spinel/CMakeLists.txt
index b8be41c..1c21d6e 100644
--- a/src/lib/spinel/CMakeLists.txt
+++ b/src/lib/spinel/CMakeLists.txt
@@ -89,7 +89,11 @@
 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
+)
 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..b250f09 100644
--- a/src/lib/spinel/radio_spinel.cpp
+++ b/src/lib/spinel/radio_spinel.cpp
@@ -46,6 +46,7 @@
 #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"
 
 namespace ot {
@@ -81,7 +82,8 @@
 }
 
 RadioSpinel::RadioSpinel(void)
-    : mInstance(nullptr)
+    : Logger("RadioSpinel")
+    , mInstance(nullptr)
     , mSpinelInterface(nullptr)
     , mCmdTidsInUse(0)
     , mCmdNextTid(1)
@@ -670,6 +672,9 @@
                 ExitNow();
             }
 
+            // this clear is necessary in case the RCP has sent messages between disable and reset
+            mRxFrameBuffer.Clear();
+
             LogInfo("RCP reset: %s", spinel_status_to_cstr(status));
             sIsReady = true;
         }
@@ -2448,647 +2453,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 +2530,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..5f55c75 100644
--- a/src/lib/spinel/radio_spinel.hpp
+++ b/src/lib/spinel/radio_spinel.hpp
@@ -38,6 +38,7 @@
 
 #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_interface.hpp"
@@ -148,7 +149,7 @@
  * co-processor(RCP).
  *
  */
-class RadioSpinel
+class RadioSpinel : private Logger
 {
 public:
     /**
@@ -1238,17 +1239,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;
@@ -1297,8 +1287,7 @@
 
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
 
-    enum
-    {
+    enum {
         kRcpFailureNone,
         kRcpFailureTimeout,
         kRcpFailureUnexpectedReset,
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..a1ad3ad 100644
--- a/src/ncp/ncp_base.cpp
+++ b/src/ncp/ncp_base.cpp
@@ -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..aa87efa 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;
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 14c40cf..1436e45 100644
--- a/src/posix/platform/daemon.cpp
+++ b/src/posix/platform/daemon.cpp
@@ -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,7 +170,7 @@
     }
     else
     {
-        otLogInfoPlat("Session socket is ready");
+        LogInfo("Session socket is ready");
     }
 }
 
@@ -318,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);
     }
 
@@ -400,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 0d40511..be3738b 100644
--- a/src/posix/platform/daemon.hpp
+++ b/src/posix/platform/daemon.hpp
@@ -31,14 +31,18 @@
 #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);
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/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/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/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 385d8e5..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')
 
@@ -387,19 +398,23 @@
     #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # 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
 
@@ -472,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):
@@ -624,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)
 
@@ -742,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')
 
@@ -751,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
 
@@ -932,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 a8bb7da..463854a 100755
--- a/tests/toranj/cli/test-020-net-diag-vendor-info.py
+++ b/tests/toranj/cli/test-020-net-diag-vendor-info.py
@@ -76,12 +76,12 @@
 
 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')
 
 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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..2fced00 100644
--- a/tests/toranj/openthread-core-toranj-config-posix.h
+++ b/tests/toranj/openthread-core-toranj-config-posix.h
@@ -39,6 +39,16 @@
 
 #define OPENTHREAD_CONFIG_PLATFORM_INFO "POSIX-toranj"
 
+#ifdef __linux__
+#define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1
+#endif
+
+#ifndef OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
+#define OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE 1
+#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..f564e0b 100644
--- a/tests/toranj/openthread-core-toranj-config-simulation.h
+++ b/tests/toranj/openthread-core-toranj-config-simulation.h
@@ -61,7 +61,7 @@
 
 #define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 1
 
-#define OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE 1
+#define OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
 #define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 1
 
@@ -69,6 +69,8 @@
 
 #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..92337b3 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -1180,6 +1180,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_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..2904f60 100644
--- a/tests/unit/test_platform.cpp
+++ b/tests/unit/test_platform.cpp
@@ -694,39 +694,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 +734,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_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_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