Snap for 11689785 from 2bcc4406eadec5eb74d6f5b1f6255f648780d029 to mainline-conscrypt-release

Change-Id: If40eeca8ac0825cf8a8c12f8d5272687ae7bb46d
diff --git a/.github/workflows/border_router.yml b/.github/workflows/border_router.yml
index 591853c..86d484c 100644
--- a/.github/workflows/border_router.yml
+++ b/.github/workflows/border_router.yml
@@ -131,7 +131,7 @@
         echo "id=${GITHUB_WORKFLOW}-${GITHUB_JOB}-${GITHUB_RUN_ID}-${{matrix.name}}" >> $GITHUB_OUTPUT
     - name: Check cached result
       id: check_cache_result
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
         path: |
           _test_complete_
@@ -174,7 +174,7 @@
         export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e OTBR_COVERAGE"
         echo "CI_ENV=${CI_ENV}"
         (cd third_party/openthread/repo && sudo -E ./script/test cert_suite ${{ matrix.cert_scripts }} || (sudo chmod a+r *.log *.json *.pcap && false))
-    - uses: actions/upload-artifact@v3
+    - uses: actions/upload-artifact@v4
       if: ${{ failure() && steps.check_cache_result.outputs.cache-hit != 'true' }}
       with:
         name: thread-1-3-backbone-results
@@ -184,7 +184,7 @@
           third_party/openthread/repo/*.log
     - name: Codecov
       if: ${{ success() && steps.check_cache_result.outputs.cache-hit != 'true' }}
-      uses: codecov/codecov-action@v3
+      uses: codecov/codecov-action@v4
     - name: Cache test result
       if: ${{ success() && steps.check_cache_result.outputs.cache-hit != 'true' }}
       run: |
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 01e02e4..cbd4e17 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -62,7 +62,7 @@
       BUILD_TARGET: check
       OTBR_BUILD_TYPE: ${{ matrix.build_type }}
       OTBR_MDNS: ${{ matrix.mdns }}
-      OTBR_OPTIONS: "-DOTBR_SRP_ADVERTISING_PROXY=ON -DOTBR_BORDER_ROUTING=ON -DOTBR_NAT64=1 -DOTBR_SRP_SERVER_AUTO_ENABLE=OFF"
+      OTBR_OPTIONS: "-DOTBR_SRP_ADVERTISING_PROXY=ON -DOTBR_BORDER_ROUTING=ON -DOTBR_NAT64=ON -DOTBR_DHCP6_PD=ON -DOTBR_SRP_SERVER_AUTO_ENABLE=OFF -DOTBR_TREL=ON"
       OTBR_COVERAGE: 1
     steps:
     - uses: actions/checkout@v4
@@ -73,7 +73,7 @@
     - name: Run
       run: script/test build check
     - name: Codecov
-      uses: codecov/codecov-action@v3
+      uses: codecov/codecov-action@v4
 
   rest-check:
     runs-on: ubuntu-20.04
@@ -85,7 +85,7 @@
       BUILD_TARGET: check
       OTBR_REST: ${{ matrix.rest }}
       OTBR_MDNS: mDNSResponder
-      OTBR_OPTIONS: "-DOTBR_SRP_ADVERTISING_PROXY=ON -DOTBR_DNSSD_DISCOVERY_PROXY=ON"
+      OTBR_OPTIONS: "-DOTBR_SRP_ADVERTISING_PROXY=ON -DOTBR_DNSSD_DISCOVERY_PROXY=ON -DOTBR_TREL=ON"
       OTBR_COVERAGE: 1
     steps:
     - uses: actions/checkout@v4
@@ -96,7 +96,7 @@
     - name: Run
       run: script/test build check
     - name: Codecov
-      uses: codecov/codecov-action@v3
+      uses: codecov/codecov-action@v4
 
   script-check:
     runs-on: ubuntu-20.04
@@ -112,7 +112,7 @@
     - name: Run
       run: tests/scripts/check-scripts
     - name: Codecov
-      uses: codecov/codecov-action@v3
+      uses: codecov/codecov-action@v4
 
   scan-build:
     runs-on: ubuntu-20.04
diff --git a/.github/workflows/macOS.yml b/.github/workflows/macOS.yml
index 46f9893..a99adde 100644
--- a/.github/workflows/macOS.yml
+++ b/.github/workflows/macOS.yml
@@ -49,18 +49,19 @@
         submodules: true
     - name: Bootstrap
       run: |
-        rm -f /usr/local/bin/2to3
-        rm -f /usr/local/bin/2to3-3.11
-        rm -f /usr/local/bin/idle3
-        rm -f /usr/local/bin/idle3.11
-        rm -f /usr/local/bin/pydoc3
-        rm -f /usr/local/bin/pydoc3.11
-        rm -f /usr/local/bin/python3
-        rm -f /usr/local/bin/python3.11
-        rm -f /usr/local/bin/python3-config
-        rm -f /usr/local/bin/python3.11-config
+        rm -f /usr/local/bin/2to3*
+        rm -f /usr/local/bin/idle3*
+        rm -f /usr/local/bin/pydoc3*
+        rm -f /usr/local/bin/python3*
         brew update
         brew reinstall boost cmake cpputest dbus jsoncpp ninja protobuf@21 pkg-config
+        brew upgrade node
     - name: Build
       run: |
-        OTBR_OPTIONS='-DOTBR_BORDER_AGENT=OFF -DOTBR_MDNS=OFF -DOT_FIREWALL=OFF -DOTBR_DBUS=OFF' ./script/test build
+        OTBR_OPTIONS="-DOTBR_BORDER_AGENT=OFF \
+                      -DOTBR_MDNS=OFF \
+                      -DOTBR_ADVERTISING_PROXY=OFF \
+                      -DOTBR_DISCOVERY_PROXY=OFF \
+                      -DOTBR_TREL=OFF \
+                      -DOT_FIREWALL=OFF \
+                      -DOTBR_DBUS=OFF" ./script/test build
diff --git a/.github/workflows/meshcop.yml b/.github/workflows/meshcop.yml
index e3aaae5..6bbc850 100644
--- a/.github/workflows/meshcop.yml
+++ b/.github/workflows/meshcop.yml
@@ -80,4 +80,4 @@
         OTBR_USE_WEB_COMMISSIONER: 1
       run: OTBR_VERBOSE=${RUNNER_DEBUG:-0} script/test meshcop
     - name: Codecov
-      uses: codecov/codecov-action@v3
+      uses: codecov/codecov-action@v4
diff --git a/.gitignore b/.gitignore
index 15c8635..fb15842 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,6 +109,10 @@
 .idea/**
 cmake-build-*/**
 
+# VS Code
+.vscode
+*.code-workspace
+
 # Thread local storage
 ./tmp/
 *.data
diff --git a/Android.bp b/Android.bp
index 4aa2138..f6ba8a5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -27,6 +27,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["external_ot-br-posix_license"],
 }
 
@@ -133,15 +134,27 @@
         "-DOTBR_CONFIG_ANDROID_VERSION_HEADER_ENABLE=1",
         "-DOTBR_CONFIG_FILE=\"src/android/otbr-config-android.h\"",
         "-DOTBR_ENABLE_VENDOR_SERVER=1", // for OtDaemonServer
+        "-DOTBR_ENABLE_BACKBONE_ROUTER=1",
         "-DOTBR_ENABLE_BORDER_ROUTING=1",
+        "-DOTBR_ENABLE_BORDER_ROUTING_COUNTERS=1",
         "-DOTBR_ENABLE_BORDER_AGENT=1",
         "-DOTBR_ENABLE_PUBLISH_MESHCOP_BA_ID=1",
         // Used for bypassing the macro check. In fact mdnssd is not used because we don't compile
         // the related source files.
         "-DOTBR_ENABLE_MDNS_MDNSSD=1",
-        "-DOTBR_ENABLE_SRP_ADVERTISING_PROXY=0",
-        "-DOTBR_ENABLE_DNSSD_DISCOVERY_PROXY=0",
+        "-DOTBR_ENABLE_SRP_ADVERTISING_PROXY=1",
+        "-DOTBR_ENABLE_DNSSD_DISCOVERY_PROXY=1",
+        "-DOTBR_ENABLE_SRP_SERVER_AUTO_ENABLE_MODE=1",
         "-DOTBR_PACKAGE_NAME=\"OTBR_AGENT\"",
+        "-DOTBR_STOP_BORDER_AGENT_ON_INIT=1",
+        // The platform specific rules for selecting infrastructure link do not apply to Android
+        "-DOTBR_ENABLE_VENDOR_INFRA_LINK_SELECT=0",
+
+        // Disable 1.4 features, they are not supported on Android yet.
+        "-DOTBR_ENABLE_NAT64=0",
+        "-DOTBR_ENABLE_DNS_UPSTREAM_QUERY=0",
+        "-DOTBR_ENABLE_DHCP6_PD=0",
+        "-DOTBR_ENABLE_TREL=0",
     ],
 
     srcs: [
@@ -149,6 +162,7 @@
         "src/android/mdns_publisher.cpp",
         "src/android/otdaemon_server.cpp",
         "src/android/otdaemon_telemetry.cpp",
+        "src/backbone_router/backbone_agent.cpp",
         "src/border_agent/border_agent.cpp",
         "src/ncp/ncp_openthread.cpp",
         "src/sdp_proxy/advertising_proxy.cpp",
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0421e78..7724a44 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,6 +9,7 @@
   - [4.1 Initial Setup](#initial-setup)
   - [4.2 Contributor License Agreement (CLA)](#contributor-license-agreement--cla-)
   - [4.3 Submitting a Pull Request](#submitting-a-pull-request)
+- [5 Contributing Documentation](#contributing-documentation)
 
 ## Code of Conduct
 
@@ -37,7 +38,6 @@
 Setup your GitHub fork and continuous-integration services:
 
 1. Fork the [OpenThread repository](https://github.com/openthread/ot-br-posix) by clicking "Fork" on the web UI.
-2. Enable [Travis CI](https://travis-ci.org/) by logging in with your GitHub account and enabling your newly created fork. We use Travis CI for Linux and macOS continuous integration checks. All contributions must pass these checks to be accepted.
 
 Setup your local development environment:
 
@@ -109,7 +109,7 @@
 
 #### Coding Conventions and Style
 
-OpenThread uses and enforces the [OpenThread Coding Conventions and Style](STYLE_GUIDE.md) on all code, except for code located in [third_party](third_party). Use `script/make-pretty` and `script/make-pretty check` to automatically reformat code and check for code-style compliance, respectively. OpenThread currently requires [clang-format v14.0.0](https://releases.llvm.org/download.html#14.0.0) for C/C++ and [yapf](https://github.com/google/yapf) for Python.
+OpenThread uses and enforces the [OpenThread Coding Conventions and Style](STYLE_GUIDE.md) on all code, except for code located in [third_party](third_party). Use `script/make-pretty` and `script/make-pretty check` to automatically reformat code and check for code-style compliance, respectively. OpenThread currently requires [clang-format v14.0.0](https://releases.llvm.org/download.html#14.0.0) for C/C++ and [yapf v0.31.0](https://github.com/google/yapf) for Python.
 
 As part of the cleanup process, you should also run `script/make-pretty check` to ensure that your code passes the baseline code style checks.
 
@@ -123,8 +123,38 @@
 git push origin <branch-name>
 ```
 
-This will trigger the Travis CI continuous-integration checks. You can view the results in the respective services. Note that the integration checks will report failures on occasion. If a failure occurs, you may try rerunning the test via the Travis web UI.
+This will trigger continuous-integration checks using GitHub Actions. You can view the status and logs via the "Actions" tab in your fork.
 
 #### Submit Pull Request
 
-Once you've validated the Travis CI results, go to the page for your fork on GitHub, select your development branch, and click the pull request button. If you need to make any adjustments to your pull request, just push the updates to GitHub. Your pull request will automatically track the changes on your development branch and update.
+Once you've validated that all continuous-integration checks have passed, go to the page for your fork on GitHub, select your development branch, and click the pull request button. If you need to make any adjustments to your pull request, just push the updates to GitHub. Your pull request will automatically track the changes on your development branch and update.
+
+#### Checks fail
+
+Once you've submitted a pull request, all continuous-integration checks are triggered again. If some of these checks fail, it could be either problems with the pull request or an intermittent failure of some test cases. For more information on the failure, check the output and download artifacts. (After all jobs in one group are completed, an `Artifacts` button appears beside the `Re-run` jobs button.) If the failure is intermittent, the check will usually pass after rerunning once or twice.
+
+We want to eliminate intermittent failures as well, so when you experience such a failure, please log an issue and attach any relevant artifacts. If the artifacts are too big, provide the link of the failed run (do not rerun checks again, or it will be overwritten). Alternatively, upload the artifacts to a file-sharing service like Google Drive and share a link to it.
+
+## Contributing Documentation
+
+Documentation undergoes the same review process as code and contributions may be mirrored on our [openthread.io](https://openthread.io) website.
+
+### Codelabs and Guides
+
+To review and contribute to OpenThread Codelabs and Guides, refer to the following GitHub repositories:
+
+- [Codelabs](https://github.com/openthread/ot-docs/tree/main/site/en/codelabs)
+- [Guides](https://github.com/openthread/ot-docs/tree/main/site/en/guides)
+
+For information on how to author and format documentation for contribution, refer to the [Documentation Style Guide](https://github.com/openthread/ot-docs/blob/main/STYLE_GUIDE.md).
+
+### API Reference topics
+
+API Reference topics use [Doxygen comment blocks](https://www.doxygen.nl/manual/docblocks.html) to render the HTML output on [https://openthread.io/reference](https://openthread.io/reference). OpenThread scripts support the following Doxygen [special commands](https://www.doxygen.nl/manual/commands.html):
+
+- @file
+- @brief
+- @param
+- @returns
+
+You can find most of these comments in the [OpenThread header files](https://github.com/openthread/openthread/tree/main/include/openthread). To review an example, refer to [`border_agent.h`](https://github.com/openthread/openthread/tree/main/include/openthread/border_agent.h). The Doxygen comments in `border_agent.h` output the [Border Agent](https://openthread.io/reference/group/api-border-agent) reference topic on openthread.io. For more information, refer to [Comments](https://github.com/openthread/openthread/blob/main/STYLE_GUIDE.md#comments) in the OpenThread Coding Conventions and Style guide.
diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile
index eef7a94..08f160c 100644
--- a/etc/docker/Dockerfile
+++ b/etc/docker/Dockerfile
@@ -78,7 +78,7 @@
   libnetfilter-queue-dev
 
 # Required for OpenThread Backbone CI
-ENV OTBR_OT_BACKBONE_CI_DEPS curl lcov wget build-essential python3-dbus python3-zeroconf
+ENV OTBR_OT_BACKBONE_CI_DEPS curl lcov wget build-essential python3-dbus python3-zeroconf socat
 
 # Required and installed during build (script/bootstrap) when RELEASE=1, could be removed
 ENV OTBR_NORELEASE_DEPS \
diff --git a/src/Android.bp b/src/Android.bp
index d647729..b9a3a54 100644
--- a/src/Android.bp
+++ b/src/Android.bp
@@ -26,6 +26,7 @@
 //  POSSIBILITY OF SUCH DAMAGE.
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["external_ot-br-posix_license"],
 }
 
diff --git a/src/agent/application.cpp b/src/agent/application.cpp
index 3369602..16f4082 100644
--- a/src/agent/application.cpp
+++ b/src/agent/application.cpp
@@ -61,12 +61,24 @@
     , mBackboneInterfaceName(aBackboneInterfaceNames.empty() ? "" : aBackboneInterfaceNames.front())
 #endif
     , mNcp(mInterfaceName.c_str(), aRadioUrls, mBackboneInterfaceName, /* aDryRun */ false, aEnableAutoAttach)
+#if OTBR_ENABLE_MDNS
+    , mPublisher(Mdns::Publisher::Create([this](Mdns::Publisher::State aState) { this->HandleMdnsState(aState); }))
+#endif
 #if OTBR_ENABLE_BORDER_AGENT
-    , mBorderAgent(mNcp)
+    , mBorderAgent(mNcp, *mPublisher)
 #endif
 #if OTBR_ENABLE_BACKBONE_ROUTER
     , mBackboneAgent(mNcp, aInterfaceName, mBackboneInterfaceName)
 #endif
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    , mAdvertisingProxy(mNcp, *mPublisher)
+#endif
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+    , mDiscoveryProxy(mNcp, *mPublisher)
+#endif
+#if OTBR_ENABLE_TREL
+    , mTrelDnssd(mNcp, *mPublisher)
+#endif
 #if OTBR_ENABLE_OPENWRT
     , mUbusAgent(mNcp)
 #endif
@@ -74,7 +86,7 @@
     , mRestWebServer(mNcp, aRestListenAddress, aRestListenPort)
 #endif
 #if OTBR_ENABLE_DBUS_SERVER && OTBR_ENABLE_BORDER_AGENT
-    , mDBusAgent(mNcp, mBorderAgent.GetPublisher())
+    , mDBusAgent(mNcp, *mPublisher)
 #endif
 #if OTBR_ENABLE_VENDOR_SERVER
     , mVendorServer(vendor::VendorServer::newInstance(*this))
@@ -88,12 +100,27 @@
 {
     mNcp.Init();
 
+#if OTBR_ENABLE_MDNS
+    mPublisher->Start();
+#endif
 #if OTBR_ENABLE_BORDER_AGENT
-    mBorderAgent.Init();
+// This is for delaying publishing the MeshCoP service until the correct
+// vendor name and OUI etc. are correctly set by BorderAgent::SetMeshCopServiceValues()
+#if OTBR_STOP_BORDER_AGENT_ON_INIT
+    mBorderAgent.SetEnabled(false);
+#else
+    mBorderAgent.SetEnabled(true);
+#endif
 #endif
 #if OTBR_ENABLE_BACKBONE_ROUTER
     mBackboneAgent.Init();
 #endif
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    mAdvertisingProxy.SetEnabled(true);
+#endif
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+    mDiscoveryProxy.SetEnabled(true);
+#endif
 #if OTBR_ENABLE_OPENWRT
     mUbusAgent.Init();
 #endif
@@ -110,8 +137,17 @@
 
 void Application::Deinit(void)
 {
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    mAdvertisingProxy.SetEnabled(false);
+#endif
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+    mDiscoveryProxy.SetEnabled(false);
+#endif
 #if OTBR_ENABLE_BORDER_AGENT
-    mBorderAgent.Deinit();
+    mBorderAgent.SetEnabled(false);
+#endif
+#if OTBR_ENABLE_MDNS
+    mPublisher->Stop();
 #endif
 
     mNcp.Deinit();
@@ -192,6 +228,24 @@
     return error;
 }
 
+void Application::HandleMdnsState(Mdns::Publisher::State aState)
+{
+    OTBR_UNUSED_VARIABLE(aState);
+
+#if OTBR_ENABLE_BORDER_AGENT
+    mBorderAgent.HandleMdnsState(aState);
+#endif
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    mAdvertisingProxy.HandleMdnsState(aState);
+#endif
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+    mDiscoveryProxy.HandleMdnsState(aState);
+#endif
+#if OTBR_ENABLE_TREL
+    mTrelDnssd.HandleMdnsState(aState);
+#endif
+}
+
 void Application::HandleSignal(int aSignal)
 {
     sShouldTerminate = true;
diff --git a/src/agent/application.hpp b/src/agent/application.hpp
index 55b39ee..2f92d69 100644
--- a/src/agent/application.hpp
+++ b/src/agent/application.hpp
@@ -134,6 +134,18 @@
      */
     Ncp::ControllerOpenThread &GetNcp(void) { return mNcp; }
 
+#if OTBR_ENABLE_MDNS
+    /**
+     * Get the Publisher object the application is using.
+     *
+     * @returns The Publisher object.
+     */
+    Mdns::Publisher &GetPublisher(void)
+    {
+        return *mPublisher;
+    }
+#endif
+
 #if OTBR_ENABLE_BORDER_AGENT
     /**
      * Get the border agent the application is using.
@@ -158,6 +170,42 @@
     }
 #endif
 
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    /**
+     * Get the advertising proxy the application is using.
+     *
+     * @returns The advertising proxy.
+     */
+    AdvertisingProxy &GetAdvertisingProxy(void)
+    {
+        return mAdvertisingProxy;
+    }
+#endif
+
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+    /**
+     * Get the discovery proxy the application is using.
+     *
+     * @returns The discovery proxy.
+     */
+    Dnssd::DiscoveryProxy &GetDiscoveryProxy(void)
+    {
+        return mDiscoveryProxy;
+    }
+#endif
+
+#if OTBR_ENABLE_TREL
+    /**
+     * Get the TrelDnssd object the application is using.
+     *
+     * @returns The TrelDnssd.
+     */
+    TrelDnssd::TrelDnssd &GetTrelDnssd(void)
+    {
+        return mTrelDnssd;
+    }
+#endif
+
 #if OTBR_ENABLE_OPENWRT
     /**
      * Get the UBus agent the application is using.
@@ -194,6 +242,14 @@
     }
 #endif
 
+    /**
+     * This method handles mDNS publisher's state changes.
+     *
+     * @param[in] aState  The state of mDNS publisher.
+     *
+     */
+    void HandleMdnsState(Mdns::Publisher::State aState);
+
 private:
     // Default poll timeout.
     static const struct timeval kPollTimeout;
@@ -206,12 +262,24 @@
 #endif
     const char               *mBackboneInterfaceName;
     Ncp::ControllerOpenThread mNcp;
+#if OTBR_ENABLE_MDNS
+    std::unique_ptr<Mdns::Publisher> mPublisher;
+#endif
 #if OTBR_ENABLE_BORDER_AGENT
     BorderAgent mBorderAgent;
 #endif
 #if OTBR_ENABLE_BACKBONE_ROUTER
     BackboneRouter::BackboneAgent mBackboneAgent;
 #endif
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    AdvertisingProxy mAdvertisingProxy;
+#endif
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+    Dnssd::DiscoveryProxy mDiscoveryProxy;
+#endif
+#if OTBR_ENABLE_TREL
+    TrelDnssd::TrelDnssd mTrelDnssd;
+#endif
 #if OTBR_ENABLE_OPENWRT
     ubus::UBusAgent mUbusAgent;
 #endif
diff --git a/src/agent/main.cpp b/src/agent/main.cpp
index 4ed5e27..776c3e7 100644
--- a/src/agent/main.cpp
+++ b/src/agent/main.cpp
@@ -70,6 +70,7 @@
     OTBR_OPT_HELP                    = 'h',
     OTBR_OPT_INTERFACE_NAME          = 'I',
     OTBR_OPT_VERBOSE                 = 'v',
+    OTBR_OPT_SYSLOG_DISABLE          = 's',
     OTBR_OPT_VERSION                 = 'V',
     OTBR_OPT_SHORTMAX                = 128,
     OTBR_OPT_RADIO_VERSION,
@@ -90,6 +91,7 @@
     {"help", no_argument, nullptr, OTBR_OPT_HELP},
     {"thread-ifname", required_argument, nullptr, OTBR_OPT_INTERFACE_NAME},
     {"verbose", no_argument, nullptr, OTBR_OPT_VERBOSE},
+    {"syslog-disable", no_argument, nullptr, OTBR_OPT_SYSLOG_DISABLE},
     {"version", no_argument, nullptr, OTBR_OPT_VERSION},
     {"radio-version", no_argument, nullptr, OTBR_OPT_RADIO_VERSION},
     {"auto-attach", optional_argument, nullptr, OTBR_OPT_AUTO_ATTACH},
@@ -136,9 +138,10 @@
 static void PrintHelp(const char *aProgramName)
 {
     fprintf(stderr,
-            "Usage: %s [-I interfaceName] [-B backboneIfName] [-d DEBUG_LEVEL] [-v] [--auto-attach[=0/1]] RADIO_URL "
-            "[RADIO_URL]\n"
-            "    --auto-attach defaults to 1\n",
+            "Usage: %s [-I interfaceName] [-B backboneIfName] [-d DEBUG_LEVEL] [-v] [-s] [--auto-attach[=0/1]] "
+            "RADIO_URL [RADIO_URL]\n"
+            "    --auto-attach defaults to 1\n"
+            "    -s disables syslog and prints to standard out\n",
             aProgramName);
     fprintf(stderr, "%s", otSysGetRadioUrlHelpString());
 }
@@ -195,6 +198,7 @@
     int                       ret               = EXIT_SUCCESS;
     const char               *interfaceName     = kDefaultInterfaceName;
     bool                      verbose           = false;
+    bool                      syslogDisable     = false;
     bool                      printRadioVersion = false;
     bool                      enableAutoAttach  = true;
     const char               *restListenAddress = "";
@@ -205,7 +209,7 @@
 
     std::set_new_handler(OnAllocateFailed);
 
-    while ((opt = getopt_long(argc, argv, "B:d:hI:Vv", kOptions, nullptr)) != -1)
+    while ((opt = getopt_long(argc, argv, "B:d:hI:Vvs", kOptions, nullptr)) != -1)
     {
         switch (opt)
         {
@@ -228,6 +232,10 @@
             verbose = true;
             break;
 
+        case OTBR_OPT_SYSLOG_DISABLE:
+            syslogDisable = true;
+            break;
+
         case OTBR_OPT_VERSION:
             PrintVersion();
             ExitNow();
@@ -269,7 +277,7 @@
         }
     }
 
-    otbrLogInit(argv[0], logLevel, verbose);
+    otbrLogInit(argv[0], logLevel, verbose, syslogDisable);
     otbrLogNotice("Running %s", OTBR_PACKAGE_VERSION);
     otbrLogNotice("Thread version: %s", otbr::Ncp::ControllerOpenThread::GetThreadVersion());
     otbrLogNotice("Thread interface: %s", interfaceName);
diff --git a/src/android/aidl/Android.bp b/src/android/aidl/Android.bp
index f33ad8c..bf25b72 100644
--- a/src/android/aidl/Android.bp
+++ b/src/android/aidl/Android.bp
@@ -27,6 +27,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["external_ot-br-posix_license"],
 }
 
@@ -37,6 +38,7 @@
     unstable: true,
     srcs: [
         "com/android/**/*.aidl",
+        ":framework-thread-ot-daemon-shared-aidl-sources",
     ],
     backend: {
         java: {
@@ -61,5 +63,5 @@
         "//external/ot-br-posix:__subpackages__",
         "//packages/modules/ThreadNetwork/service:__subpackages__",
         "//system/tools/aidl:__subpackages__",
-    ]
+    ],
 }
diff --git a/src/android/aidl/com/android/server/thread/openthread/BackboneRouterState.aidl b/src/android/aidl/com/android/server/thread/openthread/BackboneRouterState.aidl
new file mode 100644
index 0000000..22bb3ba
--- /dev/null
+++ b/src/android/aidl/com/android/server/thread/openthread/BackboneRouterState.aidl
@@ -0,0 +1,41 @@
+/*
+ *    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.
+ */
+
+package com.android.server.thread.openthread;
+
+/**
+ * Contains all backbone router states which the system_server and/or client apps care about.
+ */
+parcelable BackboneRouterState {
+    // true when multicast forwarding should be enabled when BackboneRoute is primary, false
+    // otherwise.
+    boolean multicastForwardingEnabled;
+
+    // The list of multicast group address subscribed in Thread network
+    List<String> listeningAddresses;
+}
diff --git a/src/android/aidl/com/android/server/thread/openthread/IChannelMasksReceiver.aidl b/src/android/aidl/com/android/server/thread/openthread/IChannelMasksReceiver.aidl
new file mode 100644
index 0000000..006d4b2
--- /dev/null
+++ b/src/android/aidl/com/android/server/thread/openthread/IChannelMasksReceiver.aidl
@@ -0,0 +1,35 @@
+/*
+ *    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.
+ */
+
+package com.android.server.thread.openthread;
+
+/** Receives the status of an OpenThread operation which may fail with an {@code otError} code. */
+oneway interface IChannelMasksReceiver {
+    void onSuccess(in int supportedChannelMask, in int preferredChannelMask);
+    void onError(int errorCode, String errorMessage);
+}
diff --git a/src/android/aidl/com/android/server/thread/openthread/INsdDiscoverServiceCallback.aidl b/src/android/aidl/com/android/server/thread/openthread/INsdDiscoverServiceCallback.aidl
new file mode 100644
index 0000000..21de2d1
--- /dev/null
+++ b/src/android/aidl/com/android/server/thread/openthread/INsdDiscoverServiceCallback.aidl
@@ -0,0 +1,36 @@
+/*
+ *    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.
+ */
+
+package com.android.server.thread.openthread;
+
+/** Receives the information when a service instance is found/lost. */
+oneway interface INsdDiscoverServiceCallback {
+    void onServiceDiscovered(in String name,
+                             in String type,
+                             boolean isFound);
+}
diff --git a/src/android/aidl/com/android/server/thread/openthread/INsdPublisher.aidl b/src/android/aidl/com/android/server/thread/openthread/INsdPublisher.aidl
index 11ba51c..84549f9 100644
--- a/src/android/aidl/com/android/server/thread/openthread/INsdPublisher.aidl
+++ b/src/android/aidl/com/android/server/thread/openthread/INsdPublisher.aidl
@@ -30,6 +30,8 @@
 
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.INsdStatusReceiver;
+import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
+import com.android.server.thread.openthread.INsdResolveServiceCallback;
 
 /**
  * The service which supports mDNS advertising and discovery by {@link NsdManager}.
@@ -51,7 +53,8 @@
      * @param port the port number of the service
      * @param txt the entries of the TXT record
      * @param receiver the receiver of the register callback
-     * @param listenerId the listener ID of the 'unregister' opreation
+     * @param listenerId the ID of the NsdManager.RegistrationListener which is used to
+     *                             identify the registration
      */
     void registerService(in @nullable String hostname,
                         in String name,
@@ -63,13 +66,88 @@
                         int listenerId);
 
     /**
+     * Registers an mDNS host.
+     *
+     * <p>The listenerId is an integer ID generated by the caller. It's used by the caller and
+     * the NsdPublisher service to uniquely identify a registration.
+     *
+     * @param name the hostname like "my-host"
+     * @param addresses the IPv6 addresses of the host. Each String represents an address.
+     * @param receiver the receiver of the register callback
+     * @param listenerId the ID of the NsdManager.RegistrationListener which is used to
+     *                             identify the registration
+     */
+    void registerHost(in String name,
+                      in List<String> addresses,
+                      in INsdStatusReceiver receiver,
+                      int listenerId);
+
+    /**
      * Unregisters an mDNS service.
      *
      * <p>To unregister a previously registered service/host/key, the caller must pass in the same
      * listener which was used when registering the service/host/key.
      *
      * @param receiver the receiver of the unregister callback
-     * @param listenerId the listenerId of the 'unregister' operation
+     * @param listenerId the ID of the NsdManager.RegistrationListener which is used to
+     *                             identify the registration
      */
     void unregister(in INsdStatusReceiver receiver, int listenerId);
+
+    /** Resets the NsdPublisher, i.e. clear all registrations. */
+    void reset();
+
+    /**
+     * Discovers mDNS services of a specific type.
+     *
+     * <p>To stop discovering services, the caller must pass in the same listener ID which was used
+     * when starting discoverying the services.
+     *
+     * @param type the service type
+     * @param callback the callback when a service is found/lost
+     * @param listenerId the ID of the NsdManager.DiscoveryListener which is used to identify the
+     *                             service discovery operation
+     */
+    void discoverService(in String type,
+                         in INsdDiscoverServiceCallback callback,
+                         int listenerId);
+
+    /**
+     * Stops discovering services of a specific type.
+     *
+     * <p>To stop discovering services, the caller must pass in the same listener ID which was used
+     * when starting discoverying the services.
+     *
+     * @param listenerId the ID of the NsdManager.DiscoveryListener which is used to identify the
+     *                             service discovery operation
+     */
+    void stopServiceDiscovery(int listenerId);
+
+    /**
+     * Resolves an mDNS service instance.
+     *
+     * <p>To stop resolving a service, the caller must pass in the same listener ID which was used
+     * when starting resolving the service.
+     *
+     * @param name the service instance name
+     * @param type the service type
+     * @param callback the callback when a service is updated
+     * @param listenerId the ID of the NsdManager.ServiceInfoCallback which is used to identify the
+     *                             service resolution operation
+     */
+    void resolveService(in String name,
+                        in String type,
+                        in INsdResolveServiceCallback callback,
+                        int listenerId);
+
+    /**
+     * Stops resolving an mDNS service instance.
+     *
+     * <p>To stop resolving a service, the caller must pass in the same listener ID which was used
+     * when starting resolving the service.
+     *
+     * @param listenerId the ID of the NsdManager.ServiceInfoCallback which is used to identify the
+     *                             service resolution operation
+     */
+    void stopServiceResolution(int listenerId);
 }
diff --git a/src/android/aidl/com/android/server/thread/openthread/INsdResolveServiceCallback.aidl b/src/android/aidl/com/android/server/thread/openthread/INsdResolveServiceCallback.aidl
new file mode 100644
index 0000000..48d7b5c
--- /dev/null
+++ b/src/android/aidl/com/android/server/thread/openthread/INsdResolveServiceCallback.aidl
@@ -0,0 +1,42 @@
+/*
+ *    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.
+ */
+
+package com.android.server.thread.openthread;
+
+import com.android.server.thread.openthread.DnsTxtAttribute;
+
+/** Receives the information of a resolved service instance. */
+oneway interface INsdResolveServiceCallback {
+    void onServiceResolved(in String hostname,
+                           in String name,
+                           in String type,
+                           int port,
+                           in List<String> addresses,
+                           in List<DnsTxtAttribute> txt,
+                           int ttlSeconds);
+}
diff --git a/src/android/aidl/com/android/server/thread/openthread/IOtDaemon.aidl b/src/android/aidl/com/android/server/thread/openthread/IOtDaemon.aidl
index e73df90..d07f72a 100644
--- a/src/android/aidl/com/android/server/thread/openthread/IOtDaemon.aidl
+++ b/src/android/aidl/com/android/server/thread/openthread/IOtDaemon.aidl
@@ -30,11 +30,14 @@
 
 import android.os.ParcelFileDescriptor;
 
+import android.net.thread.ChannelMaxPower;
 import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.IChannelMasksReceiver;
 import com.android.server.thread.openthread.Ipv6AddressInfo;
 import com.android.server.thread.openthread.IOtStatusReceiver;
 import com.android.server.thread.openthread.IOtDaemonCallback;
 import com.android.server.thread.openthread.INsdPublisher;
+import com.android.server.thread.openthread.MeshcopTxtAttributes;
 
 /**
  * The OpenThread daemon service which provides access to the core Thread stack for
@@ -54,36 +57,55 @@
     /** Thread radio is being disabled. */
     const int OT_STATE_DISABLING = 2;
 
-    // The error code below MUST be consistent with openthread/include/openthread/error.h
-    // TODO: add a unit test to make sure that values are always match
     enum ErrorCode {
+        // Converts to ThreadNetworkException#ERROR_FAILED_PRECONDITION
+        OT_ERROR_FAILED_PRECONDITION = -3,
+        // Converts to ThreadNetworkException#ERROR_THREAD_DISABLED
         OT_ERROR_THREAD_DISABLED = -2,
+        // Converts to ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL
         // TODO: Add this error code to OpenThread and make sure `otDatasetSetActiveTlvs()` returns
         // this error code when an unsupported channel is provided
         OT_ERROR_UNSUPPORTED_CHANNEL = -1,
 
+        // The error code below MUST be consistent with openthread/include/openthread/error.h
+        // TODO: add a unit test to make sure that values are always match
+
         OT_ERROR_NO_BUFS = 3,
         OT_ERROR_BUSY = 5,
         OT_ERROR_PARSE = 6,
         OT_ERROR_ABORT = 11,
+        OT_ERROR_NOT_IMPLEMENTED = 12,
         OT_ERROR_INVALID_STATE = 13,
-        OT_ERROR_DETACHED = 16,
         OT_ERROR_RESPONSE_TIMEOUT = 28,
         OT_ERROR_REASSEMBLY_TIMEOUT = 30,
         OT_ERROR_REJECTED = 37,
     }
 
     /**
-     * Initializes this service with Thread tunnel interface FD.
+     * Initializes this service.
+     *
+     * <p>This API MUST be called before all other APIs of this interface.
      *
      * @param tunFd the Thread tunnel interface FD which can be used to transmit/receive
      *              packets to/from Thread PAN
      * @param enabled the Thead enabled state from Persistent Settings
      * @param nsdPublisher the INsdPublisher which can be used for mDNS advertisement/discovery
-     *                    on AIL by {@link NsdManager}
+     *                     on AIL by {@link NsdManager}
+     * @param meshcopTxts the MeshCoP TXT values set by the system_server to override the default
+     *                    ones
+     * @param callback the callback for receiving OtDaemonState changes
+     * @param countryCode 2 bytes country code (as defined in ISO 3166) to set
      */
-    void initialize(in ParcelFileDescriptor tunFd, in boolean enabled,
-                    in INsdPublisher nsdPublisher);
+    void initialize(
+            in ParcelFileDescriptor tunFd,
+            in boolean enabled,
+            in INsdPublisher nsdPublisher,
+            in MeshcopTxtAttributes meshcopTxts,
+            in IOtDaemonCallback callback,
+            in String countryCode);
+
+    /** Terminates the ot-daemon process. */
+    void terminate();
 
     /**
      * Enables/disables Thread.
@@ -138,7 +160,7 @@
     /**
      * Sets the country code.
      *
-     * @param countryCode 2 byte country code (as defined in ISO 3166) to set.
+     * @param countryCode 2 bytes country code (as defined in ISO 3166) to set.
      * @param receiver the receiver to receive result of this operation
      */
     oneway void setCountryCode(in String countryCode, in IOtStatusReceiver receiver);
@@ -152,4 +174,21 @@
      */
     oneway void configureBorderRouter(
         in BorderRouterConfigurationParcel brConfig, in IOtStatusReceiver receiver);
+
+    /**
+     * Gets the supported and preferred channel masks.
+     *
+     * @param receiver the receiver to receive result of this operation
+     */
+    void getChannelMasks(in IChannelMasksReceiver receiver);
+
+   /**
+    * Sets the max power of each channel
+    *
+    * @param channelMaxPowers an array of {@code ChannelMaxPower}.
+    * @param receiver the receiver to the receive result of this operation.
+    */
+    void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOtStatusReceiver receiver);
+
+    // TODO: add Border Router APIs
 }
diff --git a/src/android/aidl/com/android/server/thread/openthread/IOtDaemonCallback.aidl b/src/android/aidl/com/android/server/thread/openthread/IOtDaemonCallback.aidl
index 28686b1..afc0795 100644
--- a/src/android/aidl/com/android/server/thread/openthread/IOtDaemonCallback.aidl
+++ b/src/android/aidl/com/android/server/thread/openthread/IOtDaemonCallback.aidl
@@ -28,6 +28,7 @@
 
 package com.android.server.thread.openthread;
 
+import com.android.server.thread.openthread.BackboneRouterState;
 import com.android.server.thread.openthread.Ipv6AddressInfo;
 import com.android.server.thread.openthread.OtDaemonState;
 
@@ -54,14 +55,12 @@
     void onAddressChanged(in Ipv6AddressInfo addressInfo, boolean isAdded);
 
     /**
-     * Called when multicast forwarding listening address has been changed.
+     * Called when backbone router state or multicast forwarding listening addresses has been
+     * changed.
      *
-     * @param address the IPv6 address in bytes which has been updated. This is a multicast
-     *                address registered by multicast listeners
-     * @param isAdded {@code true} if this multicast address is being added;
-     *                Otherwise, this multicast address is being removed
+     * @param bbrState the backbone router state
      */
-    void onMulticastForwardingAddressChanged(in byte[] ipv6Address, boolean isAdded);
+    void onBackboneRouterStateChanged(in BackboneRouterState bbrState);
 
     /**
      * Called when Thread enabled state has changed. Valid values are STATE_* defined in
diff --git a/src/android/aidl/com/android/server/thread/openthread/MeshcopTxtAttributes.aidl b/src/android/aidl/com/android/server/thread/openthread/MeshcopTxtAttributes.aidl
new file mode 100644
index 0000000..3ac1db8
--- /dev/null
+++ b/src/android/aidl/com/android/server/thread/openthread/MeshcopTxtAttributes.aidl
@@ -0,0 +1,57 @@
+/*
+ *    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.
+ */
+
+package com.android.server.thread.openthread;
+
+/**
+ *  A collection of MeshCoP TXT entries that are supplied by Android platform.
+ */
+parcelable MeshcopTxtAttributes {
+    /**
+     * Predefined MeshCoP TXT entry named "mn".
+     *
+     * The length must not exceed 24 UTF-8 bytes.
+     */
+    String modelName;
+
+    /**
+     * Predefined MeshCoP TXT entry named "vn".
+     *
+     * The length must not exceed 24 UTF-8 bytes.
+     */
+    String vendorName;
+
+    /**
+     * Predefined MeshCoP TXT entry named "vo".
+     *
+     * The length must be 3 bytes.
+     */
+    byte[] vendorOui;
+
+    // More vendor-specific (v*) TXT entries can be added here
+}
diff --git a/src/android/aidl/com/android/server/thread/openthread/OtDaemonState.aidl b/src/android/aidl/com/android/server/thread/openthread/OtDaemonState.aidl
index 5d4fbb8..c13a949 100644
--- a/src/android/aidl/com/android/server/thread/openthread/OtDaemonState.aidl
+++ b/src/android/aidl/com/android/server/thread/openthread/OtDaemonState.aidl
@@ -48,6 +48,4 @@
     // Active Oprational Dataset encoded as Thread TLVs. Empty array means the dataset doesn't
     // exist
     byte[] pendingDatasetTlvs;
-
-    boolean multicastForwardingEnabled;
 }
diff --git a/src/android/java/Android.bp b/src/android/java/Android.bp
index 89f7910..d02c726 100644
--- a/src/android/java/Android.bp
+++ b/src/android/java/Android.bp
@@ -27,6 +27,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["external_ot-br-posix_license"],
 }
 
diff --git a/src/android/java/com/android/server/thread/openthread/testing/FakeOtDaemon.java b/src/android/java/com/android/server/thread/openthread/testing/FakeOtDaemon.java
index 56508e8..ce50eba 100644
--- a/src/android/java/com/android/server/thread/openthread/testing/FakeOtDaemon.java
+++ b/src/android/java/com/android/server/thread/openthread/testing/FakeOtDaemon.java
@@ -33,20 +33,25 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.thread.ChannelMaxPower;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 
+import com.android.server.thread.openthread.BackboneRouterState;
 import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.IChannelMasksReceiver;
 import com.android.server.thread.openthread.INsdPublisher;
 import com.android.server.thread.openthread.IOtDaemon;
 import com.android.server.thread.openthread.IOtDaemonCallback;
 import com.android.server.thread.openthread.IOtStatusReceiver;
+import com.android.server.thread.openthread.MeshcopTxtAttributes;
 import com.android.server.thread.openthread.OtDaemonState;
 
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.NoSuchElementException;
 
 /** A fake implementation of the {@link IOtDaemon} AIDL API for testing. */
@@ -59,33 +64,51 @@
     static final int OT_DEVICE_ROLE_CHILD = 2;
     static final int OT_DEVICE_ROLE_ROUTER = 3;
     static final int OT_DEVICE_ROLE_LEADER = 4;
+    static final int OT_ERROR_NONE = 0;
 
     private static final long PROACTIVE_LISTENER_ID = -1;
 
     private final Handler mHandler;
-    private final OtDaemonState mState;
-    private int mThreadEnabled = OT_STATE_ENABLED;
+    private OtDaemonState mState;
+    private BackboneRouterState mBbrState;
+    private boolean mIsInitialized = false;
+    private int mThreadEnabled = OT_STATE_DISABLED;
+    private int mChannelMasksReceiverOtError = OT_ERROR_NONE;
+    private int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
+    private int mPreferredChannelMask = 0;
 
     @Nullable private DeathRecipient mDeathRecipient;
-
     @Nullable private ParcelFileDescriptor mTunFd;
-
-    @NonNull private INsdPublisher mNsdPublisher;
-
+    @Nullable private INsdPublisher mNsdPublisher;
+    @Nullable private MeshcopTxtAttributes mOverriddenMeshcopTxts;
     @Nullable private IOtDaemonCallback mCallback;
-
     @Nullable private Long mCallbackListenerId;
-
     @Nullable private RemoteException mJoinException;
+    @Nullable private String mCountryCode;
 
     public FakeOtDaemon(Handler handler) {
         mHandler = handler;
+        resetStates();
+    }
+
+    private void resetStates() {
         mState = new OtDaemonState();
         mState.isInterfaceUp = false;
+        mState.partitionId = -1;
         mState.deviceRole = OT_DEVICE_ROLE_DISABLED;
         mState.activeDatasetTlvs = new byte[0];
         mState.pendingDatasetTlvs = new byte[0];
-        mState.multicastForwardingEnabled = false;
+        mBbrState = new BackboneRouterState();
+        mBbrState.multicastForwardingEnabled = false;
+        mBbrState.listeningAddresses = new ArrayList<>();
+
+        mTunFd = null;
+        mThreadEnabled = OT_STATE_DISABLED;
+        mNsdPublisher = null;
+        mIsInitialized = false;
+
+        mCallback = null;
+        mCallbackListenerId = null;
     }
 
     @Override
@@ -112,23 +135,47 @@
         return true;
     }
 
-    @Override
-    public void initialize(ParcelFileDescriptor tunFd, boolean enabled, INsdPublisher nsdPublisher)
-            throws RemoteException {
-        mTunFd = tunFd;
-        mThreadEnabled = enabled ? OT_STATE_ENABLED : OT_STATE_DISABLED;
-        mNsdPublisher = nsdPublisher;
+    @Nullable
+    public DeathRecipient getDeathRecipient() {
+        return mDeathRecipient;
     }
 
     @Override
-    public void setThreadEnabled(boolean enabled, IOtStatusReceiver receiver) {
+    public void initialize(
+            ParcelFileDescriptor tunFd,
+            boolean enabled,
+            INsdPublisher nsdPublisher,
+            MeshcopTxtAttributes overriddenMeshcopTxts,
+            IOtDaemonCallback callback,
+            String countryCode)
+            throws RemoteException {
+        mIsInitialized = true;
+        mTunFd = tunFd;
+        mThreadEnabled = enabled ? OT_STATE_ENABLED : OT_STATE_DISABLED;
+        mNsdPublisher = nsdPublisher;
+        mCountryCode = countryCode;
+
+        mOverriddenMeshcopTxts = new MeshcopTxtAttributes();
+        mOverriddenMeshcopTxts.vendorOui = overriddenMeshcopTxts.vendorOui.clone();
+        mOverriddenMeshcopTxts.vendorName = overriddenMeshcopTxts.vendorName;
+        mOverriddenMeshcopTxts.modelName = overriddenMeshcopTxts.modelName;
+
+        registerStateCallback(callback, PROACTIVE_LISTENER_ID);
+    }
+
+    /** Returns {@code true} if {@link initialize} has been called to initialize this object. */
+    public boolean isInitialized() {
+        return mIsInitialized;
+    }
+
+    @Override
+    public void terminate() throws RemoteException {
         mHandler.post(
                 () -> {
-                    mThreadEnabled = enabled ? OT_STATE_ENABLED : OT_STATE_DISABLED;
-                    try {
-                        receiver.onSuccess();
-                    } catch (RemoteException e) {
-                        throw new AssertionError(e);
+                    resetStates();
+                    if (mDeathRecipient != null) {
+                        mDeathRecipient.binderDied();
+                        mDeathRecipient = null;
                     }
                 });
     }
@@ -137,6 +184,14 @@
         return mThreadEnabled;
     }
 
+    public OtDaemonState getState() {
+        return makeCopy(mState);
+    }
+
+    public BackboneRouterState getBackboneRouterState() {
+        return makeCopy(mBbrState);
+    }
+
     /**
      * Returns the Thread TUN interface FD sent to OT daemon or {@code null} if {@link initialize}
      * is never called.
@@ -155,6 +210,33 @@
         return mNsdPublisher;
     }
 
+    /**
+     * Returns the overridden MeshCoP TXT attributes that is to OT daemon or {@code null} if {@link
+     * #initialize} is never called.
+     */
+    @Nullable
+    public MeshcopTxtAttributes getOverriddenMeshcopTxtAttributes() {
+        return mOverriddenMeshcopTxts;
+    }
+
+    @Nullable
+    public IOtDaemonCallback getCallback() {
+        return mCallback;
+    }
+
+    @Override
+    public void setThreadEnabled(boolean enabled, IOtStatusReceiver receiver) {
+        mHandler.post(
+                () -> {
+                    mThreadEnabled = enabled ? OT_STATE_ENABLED : OT_STATE_DISABLED;
+                    try {
+                        receiver.onSuccess();
+                    } catch (RemoteException e) {
+                        throw new AssertionError(e);
+                    }
+                });
+    }
+
     @Override
     public void registerStateCallback(IOtDaemonCallback callback, long listenerId)
             throws RemoteException {
@@ -162,6 +244,7 @@
         mCallbackListenerId = listenerId;
 
         mHandler.post(() -> onStateChanged(mState, mCallbackListenerId));
+        mHandler.post(() -> onBackboneRouterStateChanged(mBbrState));
     }
 
     @Nullable
@@ -169,6 +252,15 @@
         return mCallback;
     }
 
+    /**
+     * Returns the country code sent to OT daemon or {@code null} if {@link #initialize} is never
+     * called.
+     */
+    @Nullable
+    public String getCountryCode() {
+        return mCountryCode;
+    }
+
     @Override
     public void join(byte[] activeDataset, IOtStatusReceiver receiver) throws RemoteException {
         if (mJoinException != null) {
@@ -186,9 +278,10 @@
                 () -> {
                     mState.deviceRole = OT_DEVICE_ROLE_LEADER;
                     mState.activeDatasetTlvs = activeDataset.clone();
-                    mState.multicastForwardingEnabled = true;
+                    mBbrState.multicastForwardingEnabled = true;
 
                     onStateChanged(mState, PROACTIVE_LISTENER_ID);
+                    onBackboneRouterStateChanged(mBbrState);
                     try {
                         receiver.onSuccess();
                     } catch (RemoteException e) {
@@ -198,16 +291,27 @@
                 JOIN_DELAY.toMillis());
     }
 
+    private OtDaemonState makeCopy(OtDaemonState state) {
+        OtDaemonState copyState = new OtDaemonState();
+        copyState.isInterfaceUp = state.isInterfaceUp;
+        copyState.deviceRole = state.deviceRole;
+        copyState.partitionId = state.partitionId;
+        copyState.activeDatasetTlvs = state.activeDatasetTlvs.clone();
+        copyState.pendingDatasetTlvs = state.pendingDatasetTlvs.clone();
+        return copyState;
+    }
+
+    private BackboneRouterState makeCopy(BackboneRouterState state) {
+        BackboneRouterState copyState = new BackboneRouterState();
+        copyState.multicastForwardingEnabled = state.multicastForwardingEnabled;
+        copyState.listeningAddresses = new ArrayList<>(state.listeningAddresses);
+        return copyState;
+    }
+
     private void onStateChanged(OtDaemonState state, long listenerId) {
         try {
-            // Make a copy of mState so that clients won't keep a direct reference to it
-            OtDaemonState copyState = new OtDaemonState();
-            copyState.isInterfaceUp = state.isInterfaceUp;
-            copyState.deviceRole = state.deviceRole;
-            copyState.partitionId = state.partitionId;
-            copyState.activeDatasetTlvs = state.activeDatasetTlvs.clone();
-            copyState.pendingDatasetTlvs = state.pendingDatasetTlvs.clone();
-            copyState.multicastForwardingEnabled = state.multicastForwardingEnabled;
+            // Make a copy of state so that clients won't keep a direct reference to it
+            OtDaemonState copyState = makeCopy(state);
 
             mCallback.onStateChanged(copyState, listenerId);
         } catch (RemoteException e) {
@@ -215,6 +319,17 @@
         }
     }
 
+    private void onBackboneRouterStateChanged(BackboneRouterState state) {
+        try {
+            // Make a copy of state so that clients won't keep a direct reference to it
+            BackboneRouterState copyState = makeCopy(state);
+
+            mCallback.onBackboneRouterStateChanged(copyState);
+        } catch (RemoteException e) {
+            throw new AssertionError(e);
+        }
+    }
+
     /** Sets the {@link RemoteException} which will be thrown from {@link #join}. */
     public void setJoinException(RemoteException exception) {
         mJoinException = exception;
@@ -243,7 +358,39 @@
     @Override
     public void setCountryCode(String countryCode, IOtStatusReceiver receiver)
             throws RemoteException {
+        throw new UnsupportedOperationException("FakeOtDaemon#setCountryCode is not implemented!");
+    }
+
+    @Override
+    public void getChannelMasks(IChannelMasksReceiver receiver) throws RemoteException {
+        mHandler.post(
+                () -> {
+                    try {
+                        if (mChannelMasksReceiverOtError == OT_ERROR_NONE) {
+                            receiver.onSuccess(mSupportedChannelMask, mPreferredChannelMask);
+                        } else {
+                            receiver.onError(
+                                    mChannelMasksReceiverOtError, "Get channel masks failed");
+                        }
+                    } catch (RemoteException e) {
+                        throw new AssertionError(e);
+                    }
+                });
+    }
+
+    public void setChannelMasks(int supportedChannelMask, int preferredChannelMask) {
+        mSupportedChannelMask = supportedChannelMask;
+        mPreferredChannelMask = preferredChannelMask;
+    }
+
+    public void setChannelMasksReceiverOtError(int otError) {
+        mChannelMasksReceiverOtError = otError;
+    }
+
+    @Override
+    public void setChannelMaxPowers(ChannelMaxPower[] channelMaxPowers, IOtStatusReceiver receiver)
+            throws RemoteException {
         throw new UnsupportedOperationException(
-                "FakeOtDaemon#scheduleMigration is not implemented!");
+                "FakeOtDaemon#setChannelTargetPowers is not implemented!");
     }
 }
diff --git a/src/android/mdns_publisher.cpp b/src/android/mdns_publisher.cpp
index ddb0437..6febecc 100644
--- a/src/android/mdns_publisher.cpp
+++ b/src/android/mdns_publisher.cpp
@@ -26,6 +26,8 @@
  *    POSSIBILITY OF SUCH DAMAGE.
  */
 
+#define OTBR_LOG_TAG "MDNS"
+
 #include "android/mdns_publisher.hpp"
 
 namespace otbr {
@@ -36,9 +38,6 @@
 }
 
 namespace Android {
-
-using Status = ::ndk::ScopedAStatus;
-
 otbrError DnsErrorToOtbrErrorImpl(int32_t aError)
 {
     return aError == 0 ? OTBR_ERROR_NONE : OTBR_ERROR_MDNS;
@@ -59,19 +58,69 @@
     return Status::ok();
 }
 
+Status MdnsPublisher::NsdDiscoverServiceCallback::onServiceDiscovered(const std::string &aName,
+                                                                      const std::string &aType,
+                                                                      bool               aIsFound)
+{
+    VerifyOrExit(aIsFound, mSubscription.mPublisher.OnServiceRemoved(0, aType, aName));
+
+    mSubscription.Resolve(aName, aType);
+
+exit:
+    return Status::ok();
+}
+
+Status MdnsPublisher::NsdResolveServiceCallback::onServiceResolved(const std::string                  &aHostname,
+                                                                   const std::string                  &aName,
+                                                                   const std::string                  &aType,
+                                                                   int                                 aPort,
+                                                                   const std::vector<std::string>     &aAddresses,
+                                                                   const std::vector<DnsTxtAttribute> &aTxt,
+                                                                   int                                 aTtlSeconds)
+{
+    DiscoveredInstanceInfo info;
+    TxtList                txtList;
+
+    info.mHostName = aHostname + ".local.";
+    info.mName     = aName;
+    info.mPort     = aPort;
+    info.mTtl      = std::clamp(aTtlSeconds, kMinResolvedTtl, kMaxResolvedTtl);
+    for (const auto &addressStr : aAddresses)
+    {
+        Ip6Address address;
+        int        error = Ip6Address::FromString(addressStr.c_str(), address);
+
+        if (error != OTBR_ERROR_NONE)
+        {
+            otbrLogInfo("Failed to parse resolved IPv6 address: %s", addressStr.c_str());
+            continue;
+        }
+        info.mAddresses.push_back(address);
+    }
+    for (const auto &entry : aTxt)
+    {
+        txtList.emplace_back(entry.name.c_str(), entry.value.data(), entry.value.size());
+    }
+    EncodeTxtData(txtList, info.mTxtData);
+
+    mSubscription.mPublisher.OnServiceResolved(aType, info);
+
+    return Status::ok();
+}
+
 void MdnsPublisher::SetINsdPublisher(std::shared_ptr<INsdPublisher> aINsdPublisher)
 {
     otbrLogInfo("Set INsdPublisher %p", aINsdPublisher.get());
 
-    if (aINsdPublisher)
+    mNsdPublisher = std::move(aINsdPublisher);
+
+    if (mNsdPublisher != nullptr)
     {
-        mNsdPublisher = std::move(aINsdPublisher);
         mStateCallback(Mdns::Publisher::State::kReady);
     }
     else
     {
-        Stop();
-        mNsdPublisher = std::move(aINsdPublisher);
+        mStateCallback(Mdns::Publisher::State::kIdle);
     }
 }
 
@@ -90,6 +139,18 @@
     return ndk::SharedRefBase::make<MdnsPublisher::NsdStatusReceiver>(std::move(aCallback));
 }
 
+std::shared_ptr<MdnsPublisher::NsdDiscoverServiceCallback> CreateNsdDiscoverServiceCallback(
+    MdnsPublisher::ServiceSubscription &aServiceSubscription)
+{
+    return ndk::SharedRefBase::make<MdnsPublisher::NsdDiscoverServiceCallback>(aServiceSubscription);
+}
+
+std::shared_ptr<MdnsPublisher::NsdResolveServiceCallback> CreateNsdResolveServiceCallback(
+    MdnsPublisher::ServiceSubscription &aServiceSubscription)
+{
+    return ndk::SharedRefBase::make<MdnsPublisher::NsdResolveServiceCallback>(aServiceSubscription);
+}
+
 void DieForNotImplemented(const char *aFuncName)
 {
     VerifyOrDie(false, (std::string(aFuncName) + " is not implemented").c_str());
@@ -149,7 +210,7 @@
         static_cast<NsdServiceRegistration *>(FindServiceRegistration(aName, aType));
 
     VerifyOrExit(IsStarted(), std::move(aCallback)(OTBR_ERROR_MDNS));
-    if (mNsdPublisher != nullptr)
+    if (mNsdPublisher == nullptr)
     {
         otbrLogWarning("No platform mDNS implementation registered!");
         ExitNow(std::move(aCallback)(OTBR_ERROR_MDNS));
@@ -167,8 +228,51 @@
                                          const AddressList &aAddresses,
                                          ResultCallback   &&aCallback)
 {
+    int32_t   listenerId = AllocateListenerId();
+    TxtList   txtList;
+    otbrError error = OTBR_ERROR_NONE;
+
+    std::vector<std::string> addressStrings;
+
+    VerifyOrExit(IsStarted(), error = OTBR_ERROR_MDNS);
+    if (mNsdPublisher == nullptr)
+    {
+        otbrLogWarning("No platform mDNS implementation registered!");
+        ExitNow(error = OTBR_ERROR_MDNS);
+    }
+
+    aCallback = HandleDuplicateHostRegistration(aName, aAddresses, std::move(aCallback));
+    VerifyOrExit(!aCallback.IsNull(), error = OTBR_ERROR_INVALID_STATE);
+
+    AddHostRegistration(
+        MakeUnique<NsdHostRegistration>(aName, aAddresses, /* aCallback= */ nullptr, this, listenerId, mNsdPublisher));
+
+    otbrLogInfo("Publishing host %s listener ID = %d", aName.c_str(), listenerId);
+
+    addressStrings.reserve(aAddresses.size());
+    for (const Ip6Address &address : aAddresses)
+    {
+        addressStrings.push_back(address.ToString());
+    }
+
+    if (aAddresses.size())
+    {
+        mNsdPublisher->registerHost(aName, addressStrings, CreateReceiver(std::move(aCallback)), listenerId);
+    }
+    else
+    {
+        // No addresses to register.
+        std::move(aCallback)(OTBR_ERROR_NONE);
+    }
+
+exit:
+    return error;
+}
+
+otbrError MdnsPublisher::PublishKeyImpl(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback)
+{
     OTBR_UNUSED_VARIABLE(aName);
-    OTBR_UNUSED_VARIABLE(aAddresses);
+    OTBR_UNUSED_VARIABLE(aKeyData);
     OTBR_UNUSED_VARIABLE(aCallback);
 
     DieForNotImplemented(__func__);
@@ -178,42 +282,89 @@
 
 void MdnsPublisher::UnpublishHost(const std::string &aName, ResultCallback &&aCallback)
 {
+    NsdHostRegistration *hostRegistration = static_cast<NsdHostRegistration *>(FindHostRegistration(aName));
+
+    VerifyOrExit(IsStarted(), std::move(aCallback)(OTBR_ERROR_MDNS));
+    if (mNsdPublisher == nullptr)
+    {
+        otbrLogWarning("No platform mDNS implementation registered!");
+        ExitNow(std::move(aCallback)(OTBR_ERROR_MDNS));
+    }
+    VerifyOrExit(hostRegistration != nullptr, std::move(aCallback)(OTBR_ERROR_NONE));
+
+    hostRegistration->mUnregisterReceiver = CreateReceiver(std::move(aCallback));
+    RemoveHostRegistration(aName, OTBR_ERROR_NONE);
+
+exit:
+    return;
+}
+
+void MdnsPublisher::UnpublishKey(const std::string &aName, ResultCallback &&aCallback)
+{
     OTBR_UNUSED_VARIABLE(aName);
     OTBR_UNUSED_VARIABLE(aCallback);
 
     DieForNotImplemented(__func__);
-
-    return;
 }
 
 void MdnsPublisher::SubscribeService(const std::string &aType, const std::string &aInstanceName)
 {
-    OTBR_UNUSED_VARIABLE(aType);
-    OTBR_UNUSED_VARIABLE(aInstanceName);
+    auto service = MakeUnique<ServiceSubscription>(aType, aInstanceName, *this, mNsdPublisher);
 
-    DieForNotImplemented(__func__);
+    VerifyOrExit(IsStarted(), otbrLogWarning("No platform mDNS implementation registered!"));
+
+    mServiceSubscriptions.push_back(std::move(service));
+
+    otbrLogInfo("Subscribe service %s.%s (total %zu)", aInstanceName.c_str(), aType.c_str(),
+                mServiceSubscriptions.size());
+
+    if (aInstanceName.empty())
+    {
+        mServiceSubscriptions.back()->Browse();
+    }
+    else
+    {
+        mServiceSubscriptions.back()->Resolve(aInstanceName, aType);
+    }
+exit:
+    return;
 }
 
 void MdnsPublisher::UnsubscribeService(const std::string &aType, const std::string &aInstanceName)
 {
-    OTBR_UNUSED_VARIABLE(aType);
-    OTBR_UNUSED_VARIABLE(aInstanceName);
+    ServiceSubscriptionList::iterator it;
 
-    DieForNotImplemented(__func__);
+    VerifyOrExit(IsStarted());
+
+    it = std::find_if(mServiceSubscriptions.begin(), mServiceSubscriptions.end(),
+                      [&aType, &aInstanceName](const std::unique_ptr<ServiceSubscription> &aService) {
+                          return aService->mType == aType && aService->mName == aInstanceName;
+                      });
+
+    VerifyOrExit(it != mServiceSubscriptions.end(),
+                 otbrLogWarning("The service %s.%s is already unsubscribed.", aInstanceName.c_str(), aType.c_str()));
+
+    {
+        std::unique_ptr<ServiceSubscription> service = std::move(*it);
+
+        mServiceSubscriptions.erase(it);
+    }
+
+    otbrLogInfo("Unsubscribe service %s.%s (left %zu)", aInstanceName.c_str(), aType.c_str(),
+                mServiceSubscriptions.size());
+
+exit:
+    return;
 }
 
 void MdnsPublisher::SubscribeHost(const std::string &aHostName)
 {
     OTBR_UNUSED_VARIABLE(aHostName);
-
-    DieForNotImplemented(__func__);
 }
 
 void MdnsPublisher::UnsubscribeHost(const std::string &aHostName)
 {
     OTBR_UNUSED_VARIABLE(aHostName);
-
-    DieForNotImplemented(__func__);
 }
 
 void MdnsPublisher::OnServiceResolveFailedImpl(const std::string &aType,
@@ -246,7 +397,9 @@
 
 MdnsPublisher::NsdServiceRegistration::~NsdServiceRegistration(void)
 {
-    VerifyOrExit(mPublisher->IsStarted() && mNsdPublisher != nullptr);
+    auto nsdPublisher = mNsdPublisher.lock();
+
+    VerifyOrExit(mPublisher->IsStarted() && nsdPublisher != nullptr);
 
     otbrLogInfo("Unpublishing service %s.%s listener ID = %d", mName.c_str(), mType.c_str(), mListenerId);
 
@@ -255,11 +408,99 @@
         mUnregisterReceiver = CreateReceiver([](int) {});
     }
 
-    mNsdPublisher->unregister(mUnregisterReceiver, mListenerId);
+    nsdPublisher->unregister(mUnregisterReceiver, mListenerId);
 
 exit:
     return;
 }
 
+MdnsPublisher::NsdHostRegistration::~NsdHostRegistration(void)
+{
+    auto nsdPublisher = mNsdPublisher.lock();
+
+    VerifyOrExit(mPublisher->IsStarted() && nsdPublisher != nullptr);
+
+    otbrLogInfo("Unpublishing host %s listener ID = %d", mName.c_str(), mListenerId);
+
+    if (!mUnregisterReceiver)
+    {
+        mUnregisterReceiver = CreateReceiver([](int) {});
+    }
+
+    nsdPublisher->unregister(mUnregisterReceiver, mListenerId);
+
+exit:
+    return;
+}
+
+void MdnsPublisher::ServiceSubscription::Release(void)
+{
+    otbrLogInfo("Browsing service type %s", mType.c_str());
+
+    std::vector<std::string> instanceNames;
+
+    for (const auto &nameAndResolvers : mResolvers)
+    {
+        instanceNames.push_back(nameAndResolvers.first);
+    }
+    for (const auto &name : instanceNames)
+    {
+        RemoveServiceResolver(name);
+    }
+
+    mNsdPublisher->stopServiceDiscovery(mBrowseListenerId);
+}
+
+void MdnsPublisher::ServiceSubscription::Browse(void)
+{
+    VerifyOrExit(mPublisher.IsStarted());
+
+    otbrLogInfo("Browsing service type %s", mType.c_str());
+
+    mNsdPublisher->discoverService(mType, CreateNsdDiscoverServiceCallback(*this), mBrowseListenerId);
+
+exit:
+    return;
+}
+
+void MdnsPublisher::ServiceSubscription::Resolve(const std::string &aName, const std::string &aType)
+{
+    ServiceResolver *resolver = new ServiceResolver(mPublisher.AllocateListenerId(), mNsdPublisher);
+
+    VerifyOrExit(mPublisher.IsStarted());
+
+    otbrLogInfo("Resolving service %s.%s", aName.c_str(), aType.c_str());
+
+    AddServiceResolver(aName, resolver);
+    mNsdPublisher->resolveService(aName, aType, CreateNsdResolveServiceCallback(*this), resolver->mListenerId);
+
+exit:
+    return;
+}
+
+void MdnsPublisher::ServiceSubscription::AddServiceResolver(const std::string &aName, ServiceResolver *aResolver)
+{
+    mResolvers[aName].insert(aResolver);
+}
+void MdnsPublisher::ServiceSubscription::RemoveServiceResolver(const std::string &aName)
+{
+    int numResolvers = 0;
+
+    VerifyOrExit(mResolvers.find(aName) != mResolvers.end());
+
+    numResolvers = mResolvers[aName].size();
+
+    for (auto resolver : mResolvers[aName])
+    {
+        delete resolver;
+    }
+
+    mResolvers.erase(aName);
+
+exit:
+    otbrLogDebug("Removed %d service resolver for instance %s", numResolvers, aName.c_str());
+    return;
+}
+
 } // namespace Android
 } // namespace otbr
diff --git a/src/android/mdns_publisher.hpp b/src/android/mdns_publisher.hpp
index 97840fb..64793dc 100644
--- a/src/android/mdns_publisher.hpp
+++ b/src/android/mdns_publisher.hpp
@@ -31,12 +31,18 @@
 
 #include "mdns/mdns.hpp"
 
+#include <aidl/com/android/server/thread/openthread/BnNsdDiscoverServiceCallback.h>
+#include <aidl/com/android/server/thread/openthread/BnNsdResolveServiceCallback.h>
 #include <aidl/com/android/server/thread/openthread/BnNsdStatusReceiver.h>
 #include <aidl/com/android/server/thread/openthread/DnsTxtAttribute.h>
 #include <aidl/com/android/server/thread/openthread/INsdPublisher.h>
+#include <set>
 
 namespace otbr {
 namespace Android {
+using Status = ::ndk::ScopedAStatus;
+using aidl::com::android::server::thread::openthread::BnNsdDiscoverServiceCallback;
+using aidl::com::android::server::thread::openthread::BnNsdResolveServiceCallback;
 using aidl::com::android::server::thread::openthread::BnNsdStatusReceiver;
 using aidl::com::android::server::thread::openthread::DnsTxtAttribute;
 using aidl::com::android::server::thread::openthread::INsdPublisher;
@@ -45,24 +51,27 @@
 {
 public:
     explicit MdnsPublisher(Publisher::StateCallback aCallback)
+        : mStateCallback(std::move(aCallback))
+        , mNextListenerId(0)
+
     {
-        mNextListenerId = 0;
-        mStateCallback  = std::move(aCallback);
     }
 
     ~MdnsPublisher(void) { Stop(); }
 
-    // In this Publisher implementation, SetINsdPublisher() does the job to start/stop the Publisher. That's because we
-    // want to ensure ot-daemon won't do any mDNS operations when Thread is disabled.
+    /** Sets the INsdPublisher which forwards the mDNS API requests to the NsdManager in system_server. */
     void SetINsdPublisher(std::shared_ptr<INsdPublisher> aINsdPublisher);
 
-    otbrError Start(void) override { return OTBR_ERROR_MDNS; }
+    otbrError Start(void) override { return OTBR_ERROR_NONE; }
 
     void Stop(void) override
     {
         mServiceRegistrations.clear();
         mHostRegistrations.clear();
-        mStateCallback(Mdns::Publisher::State::kIdle);
+        if (mNsdPublisher != nullptr)
+        {
+            mNsdPublisher->reset();
+        }
     }
 
     bool IsStarted(void) const override { return mNsdPublisher != nullptr; }
@@ -71,6 +80,8 @@
 
     void UnpublishHost(const std::string &aName, ResultCallback &&aCallback) override;
 
+    void UnpublishKey(const std::string &aName, ResultCallback &&aCallback) override;
+
     void SubscribeService(const std::string &aType, const std::string &aInstanceName) override;
 
     void UnsubscribeService(const std::string &aType, const std::string &aInstanceName) override;
@@ -87,14 +98,99 @@
         {
         }
 
-        ::ndk::ScopedAStatus onSuccess(void) override;
+        Status onSuccess(void) override;
 
-        ::ndk::ScopedAStatus onError(int aError) override;
+        Status onError(int aError) override;
 
     private:
         Mdns::Publisher::ResultCallback mCallback;
     };
 
+    struct ServiceResolver : private ::NonCopyable
+    {
+        explicit ServiceResolver(int aListenerId, std::shared_ptr<INsdPublisher> aNsdPublisher)
+            : mListenerId(aListenerId)
+            , mNsdPublisher(std::move(aNsdPublisher))
+        {
+        }
+
+        ~ServiceResolver(void)
+        {
+            if (mNsdPublisher)
+            {
+                mNsdPublisher->stopServiceResolution(mListenerId);
+            }
+        }
+
+        int                            mListenerId;
+        std::shared_ptr<INsdPublisher> mNsdPublisher;
+    };
+
+    struct ServiceSubscription : private ::NonCopyable
+    {
+        explicit ServiceSubscription(std::string                    aType,
+                                     std::string                    aName,
+                                     MdnsPublisher                 &aPublisher,
+                                     std::shared_ptr<INsdPublisher> aNsdPublisher)
+            : mType(std::move(aType))
+            , mName(std::move(aName))
+            , mPublisher(aPublisher)
+            , mNsdPublisher(std::move(aNsdPublisher))
+            , mBrowseListenerId(-1)
+        {
+        }
+
+        ~ServiceSubscription(void) { Release(); }
+
+        void Release(void);
+        void Browse(void);
+        void Resolve(const std::string &aName, const std::string &aType);
+        void AddServiceResolver(const std::string &aName, ServiceResolver *aResolver);
+        void RemoveServiceResolver(const std::string &aInstanceName);
+
+        std::string                    mType;
+        std::string                    mName;
+        MdnsPublisher                 &mPublisher;
+        std::shared_ptr<INsdPublisher> mNsdPublisher;
+        int32_t                        mBrowseListenerId;
+
+        std::map<std::string, std::set<ServiceResolver *>> mResolvers;
+    };
+
+    class NsdDiscoverServiceCallback : public BnNsdDiscoverServiceCallback
+    {
+    public:
+        explicit NsdDiscoverServiceCallback(ServiceSubscription &aSubscription)
+            : mSubscription(aSubscription)
+        {
+        }
+
+        Status onServiceDiscovered(const std::string &aName, const std::string &aType, bool aIsFound);
+
+    private:
+        ServiceSubscription &mSubscription;
+    };
+
+    class NsdResolveServiceCallback : public BnNsdResolveServiceCallback
+    {
+    public:
+        explicit NsdResolveServiceCallback(ServiceSubscription &aSubscription)
+            : mSubscription(aSubscription)
+        {
+        }
+
+        Status onServiceResolved(const std::string                  &aHostname,
+                                 const std::string                  &aName,
+                                 const std::string                  &aType,
+                                 int                                 aPort,
+                                 const std::vector<std::string>     &aAddresses,
+                                 const std::vector<DnsTxtAttribute> &aTxt,
+                                 int                                 aTtlSeconds);
+
+    private:
+        ServiceSubscription &mSubscription;
+    };
+
 protected:
     otbrError PublishServiceImpl(const std::string &aHostName,
                                  const std::string &aName,
@@ -106,6 +202,8 @@
 
     otbrError PublishHostImpl(const std::string &aName, const AddressList &aAddresses, ResultCallback &&aCallback);
 
+    otbrError PublishKeyImpl(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback) override;
+
     void OnServiceResolveFailedImpl(const std::string &aType, const std::string &aInstanceName, int32_t aErrorCode);
 
     void OnHostResolveFailedImpl(const std::string &aHostName, int32_t aErrorCode);
@@ -116,16 +214,16 @@
     class NsdServiceRegistration : public ServiceRegistration
     {
     public:
-        NsdServiceRegistration(const std::string             &aHostName,
-                               const std::string             &aName,
-                               const std::string             &aType,
-                               const SubTypeList             &aSubTypeList,
-                               uint16_t                       aPort,
-                               const TxtData                 &aTxtData,
-                               ResultCallback               &&aCallback,
-                               MdnsPublisher                 *aPublisher,
-                               int32_t                        aListenerId,
-                               std::shared_ptr<INsdPublisher> aINsdPublisher)
+        NsdServiceRegistration(const std::string           &aHostName,
+                               const std::string           &aName,
+                               const std::string           &aType,
+                               const SubTypeList           &aSubTypeList,
+                               uint16_t                     aPort,
+                               const TxtData               &aTxtData,
+                               ResultCallback             &&aCallback,
+                               MdnsPublisher               *aPublisher,
+                               int32_t                      aListenerId,
+                               std::weak_ptr<INsdPublisher> aINsdPublisher)
             : ServiceRegistration(aHostName,
                                   aName,
                                   aType,
@@ -146,14 +244,44 @@
         std::shared_ptr<NsdStatusReceiver> mUnregisterReceiver;
 
     private:
-        std::shared_ptr<INsdPublisher> mNsdPublisher;
+        std::weak_ptr<INsdPublisher> mNsdPublisher;
     };
 
+    class NsdHostRegistration : public HostRegistration
+    {
+    public:
+        NsdHostRegistration(const std::string           &aName,
+                            const AddressList           &aAddresses,
+                            ResultCallback             &&aCallback,
+                            MdnsPublisher               *aPublisher,
+                            int32_t                      aListenerId,
+                            std::weak_ptr<INsdPublisher> aINsdPublisher)
+            : HostRegistration(aName, aAddresses, std::move(aCallback), aPublisher)
+            , mListenerId(aListenerId)
+            , mNsdPublisher(aINsdPublisher)
+        {
+        }
+
+        ~NsdHostRegistration(void) override;
+
+        const int32_t                      mListenerId;
+        std::shared_ptr<NsdStatusReceiver> mUnregisterReceiver;
+
+    private:
+        std::weak_ptr<INsdPublisher> mNsdPublisher;
+    };
+
+    typedef std::vector<std::unique_ptr<ServiceSubscription>> ServiceSubscriptionList;
+
+    static constexpr int kMinResolvedTtl = 1;
+    static constexpr int kMaxResolvedTtl = 10;
+
     int32_t AllocateListenerId(void);
 
     StateCallback                  mStateCallback;
     int32_t                        mNextListenerId;
     std::shared_ptr<INsdPublisher> mNsdPublisher = nullptr;
+    ServiceSubscriptionList        mServiceSubscriptions;
 };
 
 } // namespace Android
diff --git a/src/android/otdaemon_fuzzer.cpp b/src/android/otdaemon_fuzzer.cpp
index be0bc34..9eb3235 100644
--- a/src/android/otdaemon_fuzzer.cpp
+++ b/src/android/otdaemon_fuzzer.cpp
@@ -41,3 +41,7 @@
     fuzzService(service->asBinder().get(), FuzzedDataProvider(data, size));
     return 0;
 }
+
+extern "C" void otPlatReset(otInstance *aInstance)
+{
+}
diff --git a/src/android/otdaemon_server.cpp b/src/android/otdaemon_server.cpp
index 54e3d9a..9ffeb7b 100644
--- a/src/android/otdaemon_server.cpp
+++ b/src/android/otdaemon_server.cpp
@@ -34,14 +34,17 @@
 #include <string.h>
 
 #include <android-base/file.h>
+#include <android-base/stringprintf.h>
 #include <android/binder_manager.h>
 #include <android/binder_process.h>
 #include <openthread/border_router.h>
+#include <openthread/cli.h>
 #include <openthread/icmp6.h>
 #include <openthread/ip6.h>
 #include <openthread/link.h>
 #include <openthread/openthread-system.h>
 #include <openthread/platform/infra_if.h>
+#include <openthread/platform/radio.h>
 
 #include "agent/vendor.hpp"
 #include "android/otdaemon_telemetry.hpp"
@@ -97,10 +100,27 @@
     return addrInfo;
 }
 
+static const char *ThreadEnabledStateToString(int enabledState)
+{
+    switch (enabledState)
+    {
+    case IOtDaemon::OT_STATE_ENABLED:
+        return "ENABLED";
+    case IOtDaemon::OT_STATE_DISABLED:
+        return "DISABLED";
+    case IOtDaemon::OT_STATE_DISABLING:
+        return "DISABLING";
+    default:
+        assert(false);
+        return "UNKNOWN";
+    }
+}
+
 OtDaemonServer::OtDaemonServer(Application &aApplication)
-    : mNcp(aApplication.GetNcp())
+    : mApplication(aApplication)
+    , mNcp(aApplication.GetNcp())
     , mBorderAgent(aApplication.GetBorderAgent())
-    , mMdnsPublisher(static_cast<MdnsPublisher &>(aApplication.GetBorderAgent().GetPublisher()))
+    , mMdnsPublisher(static_cast<MdnsPublisher &>(aApplication.GetPublisher()))
     , mBorderRouterConfiguration()
 {
     mClientDeathRecipient =
@@ -131,7 +151,14 @@
 {
     OtDaemonServer *thisServer = static_cast<OtDaemonServer *>(aBinderServer);
 
-    otbrLogCrit("Client is died, removing callbacks...");
+    otbrLogCrit("system_server is dead, removing configs and callbacks...");
+
+    thisServer->mMeshcopTxts   = {};
+    thisServer->mINsdPublisher = nullptr;
+
+    // Note that the INsdPublisher reference is held in MdnsPublisher
+    thisServer->mMdnsPublisher.SetINsdPublisher(nullptr);
+
     thisServer->mCallback = nullptr;
     thisServer->mTunFd.set(-1); // the original FD will be closed automatically
 }
@@ -151,6 +178,17 @@
             mCallback->onStateChanged(mState, -1);
         }
     }
+    if (aFlags & OT_CHANGED_THREAD_BACKBONE_ROUTER_STATE)
+    {
+        if (mCallback == nullptr)
+        {
+            otbrLogWarning("Ignoring OT backbone router state changes: callback is not set");
+        }
+        else
+        {
+            mCallback->onBackboneRouterStateChanged(GetBackboneRouterState());
+        }
+    }
 }
 
 void OtDaemonServer::AddressCallback(const otIp6AddressInfo *aAddressInfo, bool aIsAdded, void *aBinderServer)
@@ -232,8 +270,9 @@
 
     message = otIp6NewMessage(GetOtInstance(), &settings);
     VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);
+    otMessageSetOrigin(message, OT_MESSAGE_ORIGIN_HOST_UNTRUSTED);
 
-    SuccessOrExit(error = otMessageAppend(message, packet, length));
+    SuccessOrExit(error = otMessageAppend(message, packet, static_cast<uint16_t>(length)));
 
     error   = otIp6Send(GetOtInstance(), message);
     message = nullptr;
@@ -257,41 +296,62 @@
     }
 }
 
+BackboneRouterState OtDaemonServer::GetBackboneRouterState()
+{
+    BackboneRouterState                       state;
+    otBackboneRouterState                     bbrState;
+    otBackboneRouterMulticastListenerInfo     info;
+    otBackboneRouterMulticastListenerIterator iter = OT_BACKBONE_ROUTER_MULTICAST_LISTENER_ITERATOR_INIT;
+    state.listeningAddresses                       = std::vector<std::string>();
+
+    VerifyOrExit(GetOtInstance() != nullptr, otbrLogWarning("Can't get bbr state: OT is not initialized"));
+
+    bbrState = otBackboneRouterGetState(GetOtInstance());
+    switch (bbrState)
+    {
+    case OT_BACKBONE_ROUTER_STATE_DISABLED:
+    case OT_BACKBONE_ROUTER_STATE_SECONDARY:
+        state.multicastForwardingEnabled = false;
+        break;
+    case OT_BACKBONE_ROUTER_STATE_PRIMARY:
+        state.multicastForwardingEnabled = true;
+        break;
+    }
+    otbrLogInfo("Updating backbone router state (bbr state = %d)", bbrState);
+
+    while (otBackboneRouterMulticastListenerGetNext(GetOtInstance(), &iter, &info) == OT_ERROR_NONE)
+    {
+        char string[OT_IP6_ADDRESS_STRING_SIZE];
+
+        otIp6AddressToString(&info.mAddress, string, sizeof(string));
+        state.listeningAddresses.push_back(string);
+    }
+
+exit:
+    return state;
+}
+
 void OtDaemonServer::HandleBackboneMulticastListenerEvent(void                                  *aBinderServer,
                                                           otBackboneRouterMulticastListenerEvent aEvent,
                                                           const otIp6Address                    *aAddress)
 {
     OtDaemonServer *thisServer = static_cast<OtDaemonServer *>(aBinderServer);
-
-    bool                 isAdded;
-    std::vector<uint8_t> addressBytes(aAddress->mFields.m8, BYTE_ARR_END(aAddress->mFields.m8));
-    char                 addressString[OT_IP6_ADDRESS_STRING_SIZE];
+    char            addressString[OT_IP6_ADDRESS_STRING_SIZE];
 
     otIp6AddressToString(aAddress, addressString, sizeof(addressString));
 
-    switch (aEvent)
-    {
-    case OT_BACKBONE_ROUTER_MULTICAST_LISTENER_ADDED:
-        isAdded = true;
-        break;
-    case OT_BACKBONE_ROUTER_MULTICAST_LISTENER_REMOVED:
-        isAdded = false;
-        break;
-    default:
-        otbrLogErr("Got BackboneMulticastListenerEvent with unsupported event: %d", aEvent);
-        assert(false);
-    }
+    otbrLogDebug("Multicast forwarding address changed, %s is %s", addressString,
+                 (aEvent == OT_BACKBONE_ROUTER_MULTICAST_LISTENER_ADDED) ? "added" : "removed");
 
-    otbrLogDebug("Multicast forwarding address changed, %s is %s", addressString, isAdded ? "added" : "removed");
+    if (thisServer->mCallback == nullptr)
+    {
+        otbrLogWarning("Ignoring OT multicast listener event: callback is not set");
+        ExitNow();
+    }
+    thisServer->mCallback->onBackboneRouterStateChanged(thisServer->GetBackboneRouterState());
 
-    if (thisServer->mCallback != nullptr)
-    {
-        thisServer->mCallback->onMulticastForwardingAddressChanged(addressBytes, isAdded);
-    }
-    else
-    {
-        otbrLogWarning("OT daemon callback is not set");
-    }
+exit:
+    return;
 }
 
 otInstance *OtDaemonServer::GetOtInstance()
@@ -320,14 +380,45 @@
     }
 }
 
-Status OtDaemonServer::initialize(const ScopedFileDescriptor           &aTunFd,
-                                  const bool                            enabled,
-                                  const std::shared_ptr<INsdPublisher> &aINsdPublisher)
+Status OtDaemonServer::initialize(const ScopedFileDescriptor               &aTunFd,
+                                  const bool                                enabled,
+                                  const std::shared_ptr<INsdPublisher>     &aINsdPublisher,
+                                  const MeshcopTxtAttributes               &aMeshcopTxts,
+                                  const std::shared_ptr<IOtDaemonCallback> &aCallback,
+                                  const std::string                        &aCountryCode)
 {
-    otbrLogInfo("OT daemon is initialized by system server (tunFd=%d, enabled=%s)",
-            aTunFd.get(), enabled ? "true" : "false");
+    otbrLogInfo("OT daemon is initialized by system server (tunFd=%d, enabled=%s)", aTunFd.get(),
+                enabled ? "true" : "false");
+    // The copy constructor of `ScopedFileDescriptor` is deleted. It is unable to pass the `aTunFd`
+    // to the lambda function. The processing method of `aTunFd` doesn't call OpenThread functions,
+    // we can process `aTunFd` directly in front of the task.
     mTunFd = aTunFd.dup();
+
     mINsdPublisher = aINsdPublisher;
+    mMeshcopTxts   = aMeshcopTxts;
+
+    mTaskRunner.Post([enabled, aINsdPublisher, aMeshcopTxts, aCallback, aCountryCode, this]() {
+        initializeInternal(enabled, mINsdPublisher, mMeshcopTxts, aCallback, aCountryCode);
+    });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::initializeInternal(const bool                                enabled,
+                                        const std::shared_ptr<INsdPublisher>     &aINsdPublisher,
+                                        const MeshcopTxtAttributes               &aMeshcopTxts,
+                                        const std::shared_ptr<IOtDaemonCallback> &aCallback,
+                                        const std::string                        &aCountryCode)
+{
+    std::string instanceName = aMeshcopTxts.vendorName + " " + aMeshcopTxts.modelName;
+
+    setCountryCodeInternal(aCountryCode, nullptr /* aReceiver */);
+    registerStateCallbackInternal(aCallback, -1 /* listenerId */);
+
+    mMdnsPublisher.SetINsdPublisher(aINsdPublisher);
+    mBorderAgent.SetMeshCopServiceValues(instanceName, aMeshcopTxts.modelName, aMeshcopTxts.vendorName,
+                                         aMeshcopTxts.vendorOui);
+    mBorderAgent.SetEnabled(enabled);
 
     if (enabled)
     {
@@ -335,29 +426,41 @@
     }
     else
     {
-        updateThreadEnabledState(enabled, nullptr /* Receiver */);
+        updateThreadEnabledState(OT_STATE_DISABLED, nullptr /* aReceiver */);
     }
+}
 
+Status OtDaemonServer::terminate(void)
+{
+    mTaskRunner.Post([]() {
+        otbrLogWarning("Terminating ot-daemon process...");
+        exit(0);
+    });
     return Status::ok();
 }
 
 void OtDaemonServer::updateThreadEnabledState(const int enabled, const std::shared_ptr<IOtStatusReceiver> &aReceiver)
 {
+    VerifyOrExit(enabled != mThreadEnabled);
+
+    otbrLogInfo("Thread enabled state changed: %s -> %s", ThreadEnabledStateToString(mThreadEnabled),
+                ThreadEnabledStateToString(enabled));
     mThreadEnabled = enabled;
+
     if (aReceiver != nullptr)
     {
         aReceiver->onSuccess();
     }
 
+    // Enables the BorderAgent module only when Thread is enabled because it always
+    // publishes the MeshCoP service even when no Thread network is provisioned.
     switch (enabled)
     {
     case OT_STATE_ENABLED:
-        mMdnsPublisher.SetINsdPublisher(mINsdPublisher);
+        mBorderAgent.SetEnabled(true);
         break;
     case OT_STATE_DISABLED:
-        mMdnsPublisher.SetINsdPublisher(nullptr);
-        break;
-    default:
+        mBorderAgent.SetEnabled(false);
         break;
     }
 
@@ -365,6 +468,9 @@
     {
         mCallback->onThreadEnabledChanged(mThreadEnabled);
     }
+
+exit:
+    return;
 }
 
 void OtDaemonServer::enableThread(const std::shared_ptr<IOtStatusReceiver> &aReceiver)
@@ -377,20 +483,26 @@
         (void)otIp6SetEnabled(GetOtInstance(), true);
         (void)otThreadSetEnabled(GetOtInstance(), true);
     }
-    updateThreadEnabledState(IOtDaemon::OT_STATE_ENABLED, aReceiver);
+    updateThreadEnabledState(OT_STATE_ENABLED, aReceiver);
 }
 
 Status OtDaemonServer::setThreadEnabled(const bool enabled, const std::shared_ptr<IOtStatusReceiver> &aReceiver)
 {
+    mTaskRunner.Post([enabled, aReceiver, this]() { setThreadEnabledInternal(enabled, aReceiver); });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::setThreadEnabledInternal(const bool enabled, const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
     int         error = OT_ERROR_NONE;
     std::string message;
 
     VerifyOrExit(GetOtInstance() != nullptr, error = OT_ERROR_INVALID_STATE, message = "OT is not initialized");
 
-    VerifyOrExit(mThreadEnabled != IOtDaemon::OT_STATE_DISABLING, error = OT_ERROR_BUSY,
-                 message = "Thread is disabling");
+    VerifyOrExit(mThreadEnabled != OT_STATE_DISABLING, error = OT_ERROR_BUSY, message = "Thread is disabling");
 
-    if ((mThreadEnabled == IOtDaemon::OT_STATE_ENABLED) == enabled)
+    if ((mThreadEnabled == OT_STATE_ENABLED) == enabled)
     {
         aReceiver->onSuccess();
         ExitNow();
@@ -402,17 +514,14 @@
     }
     else
     {
-        mThreadEnabled = IOtDaemon::OT_STATE_DISABLING;
-        if (mCallback != nullptr)
-        {
-            mCallback->onThreadEnabledChanged(mThreadEnabled);
-        }
+        // `aReceiver` should not be set here because the operation isn't finished yet
+        updateThreadEnabledState(OT_STATE_DISABLING, nullptr /* aReceiver */);
 
         LeaveGracefully([aReceiver, this]() {
             // Ignore errors as those operations should always succeed
             (void)otThreadSetEnabled(GetOtInstance(), false);
             (void)otIp6SetEnabled(GetOtInstance(), false);
-            updateThreadEnabledState(IOtDaemon::OT_STATE_DISABLED, aReceiver);
+            updateThreadEnabledState(OT_STATE_DISABLED, aReceiver);
         });
     }
 
@@ -421,11 +530,18 @@
     {
         PropagateResult(error, message, aReceiver);
     }
-    return Status::ok();
 }
 
 Status OtDaemonServer::registerStateCallback(const std::shared_ptr<IOtDaemonCallback> &aCallback, int64_t listenerId)
 {
+    mTaskRunner.Post([aCallback, listenerId, this]() { registerStateCallbackInternal(aCallback, listenerId); });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::registerStateCallbackInternal(const std::shared_ptr<IOtDaemonCallback> &aCallback,
+                                                   int64_t                                   listenerId)
+{
     VerifyOrExit(GetOtInstance() != nullptr, otbrLogWarning("OT is not initialized"));
 
     mCallback = aCallback;
@@ -439,9 +555,10 @@
     RefreshOtDaemonState(/* aFlags */ 0xffffffff);
     mCallback->onStateChanged(mState, listenerId);
     mCallback->onThreadEnabledChanged(mThreadEnabled);
+    mCallback->onBackboneRouterStateChanged(GetBackboneRouterState());
 
 exit:
-    return Status::ok();
+    return;
 }
 
 bool OtDaemonServer::RefreshOtDaemonState(otChangedFlags aFlags)
@@ -494,25 +611,9 @@
         haveUpdates = true;
     }
 
-    if (aFlags & OT_CHANGED_THREAD_BACKBONE_ROUTER_STATE)
-    {
-        otBackboneRouterState state = otBackboneRouterGetState(GetOtInstance());
-
-        switch (state)
-        {
-        case OT_BACKBONE_ROUTER_STATE_DISABLED:
-        case OT_BACKBONE_ROUTER_STATE_SECONDARY:
-            mState.multicastForwardingEnabled = false;
-            break;
-        case OT_BACKBONE_ROUTER_STATE_PRIMARY:
-            mState.multicastForwardingEnabled = true;
-            break;
-        }
-        haveUpdates = true;
-    }
-
     if (isAttached() && !mState.activeDatasetTlvs.empty() && mJoinReceiver != nullptr)
     {
+        otbrLogInfo("Join succeeded");
         mJoinReceiver->onSuccess();
         mJoinReceiver = nullptr;
     }
@@ -523,15 +624,23 @@
 Status OtDaemonServer::join(const std::vector<uint8_t>               &aActiveOpDatasetTlvs,
                             const std::shared_ptr<IOtStatusReceiver> &aReceiver)
 {
+    mTaskRunner.Post([aActiveOpDatasetTlvs, aReceiver, this]() { joinInternal(aActiveOpDatasetTlvs, aReceiver); });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::joinInternal(const std::vector<uint8_t>               &aActiveOpDatasetTlvs,
+                                  const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
     int                      error = OT_ERROR_NONE;
     std::string              message;
     otOperationalDatasetTlvs datasetTlvs;
 
-    VerifyOrExit(mThreadEnabled != IOtDaemon::OT_STATE_DISABLING, error = OT_ERROR_BUSY,
-                 message = "Thread is disabling");
+    VerifyOrExit(mThreadEnabled != OT_STATE_DISABLING, error = OT_ERROR_BUSY, message = "Thread is disabling");
 
-    VerifyOrExit(mThreadEnabled == IOtDaemon::OT_STATE_ENABLED,
-                 error = (int)IOtDaemon::ErrorCode::OT_ERROR_THREAD_DISABLED, message = "Thread is disabled");
+    VerifyOrExit(mThreadEnabled == OT_STATE_ENABLED,
+                 error   = static_cast<int>(IOtDaemon::ErrorCode::OT_ERROR_THREAD_DISABLED),
+                 message = "Thread is disabled");
 
     otbrLogInfo("Start joining...");
 
@@ -539,12 +648,15 @@
 
     if (otThreadGetDeviceRole(GetOtInstance()) != OT_DEVICE_ROLE_DISABLED)
     {
-        LeaveGracefully([aActiveOpDatasetTlvs, aReceiver, this]() { join(aActiveOpDatasetTlvs, aReceiver); });
+        LeaveGracefully([aActiveOpDatasetTlvs, aReceiver, this]() {
+            FinishLeave(nullptr);
+            join(aActiveOpDatasetTlvs, aReceiver);
+        });
         ExitNow();
     }
 
     std::copy(aActiveOpDatasetTlvs.begin(), aActiveOpDatasetTlvs.end(), datasetTlvs.mTlvs);
-    datasetTlvs.mLength = aActiveOpDatasetTlvs.size();
+    datasetTlvs.mLength = static_cast<uint8_t>(aActiveOpDatasetTlvs.size());
     SuccessOrExit(error   = otDatasetSetActiveTlvs(GetOtInstance(), &datasetTlvs),
                   message = "Failed to set Active Operational Dataset");
 
@@ -566,37 +678,48 @@
     {
         PropagateResult(error, message, aReceiver);
     }
-    return Status::ok();
 }
 
 Status OtDaemonServer::leave(const std::shared_ptr<IOtStatusReceiver> &aReceiver)
 {
+    mTaskRunner.Post([aReceiver, this]() { leaveInternal(aReceiver); });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::leaveInternal(const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
     std::string message;
     int         error = OT_ERROR_NONE;
 
     VerifyOrExit(GetOtInstance() != nullptr, error = OT_ERROR_INVALID_STATE, message = "OT is not initialized");
 
-    VerifyOrExit(mThreadEnabled != IOtDaemon::OT_STATE_DISABLING, error = OT_ERROR_BUSY,
-                 message = "Thread is disabling");
+    VerifyOrExit(mThreadEnabled != OT_STATE_DISABLING, error = OT_ERROR_BUSY, message = "Thread is disabling");
 
-    if (mThreadEnabled == IOtDaemon::OT_STATE_DISABLED)
+    if (mThreadEnabled == OT_STATE_DISABLED)
     {
-        (void)otInstanceErasePersistentInfo(GetOtInstance());
-        aReceiver->onSuccess();
+        FinishLeave(aReceiver);
         ExitNow();
     }
 
-    LeaveGracefully([aReceiver, this]() {
-        (void)otInstanceErasePersistentInfo(GetOtInstance());
-        aReceiver->onSuccess();
-    });
+    LeaveGracefully([aReceiver, this]() { FinishLeave(aReceiver); });
 
 exit:
     if (error != OT_ERROR_NONE)
     {
         PropagateResult(error, message, aReceiver);
     }
-    return Status::ok();
+}
+
+void OtDaemonServer::FinishLeave(const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
+    (void)otInstanceErasePersistentInfo(GetOtInstance());
+    OT_UNUSED_VARIABLE(mApplication); // Avoid the unused-private-field issue.
+    // TODO: b/323301831 - Re-init the Application class.
+    if (aReceiver != nullptr)
+    {
+        aReceiver->onSuccess();
+    }
 }
 
 void OtDaemonServer::LeaveGracefully(const LeaveCallback &aReceiver)
@@ -646,32 +769,40 @@
 Status OtDaemonServer::scheduleMigration(const std::vector<uint8_t>               &aPendingOpDatasetTlvs,
                                          const std::shared_ptr<IOtStatusReceiver> &aReceiver)
 {
-    int                  error;
+    mTaskRunner.Post(
+        [aPendingOpDatasetTlvs, aReceiver, this]() { scheduleMigrationInternal(aPendingOpDatasetTlvs, aReceiver); });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::scheduleMigrationInternal(const std::vector<uint8_t>               &aPendingOpDatasetTlvs,
+                                               const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
+    int                  error = OT_ERROR_NONE;
     std::string          message;
     otOperationalDataset emptyDataset;
 
-    VerifyOrExit(mThreadEnabled != IOtDaemon::OT_STATE_DISABLING, error = OT_ERROR_BUSY,
-                 message = "Thread is disabling");
+    VerifyOrExit(mThreadEnabled != OT_STATE_DISABLING, error = OT_ERROR_BUSY, message = "Thread is disabling");
 
-    VerifyOrExit(mThreadEnabled == IOtDaemon::OT_STATE_ENABLED,
-                 error = (int)IOtDaemon::ErrorCode::OT_ERROR_THREAD_DISABLED, message = "Thread is disabled");
+    VerifyOrExit(mThreadEnabled == OT_STATE_ENABLED,
+                 error   = static_cast<int>(IOtDaemon::ErrorCode::OT_ERROR_THREAD_DISABLED),
+                 message = "Thread is disabled");
 
     if (GetOtInstance() == nullptr)
     {
         message = "OT is not initialized";
         ExitNow(error = OT_ERROR_INVALID_STATE);
     }
-
     if (!isAttached())
     {
         message = "Cannot schedule migration when this device is detached";
-        ExitNow(error = OT_ERROR_INVALID_STATE);
+        ExitNow(error = static_cast<int>(IOtDaemon::ErrorCode::OT_ERROR_FAILED_PRECONDITION));
     }
 
     // TODO: check supported channel mask
 
     error = otDatasetSendMgmtPendingSet(GetOtInstance(), &emptyDataset, aPendingOpDatasetTlvs.data(),
-                                        aPendingOpDatasetTlvs.size(), SendMgmtPendingSetCallback,
+                                        static_cast<uint8_t>(aPendingOpDatasetTlvs.size()), SendMgmtPendingSetCallback,
                                         /* aBinderServer= */ this);
     if (error != OT_ERROR_NONE)
     {
@@ -690,7 +821,6 @@
         assert(mMigrationReceiver == nullptr);
         mMigrationReceiver = aReceiver;
     }
-    return Status::ok();
 }
 
 void OtDaemonServer::SendMgmtPendingSetCallback(otError aResult, void *aBinderServer)
@@ -707,6 +837,14 @@
 Status OtDaemonServer::setCountryCode(const std::string                        &aCountryCode,
                                       const std::shared_ptr<IOtStatusReceiver> &aReceiver)
 {
+    mTaskRunner.Post([aCountryCode, aReceiver, this]() { setCountryCodeInternal(aCountryCode, aReceiver); });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::setCountryCodeInternal(const std::string                        &aCountryCode,
+                                            const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
     static constexpr int kCountryCodeLength = 2;
     otError              error              = OT_ERROR_NONE;
     std::string          message;
@@ -717,33 +855,130 @@
 
     otbrLogInfo("Set country code: %c%c", aCountryCode[0], aCountryCode[1]);
     VerifyOrExit(GetOtInstance() != nullptr, error = OT_ERROR_INVALID_STATE, message = "OT is not initialized");
-    countryCode = (static_cast<uint16_t>(aCountryCode[0]) << 8) | aCountryCode[1];
+
+    countryCode = static_cast<uint16_t>((aCountryCode[0] << 8) | aCountryCode[1]);
     SuccessOrExit(error = otLinkSetRegion(GetOtInstance(), countryCode), message = "Failed to set the country code");
 
 exit:
     PropagateResult(error, message, aReceiver);
+}
+
+Status OtDaemonServer::getChannelMasks(const std::shared_ptr<IChannelMasksReceiver> &aReceiver)
+{
+    mTaskRunner.Post([aReceiver, this]() { getChannelMasksInternal(aReceiver); });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::getChannelMasksInternal(const std::shared_ptr<IChannelMasksReceiver> &aReceiver)
+{
+    otError  error = OT_ERROR_NONE;
+    uint32_t supportedChannelMask;
+    uint32_t preferredChannelMask;
+
+    VerifyOrExit(GetOtInstance() != nullptr, error = OT_ERROR_INVALID_STATE);
+
+    supportedChannelMask = otLinkGetSupportedChannelMask(GetOtInstance());
+    preferredChannelMask = otPlatRadioGetPreferredChannelMask(GetOtInstance());
+
+exit:
+    if (aReceiver != nullptr)
+    {
+        if (error == OT_ERROR_NONE)
+        {
+            aReceiver->onSuccess(supportedChannelMask, preferredChannelMask);
+        }
+        else
+        {
+            aReceiver->onError(error, "OT is not initialized");
+        }
+    }
+}
+
+Status OtDaemonServer::setChannelMaxPowers(const std::vector<ChannelMaxPower>       &aChannelMaxPowers,
+                                           const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
+    mTaskRunner.Post(
+        [aChannelMaxPowers, aReceiver, this]() { setChannelMaxPowersInternal(aChannelMaxPowers, aReceiver); });
+
+    return Status::ok();
+}
+
+Status OtDaemonServer::setChannelMaxPowersInternal(const std::vector<ChannelMaxPower>       &aChannelMaxPowers,
+                                                   const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
+    otError     error = OT_ERROR_NONE;
+    std::string message;
+    uint8_t     channel;
+    int16_t     maxPower;
+
+    VerifyOrExit(GetOtInstance() != nullptr, error = OT_ERROR_INVALID_STATE, message = "OT is not initialized");
+
+    for (ChannelMaxPower channelMaxPower : aChannelMaxPowers)
+    {
+        VerifyOrExit((channelMaxPower.channel >= OT_RADIO_2P4GHZ_OQPSK_CHANNEL_MIN) &&
+                         (channelMaxPower.channel <= OT_RADIO_2P4GHZ_OQPSK_CHANNEL_MAX),
+                     error = OT_ERROR_INVALID_ARGS, message = "The channel is invalid");
+        VerifyOrExit((channelMaxPower.maxPower >= INT16_MIN) && (channelMaxPower.maxPower <= INT16_MAX),
+                     error = OT_ERROR_INVALID_ARGS, message = "The max power is invalid");
+    }
+
+    for (ChannelMaxPower channelMaxPower : aChannelMaxPowers)
+    {
+        channel  = static_cast<uint8_t>(channelMaxPower.channel);
+        maxPower = static_cast<int16_t>(channelMaxPower.maxPower);
+        otbrLogInfo("Set channel max power: channel=%u, maxPower=%d", channel, maxPower);
+        SuccessOrExit(error   = otPlatRadioSetChannelTargetPower(GetOtInstance(), channel, maxPower),
+                      message = "Failed to set channel max power");
+    }
+
+exit:
+    PropagateResult(error, message, aReceiver);
     return Status::ok();
 }
 
 Status OtDaemonServer::configureBorderRouter(const BorderRouterConfigurationParcel    &aBorderRouterConfiguration,
                                              const std::shared_ptr<IOtStatusReceiver> &aReceiver)
 {
-    int         icmp6SocketFd = aBorderRouterConfiguration.infraInterfaceIcmp6Socket.dup().release();
-    std::string message;
-    otError     error = OT_ERROR_NONE;
+    int         icmp6SocketFd               = aBorderRouterConfiguration.infraInterfaceIcmp6Socket.dup().release();
+    std::string infraInterfaceName          = aBorderRouterConfiguration.infraInterfaceName;
+    bool        isBorderRoutingEnabled      = aBorderRouterConfiguration.isBorderRoutingEnabled;
+    bool        isBorderRouterConfigChanged = (mBorderRouterConfiguration != aBorderRouterConfiguration);
 
     otbrLogInfo("Configuring Border Router: %s", aBorderRouterConfiguration.toString().c_str());
 
+    // The copy constructor of `BorderRouterConfigurationParcel` is deleted. It is unable to directly pass the
+    // `aBorderRouterConfiguration` to the lambda function. Only the necessary parameters of
+    // `BorderRouterConfigurationParcel` are passed to the lambda function here.
+    mTaskRunner.Post(
+        [icmp6SocketFd, infraInterfaceName, isBorderRoutingEnabled, isBorderRouterConfigChanged, aReceiver, this]() {
+            configureBorderRouterInternal(icmp6SocketFd, infraInterfaceName, isBorderRoutingEnabled,
+                                          isBorderRouterConfigChanged, aReceiver);
+        });
+
+    return Status::ok();
+}
+
+void OtDaemonServer::configureBorderRouterInternal(int                aIcmp6SocketFd,
+                                                   const std::string &aInfraInterfaceName,
+                                                   bool               aIsBorderRoutingEnabled,
+                                                   bool               aIsBorderRouterConfigChanged,
+                                                   const std::shared_ptr<IOtStatusReceiver> &aReceiver)
+{
+    int         icmp6SocketFd = aIcmp6SocketFd;
+    otError     error         = OT_ERROR_NONE;
+    std::string message;
+
     VerifyOrExit(GetOtInstance() != nullptr, error = OT_ERROR_INVALID_STATE, message = "OT is not initialized");
 
-    if (mBorderRouterConfiguration != aBorderRouterConfiguration)
+    if (aIsBorderRouterConfigChanged)
     {
-        if (aBorderRouterConfiguration.isBorderRoutingEnabled)
+        if (aIsBorderRoutingEnabled)
         {
-            int infraIfIndex = if_nametoindex(aBorderRouterConfiguration.infraInterfaceName.c_str());
+            unsigned int infraIfIndex = if_nametoindex(aInfraInterfaceName.c_str());
             SuccessOrExit(error   = otBorderRoutingSetEnabled(GetOtInstance(), false /* aEnabled */),
                           message = "failed to disable border routing");
-            otSysSetInfraNetif(aBorderRouterConfiguration.infraInterfaceName.c_str(), icmp6SocketFd);
+            otSysSetInfraNetif(aInfraInterfaceName.c_str(), icmp6SocketFd);
             icmp6SocketFd = -1;
             SuccessOrExit(error   = otBorderRoutingInit(GetOtInstance(), infraIfIndex, otSysInfraIfIsRunning()),
                           message = "failed to initialize border routing");
@@ -760,8 +995,8 @@
         }
     }
 
-    mBorderRouterConfiguration.isBorderRoutingEnabled = aBorderRouterConfiguration.isBorderRoutingEnabled;
-    mBorderRouterConfiguration.infraInterfaceName     = aBorderRouterConfiguration.infraInterfaceName;
+    mBorderRouterConfiguration.isBorderRoutingEnabled = aIsBorderRoutingEnabled;
+    mBorderRouterConfiguration.infraInterfaceName     = aInfraInterfaceName;
 
 exit:
     if (error != OT_ERROR_NONE)
@@ -769,8 +1004,26 @@
         close(icmp6SocketFd);
     }
     PropagateResult(error, message, aReceiver);
+}
 
-    return Status::ok();
+static int OutputCallback(void *aContext, const char *aFormat, va_list aArguments)
+{
+    std::string output;
+
+    android::base::StringAppendV(&output, aFormat, aArguments);
+
+    int length = output.length();
+
+    VerifyOrExit(android::base::WriteStringToFd(output, *(static_cast<int *>(aContext))), length = 0);
+
+exit:
+    return length;
+}
+
+inline void DumpCliCommand(std::string aCommand, int aFd)
+{
+    android::base::WriteStringToFd(aCommand + '\n', aFd);
+    otCliInputLine(aCommand.data());
 }
 
 binder_status_t OtDaemonServer::dump(int aFd, const char **aArgs, uint32_t aNumArgs)
@@ -778,9 +1031,27 @@
     OT_UNUSED_VARIABLE(aArgs);
     OT_UNUSED_VARIABLE(aNumArgs);
 
-    // TODO: Use ::android::base::WriteStringToFd to dump infomration.
+    otCliInit(GetOtInstance(), OutputCallback, &aFd);
+
+    DumpCliCommand("state", aFd);
+    DumpCliCommand("srp server state", aFd);
+    DumpCliCommand("srp server service", aFd);
+    DumpCliCommand("srp server host", aFd);
+    DumpCliCommand("dataset active", aFd);
+    DumpCliCommand("leaderdata", aFd);
+    DumpCliCommand("eidcache", aFd);
+    DumpCliCommand("counters mac", aFd);
+    DumpCliCommand("counters mle", aFd);
+    DumpCliCommand("counters ip", aFd);
+    DumpCliCommand("router table", aFd);
+    DumpCliCommand("neighbor table", aFd);
+    DumpCliCommand("ipaddr -v", aFd);
+    DumpCliCommand("netdata show", aFd);
+
     fsync(aFd);
 
+    otSysCliInitUsingDaemon(GetOtInstance());
+
     return STATUS_OK;
 }
 
diff --git a/src/android/otdaemon_server.hpp b/src/android/otdaemon_server.hpp
index 60a3515..8124952 100644
--- a/src/android/otdaemon_server.hpp
+++ b/src/android/otdaemon_server.hpp
@@ -51,13 +51,17 @@
 using BinderDeathRecipient = ::ndk::ScopedAIBinder_DeathRecipient;
 using ScopedFileDescriptor = ::ndk::ScopedFileDescriptor;
 using Status               = ::ndk::ScopedAStatus;
+using aidl::android::net::thread::ChannelMaxPower;
+using aidl::com::android::server::thread::openthread::BackboneRouterState;
 using aidl::com::android::server::thread::openthread::BnOtDaemon;
 using aidl::com::android::server::thread::openthread::BorderRouterConfigurationParcel;
+using aidl::com::android::server::thread::openthread::IChannelMasksReceiver;
 using aidl::com::android::server::thread::openthread::INsdPublisher;
 using aidl::com::android::server::thread::openthread::IOtDaemon;
 using aidl::com::android::server::thread::openthread::IOtDaemonCallback;
 using aidl::com::android::server::thread::openthread::IOtStatusReceiver;
 using aidl::com::android::server::thread::openthread::Ipv6AddressInfo;
+using aidl::com::android::server::thread::openthread::MeshcopTxtAttributes;
 using aidl::com::android::server::thread::openthread::OtDaemonState;
 
 class OtDaemonServer : public BnOtDaemon, public MainloopProcessor, public vendor::VendorServer
@@ -89,45 +93,77 @@
 
     // Implements IOtDaemon.aidl
 
-    Status initialize(const ScopedFileDescriptor           &aTunFd,
-                      const bool                            enabled,
-                      const std::shared_ptr<INsdPublisher> &aNsdPublisher) override;
+    Status initialize(const ScopedFileDescriptor               &aTunFd,
+                      const bool                                enabled,
+                      const std::shared_ptr<INsdPublisher>     &aNsdPublisher,
+                      const MeshcopTxtAttributes               &aMeshcopTxts,
+                      const std::shared_ptr<IOtDaemonCallback> &aCallback,
+                      const std::string                        &aCountryCode) override;
+    void   initializeInternal(const bool                                enabled,
+                              const std::shared_ptr<INsdPublisher>     &aINsdPublisher,
+                              const MeshcopTxtAttributes               &aMeshcopTxts,
+                              const std::shared_ptr<IOtDaemonCallback> &aCallback,
+                              const std::string                        &aCountryCode);
+    Status terminate(void) override;
     Status setThreadEnabled(const bool enabled, const std::shared_ptr<IOtStatusReceiver> &aReceiver) override;
+    void   setThreadEnabledInternal(const bool enabled, const std::shared_ptr<IOtStatusReceiver> &aReceiver);
     Status registerStateCallback(const std::shared_ptr<IOtDaemonCallback> &aCallback, int64_t listenerId) override;
+    void   registerStateCallbackInternal(const std::shared_ptr<IOtDaemonCallback> &aCallback, int64_t listenerId);
     bool   isAttached(void);
     Status join(const std::vector<uint8_t>               &aActiveOpDatasetTlvs,
                 const std::shared_ptr<IOtStatusReceiver> &aReceiver) override;
+    void   joinInternal(const std::vector<uint8_t>               &aActiveOpDatasetTlvs,
+                        const std::shared_ptr<IOtStatusReceiver> &aReceiver);
     Status leave(const std::shared_ptr<IOtStatusReceiver> &aReceiver) override;
+    void   leaveInternal(const std::shared_ptr<IOtStatusReceiver> &aReceiver);
     Status scheduleMigration(const std::vector<uint8_t>               &aPendingOpDatasetTlvs,
                              const std::shared_ptr<IOtStatusReceiver> &aReceiver) override;
+    void   scheduleMigrationInternal(const std::vector<uint8_t>               &aPendingOpDatasetTlvs,
+                                     const std::shared_ptr<IOtStatusReceiver> &aReceiver);
     Status setCountryCode(const std::string &aCountryCode, const std::shared_ptr<IOtStatusReceiver> &aReceiver);
+    void   setCountryCodeInternal(const std::string &aCountryCode, const std::shared_ptr<IOtStatusReceiver> &aReceiver);
+    Status setChannelMaxPowers(const std::vector<ChannelMaxPower>       &aChannelMaxPowers,
+                               const std::shared_ptr<IOtStatusReceiver> &aReceiver);
+    Status setChannelMaxPowersInternal(const std::vector<ChannelMaxPower>       &aChannelMaxPowers,
+                                       const std::shared_ptr<IOtStatusReceiver> &aReceiver);
     Status configureBorderRouter(const BorderRouterConfigurationParcel    &aBorderRouterConfiguration,
                                  const std::shared_ptr<IOtStatusReceiver> &aReceiver) override;
+    void   configureBorderRouterInternal(int                                       aIcmp6SocketFd,
+                                         const std::string                        &aInfraInterfaceName,
+                                         bool                                      aIsBorderRoutingEnabled,
+                                         bool                                      aIsBorderRouterConfigChanged,
+                                         const std::shared_ptr<IOtStatusReceiver> &aReceiver);
+    Status getChannelMasks(const std::shared_ptr<IChannelMasksReceiver> &aReceiver) override;
+    void   getChannelMasksInternal(const std::shared_ptr<IChannelMasksReceiver> &aReceiver);
 
     bool        RefreshOtDaemonState(otChangedFlags aFlags);
     void        LeaveGracefully(const LeaveCallback &aReceiver);
+    void        FinishLeave(const std::shared_ptr<IOtStatusReceiver> &aReceiver);
     static void DetachGracefullyCallback(void *aBinderServer);
     void        DetachGracefullyCallback(void);
     static void SendMgmtPendingSetCallback(otError aResult, void *aBinderServer);
 
-    static void BinderDeathCallback(void *aBinderServer);
-    void        StateCallback(otChangedFlags aFlags);
-    static void AddressCallback(const otIp6AddressInfo *aAddressInfo, bool aIsAdded, void *aBinderServer);
-    static void ReceiveCallback(otMessage *aMessage, void *aBinderServer);
-    void        ReceiveCallback(otMessage *aMessage);
-    void        TransmitCallback(void);
-    static void HandleBackboneMulticastListenerEvent(void                                  *aBinderServer,
-                                                     otBackboneRouterMulticastListenerEvent aEvent,
-                                                     const otIp6Address                    *aAddress);
-    void        PushTelemetryIfConditionMatch();
-    void        updateThreadEnabledState(const int aEnabled, const std::shared_ptr<IOtStatusReceiver> &aReceiver);
-    void        enableThread(const std::shared_ptr<IOtStatusReceiver> &aReceiver);
+    static void         BinderDeathCallback(void *aBinderServer);
+    void                StateCallback(otChangedFlags aFlags);
+    static void         AddressCallback(const otIp6AddressInfo *aAddressInfo, bool aIsAdded, void *aBinderServer);
+    static void         ReceiveCallback(otMessage *aMessage, void *aBinderServer);
+    void                ReceiveCallback(otMessage *aMessage);
+    void                TransmitCallback(void);
+    BackboneRouterState GetBackboneRouterState(void);
+    static void         HandleBackboneMulticastListenerEvent(void                                  *aBinderServer,
+                                                             otBackboneRouterMulticastListenerEvent aEvent,
+                                                             const otIp6Address                    *aAddress);
+    void                PushTelemetryIfConditionMatch();
+    void updateThreadEnabledState(const int aEnabled, const std::shared_ptr<IOtStatusReceiver> &aReceiver);
+    void enableThread(const std::shared_ptr<IOtStatusReceiver> &aReceiver);
 
-    int                                mThreadEnabled = IOtDaemon::OT_STATE_DISABLED;
+    int                                mThreadEnabled = OT_STATE_DISABLED;
+    otbr::Application                 &mApplication;
     otbr::Ncp::ControllerOpenThread   &mNcp;
     otbr::BorderAgent                 &mBorderAgent;
     MdnsPublisher                     &mMdnsPublisher;
     std::shared_ptr<INsdPublisher>     mINsdPublisher;
+    MeshcopTxtAttributes               mMeshcopTxts;
     TaskRunner                         mTaskRunner;
     ScopedFileDescriptor               mTunFd;
     OtDaemonState                      mState;
@@ -137,7 +173,7 @@
     std::shared_ptr<IOtStatusReceiver> mMigrationReceiver;
     std::vector<LeaveCallback>         mLeaveCallbacks;
     BorderRouterConfigurationParcel    mBorderRouterConfiguration;
-    static constexpr Seconds           kTelemetryCheckInterval           = Seconds(30);           // 30 seconds
+    static constexpr Seconds           kTelemetryCheckInterval           = Seconds(600);          // 600 seconds
     static constexpr Seconds           kTelemetryUploadIntervalThreshold = Seconds(60 * 60 * 12); // 12 hours
 };
 
diff --git a/src/border_agent/border_agent.cpp b/src/border_agent/border_agent.cpp
index 78053b1..b6acf18 100644
--- a/src/border_agent/border_agent.cpp
+++ b/src/border_agent/border_agent.cpp
@@ -69,11 +69,7 @@
 
 namespace otbr {
 
-static const char kVendorName[]             = OTBR_VENDOR_NAME;
-static const char kProductName[]            = OTBR_PRODUCT_NAME;
-static const char kBorderAgentServiceType[] = "_meshcop._udp"; ///< Border agent service type of mDNS
-static const char kBorderAgentServiceInstanceName[] =
-    OTBR_MESHCOP_SERVICE_INSTANCE_NAME; ///< Border agent service name of mDNS
+static const char    kBorderAgentServiceType[]    = "_meshcop._udp"; ///< Border agent service type of mDNS
 static constexpr int kBorderAgentServiceDummyPort = 49152;
 
 /**
@@ -139,24 +135,57 @@
     }
 };
 
-BorderAgent::BorderAgent(otbr::Ncp::ControllerOpenThread &aNcp)
+BorderAgent::BorderAgent(otbr::Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher)
     : mNcp(aNcp)
-    , mPublisher(Mdns::Publisher::Create([this](Mdns::Publisher::State aNewState) { HandleMdnsState(aNewState); }))
-#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
-    , mAdvertisingProxy(aNcp, *mPublisher)
-#endif
-#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
-    , mDiscoveryProxy(aNcp, *mPublisher)
-#endif
-#if OTBR_ENABLE_TREL
-    , mTrelDnssd(aNcp, *mPublisher)
-#endif
-{
-}
-
-void BorderAgent::Init(void)
+    , mPublisher(aPublisher)
+    , mIsEnabled(false)
+    , mVendorName(OTBR_VENDOR_NAME)
+    , mProductName(OTBR_PRODUCT_NAME)
+    , mBaseServiceInstanceName(OTBR_MESHCOP_SERVICE_INSTANCE_NAME)
 {
     mNcp.AddThreadStateChangedCallback([this](otChangedFlags aFlags) { HandleThreadStateChanged(aFlags); });
+}
+
+otbrError BorderAgent::SetMeshCopServiceValues(const std::string          &aServiceInstanceName,
+                                               const std::string          &aProductName,
+                                               const std::string          &aVendorName,
+                                               const std::vector<uint8_t> &aVendorOui)
+{
+    otbrError error = OTBR_ERROR_NONE;
+
+    VerifyOrExit(aProductName.size() <= kMaxProductNameLength, error = OTBR_ERROR_INVALID_ARGS);
+    VerifyOrExit(aVendorName.size() <= kMaxVendorNameLength, error = OTBR_ERROR_INVALID_ARGS);
+    VerifyOrExit(aVendorOui.empty() || aVendorOui.size() == kVendorOuiLength, error = OTBR_ERROR_INVALID_ARGS);
+
+    mProductName = aProductName;
+    mVendorName  = aVendorName;
+    mVendorOui   = aVendorOui;
+
+    mBaseServiceInstanceName = aServiceInstanceName;
+
+exit:
+    return error;
+}
+
+void BorderAgent::SetEnabled(bool aIsEnabled)
+{
+    VerifyOrExit(IsEnabled() != aIsEnabled);
+    mIsEnabled = aIsEnabled;
+    if (mIsEnabled)
+    {
+        Start();
+    }
+    else
+    {
+        Stop();
+    }
+exit:
+    return;
+}
+
+void BorderAgent::Start(void)
+{
+    otbrLogInfo("Start Thread Border Agent");
 
 #if OTBR_ENABLE_DBUS_SERVER
     mNcp.GetThreadHelper()->SetUpdateMeshCopTxtHandler([this](std::map<std::string, std::vector<uint8_t>> aUpdate) {
@@ -169,74 +198,31 @@
     });
 #endif
 
-    mServiceInstanceName = BaseServiceInstanceName();
-
-    Start();
-}
-
-void BorderAgent::Deinit(void)
-{
-    Stop();
-}
-
-void BorderAgent::Start(void)
-{
-    otbrError error = OTBR_ERROR_NONE;
-
-    SuccessOrExit(error = mPublisher->Start());
-#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
-    mAdvertisingProxy.Start();
-#endif
-#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
-    mDiscoveryProxy.Start();
-#endif
-
-exit:
-    otbrLogResult(error, "Start Thread Border Agent");
+    mServiceInstanceName = GetServiceInstanceNameWithExtAddr(mBaseServiceInstanceName);
+    UpdateMeshCopService();
 }
 
 void BorderAgent::Stop(void)
 {
     otbrLogInfo("Stop Thread Border Agent");
-
-#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
-    mAdvertisingProxy.Stop();
-#endif
-
-#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
-    mDiscoveryProxy.Stop();
-#endif
-
     UnpublishMeshCopService();
-    mPublisher->Stop();
-}
-
-BorderAgent::~BorderAgent(void)
-{
-    if (mPublisher != nullptr)
-    {
-        delete mPublisher;
-        mPublisher = nullptr;
-    }
 }
 
 void BorderAgent::HandleMdnsState(Mdns::Publisher::State aState)
 {
+    VerifyOrExit(IsEnabled());
+
     switch (aState)
     {
     case Mdns::Publisher::State::kReady:
         UpdateMeshCopService();
-#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
-        mAdvertisingProxy.PublishAllHostsAndServices();
-#endif
-#if OTBR_ENABLE_TREL
-        mTrelDnssd.OnMdnsPublisherReady();
-#endif
         break;
     default:
         otbrLogWarning("mDNS publisher not available!");
         break;
     }
+exit:
+    return;
 }
 
 static uint64_t ConvertTimestampToUint64(const otTimestamp &aTimestamp)
@@ -393,8 +379,18 @@
     }
 #endif
 
-    txtList.emplace_back("vn", kVendorName);
-    txtList.emplace_back("mn", kProductName);
+    if (!mVendorOui.empty())
+    {
+        txtList.emplace_back("vo", mVendorOui.data(), mVendorOui.size());
+    }
+    if (!mVendorName.empty())
+    {
+        txtList.emplace_back("vn", mVendorName.c_str());
+    }
+    if (!mProductName.empty())
+    {
+        txtList.emplace_back("mn", mProductName.c_str());
+    }
     txtList.emplace_back("nn", networkName);
     txtList.emplace_back("xp", extPanId->m8, sizeof(extPanId->m8));
     txtList.emplace_back("tv", mNcp.GetThreadVersion());
@@ -441,38 +437,38 @@
     error = Mdns::Publisher::EncodeTxtData(txtList, txtData);
     assert(error == OTBR_ERROR_NONE);
 
-    mPublisher->PublishService(/* aHostName */ "", mServiceInstanceName, kBorderAgentServiceType,
-                               Mdns::Publisher::SubTypeList{}, port, txtData, [this](otbrError aError) {
-                                   if (aError == OTBR_ERROR_ABORTED)
-                                   {
-                                       // OTBR_ERROR_ABORTED is thrown when an ongoing service registration is
-                                       // cancelled. This can happen when the meshcop service is being updated
-                                       // frequently. To avoid false alarms, it should not be logged like a real error.
-                                       otbrLogInfo("Cancelled previous publishing meshcop service %s.%s.local",
-                                                   mServiceInstanceName.c_str(), kBorderAgentServiceType);
-                                   }
-                                   else
-                                   {
-                                       otbrLogResult(aError, "Result of publish meshcop service %s.%s.local",
-                                                     mServiceInstanceName.c_str(), kBorderAgentServiceType);
-                                   }
-                                   if (aError == OTBR_ERROR_DUPLICATED)
-                                   {
-                                       // Try to unpublish current service in case we are trying to register
-                                       // multiple new services simultaneously when the original service name
-                                       // is conflicted.
-                                       UnpublishMeshCopService();
-                                       mServiceInstanceName = GetAlternativeServiceInstanceName();
-                                       PublishMeshCopService();
-                                   }
-                               });
+    mPublisher.PublishService(/* aHostName */ "", mServiceInstanceName, kBorderAgentServiceType,
+                              Mdns::Publisher::SubTypeList{}, port, txtData, [this](otbrError aError) {
+                                  if (aError == OTBR_ERROR_ABORTED)
+                                  {
+                                      // OTBR_ERROR_ABORTED is thrown when an ongoing service registration is
+                                      // cancelled. This can happen when the meshcop service is being updated
+                                      // frequently. To avoid false alarms, it should not be logged like a real error.
+                                      otbrLogInfo("Cancelled previous publishing meshcop service %s.%s.local",
+                                                  mServiceInstanceName.c_str(), kBorderAgentServiceType);
+                                  }
+                                  else
+                                  {
+                                      otbrLogResult(aError, "Result of publish meshcop service %s.%s.local",
+                                                    mServiceInstanceName.c_str(), kBorderAgentServiceType);
+                                  }
+                                  if (aError == OTBR_ERROR_DUPLICATED)
+                                  {
+                                      // Try to unpublish current service in case we are trying to register
+                                      // multiple new services simultaneously when the original service name
+                                      // is conflicted.
+                                      UnpublishMeshCopService();
+                                      mServiceInstanceName = GetAlternativeServiceInstanceName();
+                                      PublishMeshCopService();
+                                  }
+                              });
 }
 
 void BorderAgent::UnpublishMeshCopService(void)
 {
     otbrLogInfo("Unpublish meshcop service %s.%s.local", mServiceInstanceName.c_str(), kBorderAgentServiceType);
 
-    mPublisher->UnpublishService(mServiceInstanceName, kBorderAgentServiceType, [this](otbrError aError) {
+    mPublisher.UnpublishService(mServiceInstanceName, kBorderAgentServiceType, [this](otbrError aError) {
         otbrLogResult(aError, "Result of unpublish meshcop service %s.%s.local", mServiceInstanceName.c_str(),
                       kBorderAgentServiceType);
     });
@@ -480,8 +476,10 @@
 
 void BorderAgent::UpdateMeshCopService(void)
 {
-    VerifyOrExit(mPublisher->IsStarted(), mPublisher->Start());
+    VerifyOrExit(IsEnabled());
+    VerifyOrExit(mPublisher.IsStarted());
     PublishMeshCopService();
+
 exit:
     return;
 }
@@ -496,7 +494,7 @@
 
 void BorderAgent::HandleThreadStateChanged(otChangedFlags aFlags)
 {
-    VerifyOrExit(mPublisher != nullptr);
+    VerifyOrExit(IsEnabled());
 
     if (aFlags & OT_CHANGED_THREAD_ROLE)
     {
@@ -508,6 +506,7 @@
     {
         UpdateMeshCopService();
     }
+
 exit:
     return;
 }
@@ -519,12 +518,12 @@
     return role == OT_DEVICE_ROLE_CHILD || role == OT_DEVICE_ROLE_ROUTER || role == OT_DEVICE_ROLE_LEADER;
 }
 
-std::string BorderAgent::BaseServiceInstanceName() const
+std::string BorderAgent::GetServiceInstanceNameWithExtAddr(const std::string &aServiceInstanceName) const
 {
     const otExtAddress *extAddress = otLinkGetExtendedAddress(mNcp.GetInstance());
     std::stringstream   ss;
 
-    ss << kBorderAgentServiceInstanceName << " #";
+    ss << aServiceInstanceName << " #";
     ss << std::uppercase << std::hex << std::setfill('0');
     ss << std::setw(2) << static_cast<int>(extAddress->m8[6]);
     ss << std::setw(2) << static_cast<int>(extAddress->m8[7]);
@@ -539,7 +538,7 @@
     uint16_t                                rand = uniform_dist(engine);
     std::stringstream                       ss;
 
-    ss << BaseServiceInstanceName() << " (" << rand << ")";
+    ss << GetServiceInstanceNameWithExtAddr(mBaseServiceInstanceName) << " (" << rand << ")";
     return ss.str();
 }
 
diff --git a/src/border_agent/border_agent.hpp b/src/border_agent/border_agent.hpp
index 0274ae1..39a0d2c 100644
--- a/src/border_agent/border_agent.hpp
+++ b/src/border_agent/border_agent.hpp
@@ -62,7 +62,7 @@
 #endif
 
 #ifndef OTBR_MESHCOP_SERVICE_INSTANCE_NAME
-#define OTBR_MESHCOP_SERVICE_INSTANCE_NAME OTBR_VENDOR_NAME " " OTBR_PRODUCT_NAME
+#define OTBR_MESHCOP_SERVICE_INSTANCE_NAME (OTBR_VENDOR_NAME " " OTBR_PRODUCT_NAME)
 #endif
 
 namespace otbr {
@@ -87,36 +87,55 @@
      * The constructor to initialize the Thread border agent.
      *
      * @param[in] aNcp  A reference to the NCP controller.
+     * @param[in] aPublisher  A reference to the mDNS Publisher.
      *
      */
-    BorderAgent(otbr::Ncp::ControllerOpenThread &aNcp);
+    BorderAgent(otbr::Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher);
 
-    ~BorderAgent(void);
+    ~BorderAgent(void) = default;
 
     /**
-     * This method initialize border agent service.
+     * Overrides MeshCoP service (i.e. _meshcop._udp) instance name, product name, vendor name and vendor OUI.
      *
+     * This method must be called before this BorderAgent is enabled by SetEnabled.
+     *
+     * @param[in] aServiceInstanceName  The service instance name; suffix may be appended to this value to avoid
+     *                                  name conflicts.
+     * @param[in] aProductName          The product name; must not exceed length of kMaxProductNameLength
+     *                                  and an empty string will be ignored.
+     * @param[in] aVendorName           The vendor name; must not exceed length of kMaxVendorNameLength
+     *                                  and an empty string will be ignored.
+     * @param[in] aVendorOui            The vendor OUI; must have length of 3 bytes or be empty and ignored.
+     *
+     * @returns OTBR_ERROR_INVALID_ARGS  If aVendorName, aProductName or aVendorOui exceeds the
+     *                                   allowed ranges.
+     * @returns OTBR_ERROR_NONE          If successfully set the meshcop service values.
      */
-    void Init(void);
+    otbrError SetMeshCopServiceValues(const std::string          &aServiceInstanceName,
+                                      const std::string          &aProductName,
+                                      const std::string          &aVendorName,
+                                      const std::vector<uint8_t> &aVendorOui = {});
 
     /**
-     * This method de-initializes border agent service.
+     * This method enables/disables the Border Agent.
+     *
+     * @param[in] aIsEnabled  Whether to enable the Border Agent.
      *
      */
-    void Deinit(void);
+    void SetEnabled(bool aIsEnabled);
 
     /**
-     * This method returns the Publisher the border agent is using.
+     * This method handles mDNS publisher's state changes.
      *
-     * @returns  A reference to the mPublisher.
+     * @param[in] aState  The state of mDNS publisher.
      *
      */
-    Mdns::Publisher &GetPublisher() { return *mPublisher; }
+    void HandleMdnsState(Mdns::Publisher::State aState);
 
 private:
     void Start(void);
     void Stop(void);
-    void HandleMdnsState(Mdns::Publisher::State aState);
+    bool IsEnabled(void) const { return mIsEnabled; }
     void PublishMeshCopService(void);
     void UpdateMeshCopService(void);
     void UnpublishMeshCopService(void);
@@ -127,26 +146,31 @@
     void HandleThreadStateChanged(otChangedFlags aFlags);
 
     bool        IsThreadStarted(void) const;
-    std::string BaseServiceInstanceName() const;
+    std::string GetServiceInstanceNameWithExtAddr(const std::string &aServiceInstanceName) const;
     std::string GetAlternativeServiceInstanceName() const;
 
     otbr::Ncp::ControllerOpenThread &mNcp;
-    Mdns::Publisher                 *mPublisher;
+    Mdns::Publisher                 &mPublisher;
+    bool                             mIsEnabled;
 
 #if OTBR_ENABLE_DBUS_SERVER
     std::map<std::string, std::vector<uint8_t>> mMeshCopTxtUpdate;
 #endif
 
-#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
-    AdvertisingProxy mAdvertisingProxy;
-#endif
-#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
-    Dnssd::DiscoveryProxy mDiscoveryProxy;
-#endif
-#if OTBR_ENABLE_TREL
-    TrelDnssd::TrelDnssd mTrelDnssd;
-#endif
+    std::vector<uint8_t> mVendorOui;
 
+    std::string mVendorName;
+    std::string mProductName;
+
+    // The base service instance name typically consists of the vendor and product name. But it can
+    // also be overridden by `OTBR_MESHCOP_SERVICE_INSTANCE_NAME` or method `SetMeshCopServiceValues()`.
+    // For example, this value can be "OpenThread Border Router".
+    std::string mBaseServiceInstanceName;
+
+    // The actual instance name advertised in the mDNS service. This is usually the value of
+    // `mBaseServiceInstanceName` plus the Extended Address and optional random number for avoiding
+    // conflicts. For example, this value can be "OpenThread Border Router #7AC3" or
+    // "OpenThread Border Router #7AC3 (14379)".
     std::string mServiceInstanceName;
 };
 
diff --git a/src/common/logging.cpp b/src/common/logging.cpp
index 5a787e8..c5f7018 100644
--- a/src/common/logging.cpp
+++ b/src/common/logging.cpp
@@ -53,6 +53,7 @@
 static const char   sLevelString[][8] = {
       "[EMERG]", "[ALERT]", "[CRIT]", "[ERR ]", "[WARN]", "[NOTE]", "[INFO]", "[DEBG]",
 };
+static bool sSyslogDisabled = false;
 
 static otbrLogLevel sDefaultLevel = OTBR_LOG_INFO;
 
@@ -77,8 +78,14 @@
     sLevel = aLevel;
 }
 
+/** Enable/disable logging with syslog */
+void otbrLogSyslogSetEnabled(bool aEnabled)
+{
+    sSyslogDisabled = !aEnabled;
+}
+
 /** Initialize logging */
-void otbrLogInit(const char *aProgramName, otbrLogLevel aLevel, bool aPrintStderr)
+void otbrLogInit(const char *aProgramName, otbrLogLevel aLevel, bool aPrintStderr, bool aSyslogDisable)
 {
     const char *ident;
 
@@ -88,7 +95,12 @@
     ident = strrchr(aProgramName, '/');
     ident = (ident != nullptr) ? ident + 1 : aProgramName;
 
-    openlog(ident, (LOG_CONS | LOG_PID) | (aPrintStderr ? LOG_PERROR : 0), OTBR_SYSLOG_FACILITY_ID);
+    otbrLogSyslogSetEnabled(!aSyslogDisable);
+
+    if (!sSyslogDisabled)
+    {
+        openlog(ident, (LOG_CONS | LOG_PID) | (aPrintStderr ? LOG_PERROR : 0), OTBR_SYSLOG_FACILITY_ID);
+    }
     sLevel        = aLevel;
     sDefaultLevel = sLevel;
 }
@@ -118,7 +130,7 @@
     return prefix;
 }
 
-/** log to the syslog or log file */
+/** log to the syslog or standard out */
 void otbrLog(otbrLogLevel aLevel, const char *aLogTag, const char *aFormat, ...)
 {
     const uint16_t kBufferSize = 1024;
@@ -129,7 +141,14 @@
 
     if ((aLevel <= sLevel) && (vsnprintf(buffer, sizeof(buffer), aFormat, ap) > 0))
     {
-        syslog(static_cast<int>(aLevel), "%s%s: %s", sLevelString[aLevel], GetPrefix(aLogTag), buffer);
+        if (sSyslogDisabled)
+        {
+            printf("%s%s: %s\n", sLevelString[aLevel], GetPrefix(aLogTag), buffer);
+        }
+        else
+        {
+            syslog(static_cast<int>(aLevel), "%s%s: %s", sLevelString[aLevel], GetPrefix(aLogTag), buffer);
+        }
     }
 
     va_end(ap);
@@ -137,7 +156,7 @@
     return;
 }
 
-/** log to the syslog or log file */
+/** log to the syslog or standard out */
 void otbrLogv(otbrLogLevel aLevel, const char *aFormat, va_list aArgList)
 {
     assert(aFormat);
@@ -148,9 +167,18 @@
     }
 }
 
+/** log to the syslog or standard out */
 void otbrLogvNoFilter(otbrLogLevel aLevel, const char *aFormat, va_list aArgList)
 {
-    vsyslog(static_cast<int>(aLevel), aFormat, aArgList);
+    if (sSyslogDisabled)
+    {
+        vprintf(aFormat, aArgList);
+        printf("\n");
+    }
+    else
+    {
+        vsyslog(static_cast<int>(aLevel), aFormat, aArgList);
+    }
 }
 
 /** Hex dump data to the log */
diff --git a/src/common/logging.hpp b/src/common/logging.hpp
index 0acdf87..ad36d4f 100644
--- a/src/common/logging.hpp
+++ b/src/common/logging.hpp
@@ -78,20 +78,21 @@
 /**
  * Control log to syslog.
  *
- * @param[in] enable  True to log to/via syslog.
+ * @param[in] aEnabled  True to enable logging to/via syslog.
  *
  */
-void otbrLogEnableSyslog(bool aEnabled);
+void otbrLogSyslogSetEnabled(bool aEnabled);
 
 /**
  * This function initialize the logging service.
  *
- * @param[in] aProgramName  The name of this runnable program.
- * @param[in] aLevel        Log level of the logger.
- * @param[in] aPrintStderr  Whether to log to stderr.
+ * @param[in] aProgramName    The name of this runnable program.
+ * @param[in] aLevel          Log level of the logger.
+ * @param[in] aPrintStderr    Whether to log to stderr.
+ * @param[in] aSyslogDisable  Whether to disable logging to syslog.
  *
  */
-void otbrLogInit(const char *aProgramName, otbrLogLevel aLevel, bool aPrintStderr);
+void otbrLogInit(const char *aProgramName, otbrLogLevel aLevel, bool aPrintStderr, bool aSyslogDisable);
 
 /**
  * This function log at level @p aLevel.
diff --git a/src/common/types.hpp b/src/common/types.hpp
index 649f0b6..a5cd547 100644
--- a/src/common/types.hpp
+++ b/src/common/types.hpp
@@ -402,16 +402,22 @@
                   "kEmaFactorDenominator must be greater than kEmaFactorNumerator");
 
     MdnsResponseCounters mHostRegistrations;
+    MdnsResponseCounters mKeyRegistrations;
     MdnsResponseCounters mServiceRegistrations;
     MdnsResponseCounters mHostResolutions;
     MdnsResponseCounters mServiceResolutions;
 
     uint32_t mHostRegistrationEmaLatency;    ///< The EMA latency of host registrations in milliseconds
+    uint32_t mKeyRegistrationEmaLatency;     ///< The EMA latency of key registrations in milliseconds
     uint32_t mServiceRegistrationEmaLatency; ///< The EMA latency of service registrations in milliseconds
     uint32_t mHostResolutionEmaLatency;      ///< The EMA latency of host resolutions in milliseconds
     uint32_t mServiceResolutionEmaLatency;   ///< The EMA latency of service resolutions in milliseconds
 };
 
+static constexpr size_t kVendorOuiLength      = 3;
+static constexpr size_t kMaxVendorNameLength  = 24;
+static constexpr size_t kMaxProductNameLength = 24;
+
 } // namespace otbr
 
 #endif // OTBR_COMMON_TYPES_HPP_
diff --git a/src/dbus/client/thread_api_dbus.cpp b/src/dbus/client/thread_api_dbus.cpp
index 0a6c36e..944c23f 100644
--- a/src/dbus/client/thread_api_dbus.cpp
+++ b/src/dbus/client/thread_api_dbus.cpp
@@ -659,6 +659,13 @@
     return GetProperty(OTBR_DBUS_PROPERTY_SRP_SERVER_INFO, aSrpServerInfo);
 }
 
+#if OTBR_ENABLE_TREL
+ClientError ThreadApiDBus::GetTrelInfo(TrelInfo &aTrelInfo)
+{
+    return GetProperty(OTBR_DBUS_PROPERTY_TREL_INFO, aTrelInfo);
+}
+#endif
+
 ClientError ThreadApiDBus::GetMdnsTelemetryInfo(MdnsTelemetryInfo &aMdnsTelemetryInfo)
 {
     return GetProperty(OTBR_DBUS_PROPERTY_MDNS_TELEMETRY_INFO, aMdnsTelemetryInfo);
diff --git a/src/dbus/client/thread_api_dbus.hpp b/src/dbus/client/thread_api_dbus.hpp
index 622faec..b47aaed 100644
--- a/src/dbus/client/thread_api_dbus.hpp
+++ b/src/dbus/client/thread_api_dbus.hpp
@@ -769,6 +769,20 @@
      */
     ClientError GetSrpServerInfo(SrpServerInfo &aSrpServerInfo);
 
+#if OTBR_ENABLE_TREL
+    /**
+     * This method gets the TREL information.
+     *
+     * @param[out] aTrelInfo  The TREL information.
+     *
+     * @retval ERROR_NONE  Successfully performed the dbus function call
+     * @retval ERROR_DBUS  dbus encode/decode error
+     * @retval ...         OpenThread defined error value otherwise
+     *
+     */
+    ClientError GetTrelInfo(TrelInfo &aTrelInfo);
+#endif
+
     /**
      * This method gets the MDNS telemetry information.
      *
diff --git a/src/dbus/common/constants.hpp b/src/dbus/common/constants.hpp
index 1d99f1d..a8e1648 100644
--- a/src/dbus/common/constants.hpp
+++ b/src/dbus/common/constants.hpp
@@ -97,7 +97,9 @@
 #define OTBR_DBUS_PROPERTY_FEATURE_FLAG_LIST_DATA "FeatureFlagListData"
 #define OTBR_DBUS_PROPERTY_RADIO_REGION "RadioRegion"
 #define OTBR_DBUS_PROPERTY_SRP_SERVER_INFO "SrpServerInfo"
+#define OTBR_DBUS_PROPERTY_TREL_INFO "TrelInfo"
 #define OTBR_DBUS_PROPERTY_DNSSD_COUNTERS "DnssdCounters"
+#define OTBR_DBUS_PROPERTY_OTBR_VERSION "OtbrVersion"
 #define OTBR_DBUS_PROPERTY_OT_HOST_VERSION "OtHostVersion"
 #define OTBR_DBUS_PROPERTY_OT_RCP_VERSION "OtRcpVersion"
 #define OTBR_DBUS_PROPERTY_THREAD_VERSION "ThreadVersion"
diff --git a/src/dbus/common/dbus_message_helper.cpp b/src/dbus/common/dbus_message_helper.cpp
index dbf7a0c..86b23b4 100644
--- a/src/dbus/common/dbus_message_helper.cpp
+++ b/src/dbus/common/dbus_message_helper.cpp
@@ -31,6 +31,17 @@
 namespace otbr {
 namespace DBus {
 
+otbrError DbusMessageIterRecurse(DBusMessageIter *aIter, DBusMessageIter *aSubIter, int aType)
+{
+    otbrError error = OTBR_ERROR_NONE;
+
+    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == aType, error = OTBR_ERROR_DBUS);
+    dbus_message_iter_recurse(aIter, aSubIter);
+
+exit:
+    return error;
+}
+
 otbrError DBusMessageExtract(DBusMessageIter *aIter, bool &aValue)
 {
     otbrError   error = OTBR_ERROR_DBUS;
diff --git a/src/dbus/common/dbus_message_helper.hpp b/src/dbus/common/dbus_message_helper.hpp
index 03c8f14..60d1953 100644
--- a/src/dbus/common/dbus_message_helper.hpp
+++ b/src/dbus/common/dbus_message_helper.hpp
@@ -115,6 +115,10 @@
 otbrError DBusMessageExtract(DBusMessageIter *aIter, Nat64ErrorCounters &aCounters);
 otbrError DBusMessageEncode(DBusMessageIter *aIter, const InfraLinkInfo &aInfraLinkInfo);
 otbrError DBusMessageExtract(DBusMessageIter *aIter, InfraLinkInfo &aInfraLinkInfo);
+otbrError DBusMessageEncode(DBusMessageIter *aIter, const TrelInfo &aTrelInfo);
+otbrError DBusMessageExtract(DBusMessageIter *aIter, TrelInfo &aTrelInfo);
+otbrError DBusMessageEncode(DBusMessageIter *aIter, const TrelInfo::TrelPacketCounters &aCounters);
+otbrError DBusMessageExtract(DBusMessageIter *aIter, TrelInfo::TrelPacketCounters &aCounters);
 
 template <typename T> struct DBusTypeTrait;
 
@@ -382,6 +386,21 @@
     static constexpr const char *TYPE_AS_STRING = "((tt)(tt)(tt)(tt))";
 };
 
+template <> struct DBusTypeTrait<TrelInfo>
+{
+    // struct of { bool,
+    //             uint16,
+    //             struct of {
+    //               uint64, uint64, uint64, uint64, uint64 } }
+    static constexpr const char *TYPE_AS_STRING = "(bq(ttttt))";
+};
+
+template <> struct DBusTypeTrait<TrelInfo::TrelPacketCounters>
+{
+    // struct of { uint64, uint64, uint64, uint64, uint64 }
+    static constexpr const char *TYPE_AS_STRING = "(ttttt)";
+};
+
 template <> struct DBusTypeTrait<InfraLinkInfo>
 {
     // struct of { string, bool, bool, bool, uint32, uint32, uint32 }
@@ -448,6 +467,8 @@
     static constexpr const char *TYPE_AS_STRING = DBUS_TYPE_BOOLEAN_AS_STRING;
 };
 
+otbrError DbusMessageIterRecurse(DBusMessageIter *aIter, DBusMessageIter *aSubIter, int aType);
+
 otbrError DBusMessageEncode(DBusMessageIter *aIter, bool aValue);
 otbrError DBusMessageEncode(DBusMessageIter *aIter, int8_t aValue);
 otbrError DBusMessageEncode(DBusMessageIter *aIter, const std::string &aValue);
diff --git a/src/dbus/common/dbus_message_helper_openthread.cpp b/src/dbus/common/dbus_message_helper_openthread.cpp
index 192279b..d0d7770 100644
--- a/src/dbus/common/dbus_message_helper_openthread.cpp
+++ b/src/dbus/common/dbus_message_helper_openthread.cpp
@@ -58,8 +58,7 @@
     // Dbus doesn't have the concept of a signed byte
     int16_t rssi = 0;
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mExtAddress));
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mNetworkName));
@@ -117,8 +116,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aResult.mChannel));
     SuccessOrExit(error = DBusMessageExtract(&sub, aResult.mMaxRssi));
@@ -166,8 +164,7 @@
     otbrError       error = OTBR_ERROR_DBUS;
     DBusMessageIter sub;
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(DBusMessageExtract(&sub, aConfig.mRxOnWhenIdle));
     SuccessOrExit(DBusMessageExtract(&sub, aConfig.mDeviceType));
@@ -201,7 +198,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = DBusMessageExtract(&sub, aPrefix.mPrefix));
     VerifyOrExit(aPrefix.mPrefix.size() <= OTBR_IP6_PREFIX_SIZE, error = OTBR_ERROR_DBUS);
     SuccessOrExit(error = DBusMessageExtract(&sub, aPrefix.mLength));
@@ -236,7 +233,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mPrefix));
     SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mRloc16));
     SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mPreference));
@@ -280,7 +277,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = DBusMessageExtract(&sub, aPrefix.mPrefix));
     SuccessOrExit(error = DBusMessageExtract(&sub, aPrefix.mRloc16));
     SuccessOrExit(error = DBusMessageExtract(&sub, aPrefix.mPreference));
@@ -335,8 +332,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -362,8 +358,7 @@
     otbrError       error = OTBR_ERROR_NONE;
     auto args = std::tie(aCounters.mTxSuccess, aCounters.mRxSuccess, aCounters.mTxFailure, aCounters.mRxFailure);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -397,8 +392,7 @@
                                      aChildInfo.mMessageErrorRate, aChildInfo.mRxOnWhenIdle, aChildInfo.mFullThreadDevice,
                                      aChildInfo.mFullNetworkData, aChildInfo.mIsStateRestoring);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -432,8 +426,7 @@
                                      aNeighborInfo.mMessageErrorRate, aNeighborInfo.mVersion, aNeighborInfo.mRxOnWhenIdle,
                                      aNeighborInfo.mFullThreadDevice, aNeighborInfo.mFullNetworkData, aNeighborInfo.mIsChild);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -461,8 +454,7 @@
     auto            args  = std::tie(aLeaderData.mPartitionId, aLeaderData.mWeighting, aLeaderData.mDataVersion,
                                      aLeaderData.mStableDataVersion, aLeaderData.mLeaderRouterId);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -488,8 +480,7 @@
     otbrError       error = OTBR_ERROR_NONE;
     auto            args  = std::tie(aQuality.mChannel, aQuality.mOccupancy);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -515,8 +506,7 @@
     otbrError       error = OTBR_ERROR_NONE;
     auto            args  = std::tie(aTxtEntry.mKey, aTxtEntry.mValue);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -546,8 +536,7 @@
                          aRegistration.mKeyLeaseTimeTotal, aRegistration.mRemainingLeaseTimeTotal,
                          aRegistration.mRemainingKeyLeaseTimeTotal);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -575,8 +564,7 @@
     auto args = std::tie(aResponseCounters.mSuccess, aResponseCounters.mServerFailure, aResponseCounters.mFormatError,
                          aResponseCounters.mNameExists, aResponseCounters.mRefused, aResponseCounters.mOther);
 
-    VerifyOrExit(dbus_message_iter_get_arg_type(aIter) == DBUS_TYPE_STRUCT, error = OTBR_ERROR_DBUS);
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
     SuccessOrExit(error = ConvertToTuple(&sub, args));
     dbus_message_iter_next(aIter);
 exit:
@@ -607,7 +595,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT);
     SuccessOrExit(error = DBusMessageExtract(&sub, aSrpServerInfo.mState));
     SuccessOrExit(error = DBusMessageExtract(&sub, aSrpServerInfo.mPort));
     SuccessOrExit(error = DBusMessageExtract(&sub, aSrpServerInfo.mAddressMode));
@@ -646,7 +634,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aDnssdCounters.mSuccessResponse));
     SuccessOrExit(error = DBusMessageExtract(&sub, aDnssdCounters.mServerFailureResponse));
@@ -688,7 +676,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aMdnsResponseCounters.mSuccess));
     SuccessOrExit(error = DBusMessageExtract(&sub, aMdnsResponseCounters.mNotFound));
@@ -731,7 +719,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aMdnsTelemetryInfo.mHostRegistrations));
     SuccessOrExit(error = DBusMessageExtract(&sub, aMdnsTelemetryInfo.mServiceRegistrations));
@@ -770,7 +758,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aRadioSpinelMetrics.mRcpTimeoutCount));
     SuccessOrExit(error = DBusMessageExtract(&sub, aRadioSpinelMetrics.mRcpUnexpectedResetCount));
@@ -808,7 +796,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aRcpInterfaceMetrics.mRcpInterfaceType));
     SuccessOrExit(error = DBusMessageExtract(&sub, aRcpInterfaceMetrics.mTransferredFrameCount));
@@ -861,7 +849,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aRadioCoexMetrics.mNumGrantGlitch));
     SuccessOrExit(error = DBusMessageExtract(&sub, aRadioCoexMetrics.mNumTxRequest));
@@ -909,7 +897,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aPacketsAndBytes.mPackets));
     SuccessOrExit(error = DBusMessageExtract(&sub, aPacketsAndBytes.mBytes));
@@ -949,7 +937,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aBorderRoutingCounters.mInboundUnicast));
     SuccessOrExit(error = DBusMessageExtract(&sub, aBorderRoutingCounters.mInboundMulticast));
@@ -988,7 +976,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aNat64State.mPrefixManagerState));
     SuccessOrExit(error = DBusMessageExtract(&sub, aNat64State.mTranslatorState));
@@ -1020,7 +1008,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.m4To6Packets));
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.m4To6Bytes));
@@ -1052,7 +1040,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.m4To6Packets));
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.m6To4Packets));
@@ -1084,7 +1072,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.mTotal));
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.mIcmp));
@@ -1119,7 +1107,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aMapping.mId));
     SuccessOrExit(error = DBusMessageExtract(&sub, aMapping.mIp4));
@@ -1154,7 +1142,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.mUnknown));
     SuccessOrExit(error = DBusMessageExtract(&sub, aCounters.mIllegalPacket));
@@ -1191,7 +1179,7 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
-    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DbusMessageIterRecurse(aIter, &sub, DBUS_TYPE_STRUCT));
 
     SuccessOrExit(error = DBusMessageExtract(&sub, aInfraLinkInfo.mName));
     SuccessOrExit(error = DBusMessageExtract(&sub, aInfraLinkInfo.mIsUp));
@@ -1206,5 +1194,69 @@
     return error;
 }
 
+otbrError DBusMessageEncode(DBusMessageIter *aIter, const TrelInfo &aTrelInfo)
+{
+    DBusMessageIter sub;
+    otbrError       error = OTBR_ERROR_NONE;
+
+    VerifyOrExit(dbus_message_iter_open_container(aIter, DBUS_TYPE_STRUCT, nullptr, &sub), error = OTBR_ERROR_DBUS);
+
+    SuccessOrExit(error = DBusMessageEncode(&sub, aTrelInfo.mEnabled));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aTrelInfo.mNumTrelPeers));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aTrelInfo.mTrelCounters));
+
+    VerifyOrExit(dbus_message_iter_close_container(aIter, &sub), error = OTBR_ERROR_DBUS);
+exit:
+    return error;
+}
+
+otbrError DBusMessageExtract(DBusMessageIter *aIter, TrelInfo &aTrelInfo)
+{
+    DBusMessageIter sub;
+    otbrError       error = OTBR_ERROR_NONE;
+
+    dbus_message_iter_recurse(aIter, &sub);
+
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelInfo.mEnabled));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelInfo.mNumTrelPeers));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelInfo.mTrelCounters));
+
+    dbus_message_iter_next(aIter);
+exit:
+    return error;
+}
+
+otbrError DBusMessageEncode(DBusMessageIter *aIter, const TrelInfo::TrelPacketCounters &aTrelCounters)
+{
+    DBusMessageIter sub;
+    otbrError       error = OTBR_ERROR_NONE;
+    auto            args  = std::tie(aTrelCounters.mTxPackets, aTrelCounters.mTxBytes, aTrelCounters.mTxFailure,
+                                     aTrelCounters.mRxPackets, aTrelCounters.mRxBytes);
+
+    VerifyOrExit(dbus_message_iter_open_container(aIter, DBUS_TYPE_STRUCT, nullptr, &sub), error = OTBR_ERROR_DBUS);
+    SuccessOrExit(error = ConvertToDBusMessage(&sub, args));
+    VerifyOrExit(dbus_message_iter_close_container(aIter, &sub) == true, error = OTBR_ERROR_DBUS);
+exit:
+    return error;
+}
+
+otbrError DBusMessageExtract(DBusMessageIter *aIter, TrelInfo::TrelPacketCounters &aTrelCounters)
+{
+    DBusMessageIter sub;
+    otbrError       error = OTBR_ERROR_NONE;
+
+    dbus_message_iter_recurse(aIter, &sub);
+
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelCounters.mTxPackets));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelCounters.mTxBytes));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelCounters.mTxFailure));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelCounters.mRxPackets));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aTrelCounters.mRxBytes));
+
+    dbus_message_iter_next(aIter);
+exit:
+    return error;
+}
+
 } // namespace DBus
 } // namespace otbr
diff --git a/src/dbus/common/types.hpp b/src/dbus/common/types.hpp
index 15a49ab..14f7fc2 100644
--- a/src/dbus/common/types.hpp
+++ b/src/dbus/common/types.hpp
@@ -703,6 +703,22 @@
     uint32_t    mGlobalUnicastAddresses; ///< The number of global unicast addresses on the infra network interface.
 };
 
+struct TrelInfo
+{
+    struct TrelPacketCounters
+    {
+        uint64_t mTxPackets; ///< Number of packets transmitted through TREL.
+        uint64_t mTxBytes;   ///< Sum of size of packets transmitted through TREL.
+        uint64_t mTxFailure; ///< Number of packet transmission failures through TREL.
+        uint64_t mRxPackets; ///< Number of packets received through TREL.
+        uint64_t mRxBytes;   ///< Sum of size of packets received through TREL.
+    };
+
+    bool               mEnabled;      ///< Whether TREL is enabled.
+    u_int16_t          mNumTrelPeers; ///< The number of TREL peers.
+    TrelPacketCounters mTrelCounters; ///< The TREL counters.
+};
+
 } // namespace DBus
 } // namespace otbr
 
diff --git a/src/dbus/server/dbus_thread_object.cpp b/src/dbus/server/dbus_thread_object.cpp
index a10ea2d..eb2c28d 100644
--- a/src/dbus/server/dbus_thread_object.cpp
+++ b/src/dbus/server/dbus_thread_object.cpp
@@ -42,6 +42,7 @@
 #include <openthread/openthread-system.h>
 #include <openthread/srp_server.h>
 #include <openthread/thread_ftd.h>
+#include <openthread/trel.h>
 #include <openthread/platform/radio.h>
 
 #include "common/api_strings.hpp"
@@ -240,6 +241,8 @@
                                std::bind(&DBusThreadObject::GetMdnsTelemetryInfoHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_DNSSD_COUNTERS,
                                std::bind(&DBusThreadObject::GetDnssdCountersHandler, this, _1));
+    RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_OTBR_VERSION,
+                               std::bind(&DBusThreadObject::GetOtbrVersionHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_OT_HOST_VERSION,
                                std::bind(&DBusThreadObject::GetOtHostVersionHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_OT_RCP_VERSION,
@@ -268,6 +271,8 @@
                                std::bind(&DBusThreadObject::GetNat64Cidr, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_INFRA_LINK_INFO,
                                std::bind(&DBusThreadObject::GetInfraLinkInfo, this, _1));
+    RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_TREL_INFO,
+                               std::bind(&DBusThreadObject::GetTrelInfoHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_DNS_UPSTREAM_QUERY_STATE,
                                std::bind(&DBusThreadObject::GetDnsUpstreamQueryState, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_TELEMETRY_DATA,
@@ -1405,6 +1410,33 @@
 #endif // OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
 }
 
+otError DBusThreadObject::GetTrelInfoHandler(DBusMessageIter &aIter)
+{
+#if OTBR_ENABLE_TREL
+    auto           instance = mNcp->GetThreadHelper()->GetInstance();
+    otError        error    = OT_ERROR_NONE;
+    TrelInfo       trelInfo;
+    otTrelCounters otTrelCounters = *otTrelGetCounters(instance);
+
+    trelInfo.mTrelCounters.mTxPackets = otTrelCounters.mTxPackets;
+    trelInfo.mTrelCounters.mTxBytes   = otTrelCounters.mTxBytes;
+    trelInfo.mTrelCounters.mTxFailure = otTrelCounters.mTxFailure;
+    trelInfo.mTrelCounters.mRxPackets = otTrelCounters.mRxPackets;
+    trelInfo.mTrelCounters.mRxBytes   = otTrelCounters.mRxBytes;
+
+    trelInfo.mNumTrelPeers = otTrelGetNumberOfPeers(instance);
+    trelInfo.mEnabled      = otTrelIsEnabled(instance);
+
+    SuccessOrExit(DBusMessageEncodeToVariant(&aIter, trelInfo), error = OT_ERROR_INVALID_ARGS);
+exit:
+    return error;
+#else  // OTBR_ENABLE_TREL
+    OTBR_UNUSED_VARIABLE(aIter);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+#endif // OTBR_ENABLE_TREL
+}
+
 otError DBusThreadObject::GetTelemetryDataHandler(DBusMessageIter &aIter)
 {
 #if OTBR_ENABLE_TELEMETRY_DATA_API
@@ -1438,6 +1470,7 @@
     otbr::Capabilities capabilities;
 
     capabilities.set_nat64(OTBR_ENABLE_NAT64);
+    capabilities.set_dhcp6_pd(OTBR_ENABLE_DHCP6_PD);
 
     {
         const std::string    dataBytes = capabilities.SerializeAsString();
@@ -1499,6 +1532,17 @@
     mGetPropertyHandlers[aPropertyName] = aHandler;
 }
 
+otError DBusThreadObject::GetOtbrVersionHandler(DBusMessageIter &aIter)
+{
+    otError     error   = OT_ERROR_NONE;
+    std::string version = OTBR_PACKAGE_VERSION;
+
+    SuccessOrExit(DBusMessageEncodeToVariant(&aIter, version), error = OT_ERROR_FAILED);
+
+exit:
+    return error;
+}
+
 otError DBusThreadObject::GetOtHostVersionHandler(DBusMessageIter &aIter)
 {
     otError     error   = OT_ERROR_NONE;
diff --git a/src/dbus/server/dbus_thread_object.hpp b/src/dbus/server/dbus_thread_object.hpp
index 9b23bd6..c3fa8d5 100644
--- a/src/dbus/server/dbus_thread_object.hpp
+++ b/src/dbus/server/dbus_thread_object.hpp
@@ -155,12 +155,14 @@
     otError GetSrpServerInfoHandler(DBusMessageIter &aIter);
     otError GetMdnsTelemetryInfoHandler(DBusMessageIter &aIter);
     otError GetDnssdCountersHandler(DBusMessageIter &aIter);
+    otError GetOtbrVersionHandler(DBusMessageIter &aIter);
     otError GetOtHostVersionHandler(DBusMessageIter &aIter);
     otError GetOtRcpVersionHandler(DBusMessageIter &aIter);
     otError GetThreadVersionHandler(DBusMessageIter &aIter);
     otError GetRadioSpinelMetricsHandler(DBusMessageIter &aIter);
     otError GetRcpInterfaceMetricsHandler(DBusMessageIter &aIter);
     otError GetUptimeHandler(DBusMessageIter &aIter);
+    otError GetTrelInfoHandler(DBusMessageIter &aIter);
     otError GetRadioCoexMetrics(DBusMessageIter &aIter);
     otError GetBorderRoutingCountersHandler(DBusMessageIter &aIter);
     otError GetNat64State(DBusMessageIter &aIter);
diff --git a/src/dbus/server/introspect.xml b/src/dbus/server/introspect.xml
index 0a89818..c6f2601 100644
--- a/src/dbus/server/introspect.xml
+++ b/src/dbus/server/introspect.xml
@@ -167,21 +167,22 @@
             uint8[] prefix_bytes
             uint8 prefix_length
           }
+          uint16 rloc16
           byte preference
-          struct {
-            boolean preferred
-            boolean slaac
-            boolean dhcp
-            boolean configure
-            boolean default_route
-            boolean on_mesh
-            boolean stable
-          }
+          bool preferred
+          bool slaac
+          bool dhcp
+          bool configure
+          bool default_route
+          bool on_mesh
+          bool stable
+          bool nd_dns
+          bool dp
         }
       </literallayout>
     -->
     <method name="AddOnMeshPrefix">
-      <arg name="prefix" type="((ayy)y(bbbbbbb))"/>
+      <arg name="prefix" type="((ayy)qybbbbbbbbb)"/>
     </method>
 
     <!-- RemoveOnMeshPrefix: Remove an on-mesh prefix from the network.
@@ -659,6 +660,11 @@
       <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
     </property>
 
+    <!-- OtbrVersion: The version string of the otbr package. -->
+    <property name="OtbrVersion" type="s" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
+    </property>
+
     <!-- OtHostVersion: The version string of the host build. -->
     <property name="OtHostVersion" type="s" access="read">
       <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
diff --git a/src/mdns/mdns.cpp b/src/mdns/mdns.cpp
index 83ccaec..3774a8b 100644
--- a/src/mdns/mdns.cpp
+++ b/src/mdns/mdns.cpp
@@ -35,6 +35,8 @@
 
 #include "mdns/mdns.hpp"
 
+#if OTBR_ENABLE_MDNS
+
 #include <assert.h>
 
 #include <algorithm>
@@ -79,6 +81,19 @@
     }
 }
 
+void Publisher::PublishKey(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback)
+{
+    otbrError error;
+
+    mKeyRegistrationBeginTime[aName] = Clock::now();
+
+    error = PublishKeyImpl(aName, aKeyData, std::move(aCallback));
+    if (error != OTBR_ERROR_NONE)
+    {
+        UpdateMdnsResponseCounters(mTelemetryInfo.mKeyRegistrations, error);
+    }
+}
+
 void Publisher::OnServiceResolveFailed(std::string aType, std::string aInstanceName, int32_t aErrorCode)
 {
     UpdateMdnsResponseCounters(mTelemetryInfo.mServiceResolutions, DnsErrorToOtbrError(aErrorCode));
@@ -197,6 +212,21 @@
                 aInstanceInfo.mRemoved ? "remove" : "add", aInstanceInfo.mName.c_str(), aInstanceInfo.mHostName.c_str(),
                 aInstanceInfo.mAddresses.size());
 
+    if (!aInstanceInfo.mRemoved)
+    {
+        std::string addressesString;
+
+        for (const auto &address : aInstanceInfo.mAddresses)
+        {
+            addressesString += address.ToString() + ",";
+        }
+        if (addressesString.size())
+        {
+            addressesString.pop_back();
+        }
+        otbrLogInfo("addresses: [ %s ]", addressesString.c_str());
+    }
+
     DnsUtils::CheckServiceNameSanity(aType);
 
     assert(aInstanceInfo.mNetifIndex > 0);
@@ -318,7 +348,7 @@
     return aName + "." + aType + ".local";
 }
 
-std::string Publisher::MakeFullHostName(const std::string &aName)
+std::string Publisher::MakeFullName(const std::string &aName)
 {
     return aName + ".local";
 }
@@ -354,6 +384,13 @@
     return it != mServiceRegistrations.end() ? it->second.get() : nullptr;
 }
 
+Publisher::ServiceRegistration *Publisher::FindServiceRegistration(const std::string &aNameAndType)
+{
+    auto it = mServiceRegistrations.find(MakeFullName(aNameAndType));
+
+    return it != mServiceRegistrations.end() ? it->second.get() : nullptr;
+}
+
 Publisher::ResultCallback Publisher::HandleDuplicateServiceRegistration(const std::string &aHostName,
                                                                         const std::string &aName,
                                                                         const std::string &aType,
@@ -464,6 +501,82 @@
     return it != mHostRegistrations.end() ? it->second.get() : nullptr;
 }
 
+Publisher::ResultCallback Publisher::HandleDuplicateKeyRegistration(const std::string &aName,
+                                                                    const KeyData     &aKeyData,
+                                                                    ResultCallback   &&aCallback)
+{
+    KeyRegistration *keyReg = FindKeyRegistration(aName);
+
+    VerifyOrExit(keyReg != nullptr);
+
+    if (keyReg->IsOutdated(aName, aKeyData))
+    {
+        otbrLogInfo("Removing existing key %s: outdated", aName.c_str());
+        RemoveKeyRegistration(keyReg->mName, OTBR_ERROR_ABORTED);
+    }
+    else if (keyReg->IsCompleted())
+    {
+        // Returns success if the same key has already been
+        // registered with exactly the same parameters.
+        std::move(aCallback)(OTBR_ERROR_NONE);
+    }
+    else
+    {
+        // If the same key is being registered with the same parameters,
+        // let's join the waiting queue for the result.
+        keyReg->mCallback = std::bind(
+            [](std::shared_ptr<ResultCallback> aExistingCallback, std::shared_ptr<ResultCallback> aNewCallback,
+               otbrError aError) {
+                std::move (*aExistingCallback)(aError);
+                std::move (*aNewCallback)(aError);
+            },
+            std::make_shared<ResultCallback>(std::move(keyReg->mCallback)),
+            std::make_shared<ResultCallback>(std::move(aCallback)), std::placeholders::_1);
+    }
+
+exit:
+    return std::move(aCallback);
+}
+
+void Publisher::AddKeyRegistration(KeyRegistrationPtr &&aKeyReg)
+{
+    mKeyRegistrations.emplace(MakeFullKeyName(aKeyReg->mName), std::move(aKeyReg));
+}
+
+void Publisher::RemoveKeyRegistration(const std::string &aName, otbrError aError)
+{
+    auto               it = mKeyRegistrations.find(MakeFullKeyName(aName));
+    KeyRegistrationPtr keyReg;
+
+    otbrLogInfo("Removing key %s", aName.c_str());
+    VerifyOrExit(it != mKeyRegistrations.end());
+
+    // Keep the KeyRegistration around before calling `Complete`
+    // to invoke the callback. This is for avoiding invalid access
+    // to the KeyRegistration when it's freed from the callback.
+    keyReg = std::move(it->second);
+    mKeyRegistrations.erase(it);
+    keyReg->Complete(aError);
+    otbrLogInfo("Removed key %s", aName.c_str());
+
+exit:
+    return;
+}
+
+Publisher::KeyRegistration *Publisher::FindKeyRegistration(const std::string &aName)
+{
+    auto it = mKeyRegistrations.find(MakeFullKeyName(aName));
+
+    return it != mKeyRegistrations.end() ? it->second.get() : nullptr;
+}
+
+Publisher::KeyRegistration *Publisher::FindKeyRegistration(const std::string &aName, const std::string &aType)
+{
+    auto it = mKeyRegistrations.find(MakeFullServiceName(aName, aType));
+
+    return it != mKeyRegistrations.end() ? it->second.get() : nullptr;
+}
+
 Publisher::Registration::~Registration(void)
 {
     TriggerCompleteCallback(OTBR_ERROR_ABORTED);
@@ -515,6 +628,26 @@
     }
 }
 
+bool Publisher::KeyRegistration::IsOutdated(const std::string &aName, const KeyData &aKeyData) const
+{
+    return !(mName == aName && mKeyData == aKeyData);
+}
+
+void Publisher::KeyRegistration::Complete(otbrError aError)
+{
+    OnComplete(aError);
+    Registration::TriggerCompleteCallback(aError);
+}
+
+void Publisher::KeyRegistration::OnComplete(otbrError aError)
+{
+    if (!IsCompleted())
+    {
+        mPublisher->UpdateMdnsResponseCounters(mPublisher->mTelemetryInfo.mKeyRegistrations, aError);
+        mPublisher->UpdateKeyRegistrationEmaLatency(mName, aError);
+    }
+}
+
 void Publisher::UpdateMdnsResponseCounters(otbr::MdnsResponseCounters &aCounters, otbrError aError)
 {
     switch (aError)
@@ -593,6 +726,18 @@
     }
 }
 
+void Publisher::UpdateKeyRegistrationEmaLatency(const std::string &aKeyName, otbrError aError)
+{
+    auto it = mKeyRegistrationBeginTime.find(aKeyName);
+
+    if (it != mKeyRegistrationBeginTime.end())
+    {
+        uint32_t latency = std::chrono::duration_cast<Milliseconds>(Clock::now() - it->second).count();
+        UpdateEmaLatency(mTelemetryInfo.mKeyRegistrationEmaLatency, latency, aError);
+        mKeyRegistrationBeginTime.erase(it);
+    }
+}
+
 void Publisher::UpdateServiceInstanceResolutionEmaLatency(const std::string &aInstanceName,
                                                           const std::string &aType,
                                                           otbrError          aError)
@@ -619,5 +764,22 @@
     }
 }
 
+void Publisher::AddAddress(AddressList &aAddressList, const Ip6Address &aAddress)
+{
+    aAddressList.push_back(aAddress);
+}
+
+void Publisher::RemoveAddress(AddressList &aAddressList, const Ip6Address &aAddress)
+{
+    auto it = std::find(aAddressList.begin(), aAddressList.end(), aAddress);
+
+    if (it != aAddressList.end())
+    {
+        aAddressList.erase(it);
+    }
+}
+
 } // namespace Mdns
 } // namespace otbr
+
+#endif // OTBR_ENABLE_MDNS
diff --git a/src/mdns/mdns.hpp b/src/mdns/mdns.hpp
index 2cb4105..ed2dc7c 100644
--- a/src/mdns/mdns.hpp
+++ b/src/mdns/mdns.hpp
@@ -36,6 +36,10 @@
 
 #include "openthread-br/config.h"
 
+#ifndef OTBR_ENABLE_MDNS
+#define OTBR_ENABLE_MDNS (OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD)
+#endif
+
 #include <functional>
 #include <list>
 #include <map>
@@ -119,6 +123,7 @@
     typedef std::vector<TxtEntry>    TxtList;
     typedef std::vector<std::string> SubTypeList;
     typedef std::vector<Ip6Address>  AddressList;
+    typedef std::vector<uint8_t>     KeyData;
 
     /**
      * This structure represents information of a discovered service instance.
@@ -136,6 +141,9 @@
         uint16_t    mWeight   = 0;       ///< Service weight.
         TxtData     mTxtData;            ///< TXT RDATA bytes.
         uint32_t    mTtl = 0;            ///< Service TTL.
+
+        void AddAddress(const Ip6Address &aAddress) { Publisher::AddAddress(mAddresses, aAddress); }
+        void RemoveAddress(const Ip6Address &aAddress) { Publisher::RemoveAddress(mAddresses, aAddress); }
     };
 
     /**
@@ -148,6 +156,9 @@
         AddressList mAddresses;      ///< IP6 addresses.
         uint32_t    mNetifIndex = 0; ///< Network interface.
         uint32_t    mTtl        = 0; ///< Host TTL.
+
+        void AddAddress(const Ip6Address &aAddress) { Publisher::AddAddress(mAddresses, aAddress); }
+        void RemoveAddress(const Ip6Address &aAddress) { Publisher::RemoveAddress(mAddresses, aAddress); }
     };
 
     /**
@@ -269,6 +280,29 @@
     virtual void UnpublishHost(const std::string &aName, ResultCallback &&aCallback) = 0;
 
     /**
+     * This method publishes or updates a key record for a name.
+     *
+     * @param[in] aName       The name associated with key record (can be host name or service instance name).
+     * @param[in] aKeyData    The key data to publish.
+     * @param[in] aCallback   The callback for receiving the publishing result.`OTBR_ERROR_NONE` will be
+     *                        returned if the operation is successful and all other values indicate a
+     *                        failure. Specifically, `OTBR_ERROR_DUPLICATED` indicates that the name has
+     *                        already been published and the caller can re-publish with a new name if an
+     *                        alternative name is available/acceptable.
+     *
+     */
+    void PublishKey(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback);
+
+    /**
+     * This method un-publishes a key record
+     *
+     * @param[in] aName      The name associated with key record.
+     * @param[in] aCallback  The callback for receiving the publishing result.
+     *
+     */
+    virtual void UnpublishKey(const std::string &aName, ResultCallback &&aCallback) = 0;
+
+    /**
      * This method subscribes a given service or service instance.
      *
      * If @p aInstanceName is not empty, this method subscribes the service instance. Otherwise, this method subscribes
@@ -503,15 +537,43 @@
         void OnComplete(otbrError aError);
     };
 
+    class KeyRegistration : public Registration
+    {
+    public:
+        std::string mName;
+        KeyData     mKeyData;
+
+        KeyRegistration(std::string aName, KeyData aKeyData, ResultCallback &&aCallback, Publisher *aPublisher)
+            : Registration(std::move(aCallback), aPublisher)
+            , mName(std::move(aName))
+            , mKeyData(std::move(aKeyData))
+        {
+        }
+
+        ~KeyRegistration(void) { OnComplete(OTBR_ERROR_ABORTED); }
+
+        void Complete(otbrError aError);
+
+        // Tells whether this `KeyRegistration` object is outdated comparing to the given parameters.
+        bool IsOutdated(const std::string &aName, const KeyData &aKeyData) const;
+
+    private:
+        void OnComplete(otbrError aError);
+    };
+
     using ServiceRegistrationPtr = std::unique_ptr<ServiceRegistration>;
     using ServiceRegistrationMap = std::map<std::string, ServiceRegistrationPtr>;
     using HostRegistrationPtr    = std::unique_ptr<HostRegistration>;
     using HostRegistrationMap    = std::map<std::string, HostRegistrationPtr>;
+    using KeyRegistrationPtr     = std::unique_ptr<KeyRegistration>;
+    using KeyRegistrationMap     = std::map<std::string, KeyRegistrationPtr>;
 
     static SubTypeList SortSubTypeList(SubTypeList aSubTypeList);
     static AddressList SortAddressList(AddressList aAddressList);
+    static std::string MakeFullName(const std::string &aName);
     static std::string MakeFullServiceName(const std::string &aName, const std::string &aType);
-    static std::string MakeFullHostName(const std::string &aName);
+    static std::string MakeFullHostName(const std::string &aName) { return MakeFullName(aName); }
+    static std::string MakeFullKeyName(const std::string &aName) { return MakeFullName(aName); }
 
     virtual otbrError PublishServiceImpl(const std::string &aHostName,
                                          const std::string &aName,
@@ -525,6 +587,8 @@
                                       const AddressList &aAddresses,
                                       ResultCallback   &&aCallback) = 0;
 
+    virtual otbrError PublishKeyImpl(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback) = 0;
+
     virtual void OnServiceResolveFailedImpl(const std::string &aType,
                                             const std::string &aInstanceName,
                                             int32_t            aErrorCode) = 0;
@@ -536,6 +600,7 @@
     void AddServiceRegistration(ServiceRegistrationPtr &&aServiceReg);
     void RemoveServiceRegistration(const std::string &aName, const std::string &aType, otbrError aError);
     ServiceRegistration *FindServiceRegistration(const std::string &aName, const std::string &aType);
+    ServiceRegistration *FindServiceRegistration(const std::string &aNameAndType);
 
     void OnServiceResolved(std::string aType, DiscoveredInstanceInfo aInstanceInfo);
     void OnServiceResolveFailed(std::string aType, std::string aInstanceName, int32_t aErrorCode);
@@ -558,10 +623,19 @@
                                                    const AddressList &aAddresses,
                                                    ResultCallback   &&aCallback);
 
+    ResultCallback HandleDuplicateKeyRegistration(const std::string &aName,
+                                                  const KeyData     &aKeyData,
+                                                  ResultCallback   &&aCallback);
+
     void              AddHostRegistration(HostRegistrationPtr &&aHostReg);
     void              RemoveHostRegistration(const std::string &aName, otbrError aError);
     HostRegistration *FindHostRegistration(const std::string &aName);
 
+    void             AddKeyRegistration(KeyRegistrationPtr &&aKeyReg);
+    void             RemoveKeyRegistration(const std::string &aName, otbrError aError);
+    KeyRegistration *FindKeyRegistration(const std::string &aName);
+    KeyRegistration *FindKeyRegistration(const std::string &aName, const std::string &aType);
+
     static void UpdateMdnsResponseCounters(MdnsResponseCounters &aCounters, otbrError aError);
     static void UpdateEmaLatency(uint32_t &aEmaLatency, uint32_t aLatency, otbrError aError);
 
@@ -569,13 +643,18 @@
                                              const std::string &aType,
                                              otbrError          aError);
     void UpdateHostRegistrationEmaLatency(const std::string &aHostName, otbrError aError);
+    void UpdateKeyRegistrationEmaLatency(const std::string &aKeyName, otbrError aError);
     void UpdateServiceInstanceResolutionEmaLatency(const std::string &aInstanceName,
                                                    const std::string &aType,
                                                    otbrError          aError);
     void UpdateHostResolutionEmaLatency(const std::string &aHostName, otbrError aError);
 
+    static void AddAddress(AddressList &aAddressList, const Ip6Address &aAddress);
+    static void RemoveAddress(AddressList &aAddressList, const Ip6Address &aAddress);
+
     ServiceRegistrationMap mServiceRegistrations;
     HostRegistrationMap    mHostRegistrations;
+    KeyRegistrationMap     mKeyRegistrations;
 
     struct DiscoverCallback
     {
@@ -603,6 +682,8 @@
     std::map<std::pair<std::string, std::string>, Timepoint> mServiceRegistrationBeginTime;
     // host name -> the timepoint to begin host registration
     std::map<std::string, Timepoint> mHostRegistrationBeginTime;
+    // key name -> the timepoint to begin key registration
+    std::map<std::string, Timepoint> mKeyRegistrationBeginTime;
     // {instance name, service type} -> the timepoint to begin service resolution
     std::map<std::pair<std::string, std::string>, Timepoint> mServiceInstanceResolutionBeginTime;
     // host name -> the timepoint to begin host resolution
diff --git a/src/mdns/mdns_avahi.cpp b/src/mdns/mdns_avahi.cpp
index c0d8762..2f3984d 100644
--- a/src/mdns/mdns_avahi.cpp
+++ b/src/mdns/mdns_avahi.cpp
@@ -492,6 +492,11 @@
     ReleaseGroup(mEntryGroup);
 }
 
+PublisherAvahi::AvahiKeyRegistration::~AvahiKeyRegistration(void)
+{
+    ReleaseGroup(mEntryGroup);
+}
+
 otbrError PublisherAvahi::Start(void)
 {
     otbrError error      = OTBR_ERROR_NONE;
@@ -565,10 +570,6 @@
     case AVAHI_ENTRY_GROUP_UNCOMMITED:
     case AVAHI_ENTRY_GROUP_REGISTERING:
         break;
-
-    default:
-        assert(false);
-        break;
     }
 }
 
@@ -576,6 +577,7 @@
 {
     ServiceRegistration *serviceReg;
     HostRegistration    *hostReg;
+    KeyRegistration     *keyReg;
 
     if ((serviceReg = FindServiceRegistration(aGroup)) != nullptr)
     {
@@ -599,6 +601,17 @@
             RemoveHostRegistration(hostReg->mName, aError);
         }
     }
+    else if ((keyReg = FindKeyRegistration(aGroup)) != nullptr)
+    {
+        if (aError == OTBR_ERROR_NONE)
+        {
+            keyReg->Complete(aError);
+        }
+        else
+        {
+            RemoveKeyRegistration(keyReg->mName, aError);
+        }
+    }
     else
     {
         otbrLogWarning("No registered service or host matches avahi group @%p", aGroup);
@@ -679,10 +692,6 @@
     case AVAHI_CLIENT_CONNECTING:
         otbrLogInfo("Avahi client is connecting to the server");
         break;
-
-    default:
-        assert(false);
-        break;
     }
 }
 
@@ -841,6 +850,64 @@
     std::move(aCallback)(error);
 }
 
+otbrError PublisherAvahi::PublishKeyImpl(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback)
+{
+    otbrError        error      = OTBR_ERROR_NONE;
+    int              avahiError = AVAHI_OK;
+    std::string      fullKeyName;
+    AvahiEntryGroup *group = nullptr;
+
+    VerifyOrExit(mState == State::kReady, error = OTBR_ERROR_INVALID_STATE);
+    VerifyOrExit(mClient != nullptr, error = OTBR_ERROR_INVALID_STATE);
+
+    aCallback = HandleDuplicateKeyRegistration(aName, aKeyData, std::move(aCallback));
+    VerifyOrExit(!aCallback.IsNull());
+
+    VerifyOrExit((group = CreateGroup(mClient)) != nullptr, error = OTBR_ERROR_MDNS);
+
+    fullKeyName = MakeFullKeyName(aName);
+
+    avahiError = avahi_entry_group_add_record(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_UNIQUE,
+                                              fullKeyName.c_str(), AVAHI_DNS_CLASS_IN, kDnsKeyRecordType, kDefaultTtl,
+                                              aKeyData.data(), aKeyData.size());
+    VerifyOrExit(avahiError == AVAHI_OK);
+
+    otbrLogInfo("Commit avahi key record for %s", aName.c_str());
+    avahiError = avahi_entry_group_commit(group);
+    VerifyOrExit(avahiError == AVAHI_OK);
+
+    AddKeyRegistration(std::unique_ptr<AvahiKeyRegistration>(
+        new AvahiKeyRegistration(aName, aKeyData, std::move(aCallback), group, this)));
+
+exit:
+    if (avahiError != AVAHI_OK || error != OTBR_ERROR_NONE)
+    {
+        if (avahiError != AVAHI_OK)
+        {
+            error = OTBR_ERROR_MDNS;
+            otbrLogErr("Failed to publish key record - avahi error: %s!", avahi_strerror(avahiError));
+        }
+
+        if (group != nullptr)
+        {
+            ReleaseGroup(group);
+        }
+        std::move(aCallback)(error);
+    }
+    return error;
+}
+
+void PublisherAvahi::UnpublishKey(const std::string &aName, ResultCallback &&aCallback)
+{
+    otbrError error = OTBR_ERROR_NONE;
+
+    VerifyOrExit(mState == Publisher::State::kReady, error = OTBR_ERROR_INVALID_STATE);
+    RemoveKeyRegistration(aName, OTBR_ERROR_ABORTED);
+
+exit:
+    std::move(aCallback)(error);
+}
+
 otbrError PublisherAvahi::TxtDataToAvahiStringList(const TxtData    &aTxtData,
                                                    AvahiStringList  *aBuffer,
                                                    size_t            aBufferSize,
@@ -922,6 +989,23 @@
     return result;
 }
 
+Publisher::KeyRegistration *PublisherAvahi::FindKeyRegistration(const AvahiEntryGroup *aEntryGroup)
+{
+    KeyRegistration *result = nullptr;
+
+    for (const auto &entry : mKeyRegistrations)
+    {
+        const auto &keyReg = static_cast<const AvahiKeyRegistration &>(*entry.second);
+        if (keyReg.GetEntryGroup() == aEntryGroup)
+        {
+            result = entry.second.get();
+            break;
+        }
+    }
+
+    return result;
+}
+
 void PublisherAvahi::SubscribeService(const std::string &aType, const std::string &aInstanceName)
 {
     auto service = MakeUnique<ServiceSubscription>(*this, aType, aInstanceName);
@@ -955,7 +1039,7 @@
                           return aService->mType == aType && aService->mInstanceName == aInstanceName;
                       });
 
-    assert(it != mSubscribedServices.end());
+    VerifyOrExit(it != mSubscribedServices.end());
 
     {
         std::unique_ptr<ServiceSubscription> service = std::move(*it);
@@ -1014,7 +1098,7 @@
         mSubscribedHosts.begin(), mSubscribedHosts.end(),
         [&aHostName](const std::unique_ptr<HostSubscription> &aHost) { return aHost->mHostName == aHostName; });
 
-    assert(it != mSubscribedHosts.end());
+    VerifyOrExit(it != mSubscribedHosts.end());
 
     {
         std::unique_ptr<HostSubscription> host = std::move(*it);
@@ -1238,6 +1322,7 @@
         {
             avahi_record_browser_free(mRecordBrowser);
             mRecordBrowser = nullptr;
+            mInstanceInfo.mAddresses.clear();
         }
         // NOTE: This `ServiceResolver` object may be freed in `OnServiceResolved`.
         mRecordBrowser = avahi_record_browser_new(mPublisherAvahi->mClient, aInterfaceIndex, AVAHI_PROTO_UNSPEC,
@@ -1299,7 +1384,7 @@
             aName, aInterfaceIndex, aProtocol, aClazz, aType, aSize, static_cast<int>(aFlags),
             static_cast<int>(aEvent));
 
-    VerifyOrExit(aEvent == AVAHI_BROWSER_NEW);
+    VerifyOrExit(aEvent == AVAHI_BROWSER_NEW || aEvent == AVAHI_BROWSER_REMOVE);
     VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE || aSize == OTBR_IP4_ADDRESS_SIZE,
                  otbrLogErr("Unexpected address data length: %zu", aSize), avahiError = AVAHI_ERR_INVALID_ADDRESS);
     VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE, otbrLogInfo("IPv4 address ignored"),
@@ -1308,9 +1393,16 @@
 
     VerifyOrExit(!address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback() && !address.IsUnspecified(),
                  avahiError = AVAHI_ERR_INVALID_ADDRESS);
-    otbrLogInfo("Resolved host address: %s", address.ToString().c_str());
-
-    mInstanceInfo.mAddresses.push_back(std::move(address));
+    otbrLogInfo("Resolved host address: %s %s", aEvent == AVAHI_BROWSER_NEW ? "add" : "remove",
+                address.ToString().c_str());
+    if (aEvent == AVAHI_BROWSER_NEW)
+    {
+        mInstanceInfo.AddAddress(address);
+    }
+    else
+    {
+        mInstanceInfo.RemoveAddress(address);
+    }
     resolved = true;
 
 exit:
@@ -1423,7 +1515,7 @@
             aName, aInterfaceIndex, aProtocol, aClazz, aType, aSize, static_cast<int>(aFlags),
             static_cast<int>(aEvent));
 
-    VerifyOrExit(aEvent == AVAHI_BROWSER_NEW);
+    VerifyOrExit(aEvent == AVAHI_BROWSER_NEW || aEvent == AVAHI_BROWSER_REMOVE);
     VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE || aSize == OTBR_IP4_ADDRESS_SIZE,
                  otbrLogErr("Unexpected address data length: %zu", aSize), avahiError = AVAHI_ERR_INVALID_ADDRESS);
     VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE, otbrLogInfo("IPv4 address ignored"),
@@ -1432,10 +1524,18 @@
 
     VerifyOrExit(!address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback() && !address.IsUnspecified(),
                  avahiError = AVAHI_ERR_INVALID_ADDRESS);
-    otbrLogInfo("Resolved host address: %s", address.ToString().c_str());
+    otbrLogInfo("Resolved host address: %s %s", aEvent == AVAHI_BROWSER_NEW ? "add" : "remove",
+                address.ToString().c_str());
 
     mHostInfo.mHostName = std::string(aName) + ".";
-    mHostInfo.mAddresses.push_back(std::move(address));
+    if (aEvent == AVAHI_BROWSER_NEW)
+    {
+        mHostInfo.AddAddress(address);
+    }
+    else
+    {
+        mHostInfo.RemoveAddress(address);
+    }
     mHostInfo.mNetifIndex = static_cast<uint32_t>(aInterfaceIndex);
     // TODO: Use a more proper TTL
     mHostInfo.mTtl = kDefaultTtl;
diff --git a/src/mdns/mdns_avahi.hpp b/src/mdns/mdns_avahi.hpp
index 844e242..d42f9a2 100644
--- a/src/mdns/mdns_avahi.hpp
+++ b/src/mdns/mdns_avahi.hpp
@@ -78,6 +78,7 @@
 
     void      UnpublishService(const std::string &aName, const std::string &aType, ResultCallback &&aCallback) override;
     void      UnpublishHost(const std::string &aName, ResultCallback &&aCallback) override;
+    void      UnpublishKey(const std::string &aName, ResultCallback &&aCallback) override;
     void      SubscribeService(const std::string &aType, const std::string &aInstanceName) override;
     void      UnsubscribeService(const std::string &aType, const std::string &aInstanceName) override;
     void      SubscribeHost(const std::string &aHostName) override;
@@ -97,6 +98,7 @@
     otbrError PublishHostImpl(const std::string &aName,
                               const AddressList &aAddresses,
                               ResultCallback   &&aCallback) override;
+    otbrError PublishKeyImpl(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback) override;
     void      OnServiceResolveFailedImpl(const std::string &aType,
                                          const std::string &aInstanceName,
                                          int32_t            aErrorCode) override;
@@ -106,6 +108,7 @@
 private:
     static constexpr size_t   kMaxSizeOfTxtRecord = 1024;
     static constexpr uint32_t kDefaultTtl         = 10; // In seconds.
+    static constexpr uint16_t kDnsKeyRecordType   = 25;
 
     class AvahiServiceRegistration : public ServiceRegistration
     {
@@ -158,6 +161,26 @@
         AvahiEntryGroup *mEntryGroup;
     };
 
+    class AvahiKeyRegistration : public KeyRegistration
+    {
+    public:
+        AvahiKeyRegistration(const std::string &aName,
+                             const KeyData     &aKeyData,
+                             ResultCallback   &&aCallback,
+                             AvahiEntryGroup   *aEntryGroup,
+                             PublisherAvahi    *aPublisher)
+            : KeyRegistration(aName, aKeyData, std::move(aCallback), aPublisher)
+            , mEntryGroup(aEntryGroup)
+        {
+        }
+
+        ~AvahiKeyRegistration(void) override;
+        const AvahiEntryGroup *GetEntryGroup(void) const { return mEntryGroup; }
+
+    private:
+        AvahiEntryGroup *mEntryGroup;
+    };
+
     struct Subscription : private ::NonCopyable
     {
         PublisherAvahi *mPublisherAvahi;
@@ -347,6 +370,7 @@
 
     ServiceRegistration *FindServiceRegistration(const AvahiEntryGroup *aEntryGroup);
     HostRegistration    *FindHostRegistration(const AvahiEntryGroup *aEntryGroup);
+    KeyRegistration     *FindKeyRegistration(const AvahiEntryGroup *aEntryGroup);
 
     AvahiClient                 *mClient;
     std::unique_ptr<AvahiPoller> mPoller;
diff --git a/src/mdns/mdns_mdnssd.cpp b/src/mdns/mdns_mdnssd.cpp
index 2dc4c27..689634a 100644
--- a/src/mdns/mdns_mdnssd.cpp
+++ b/src/mdns/mdns_mdnssd.cpp
@@ -259,6 +259,7 @@
 
     mServiceRegistrations.clear();
     mHostRegistrations.clear();
+    mKeyRegistrations.clear();
     DeallocateHostsRef();
 
     mSubscribedServices.clear();
@@ -444,11 +445,12 @@
 
 otbrError PublisherMDnsSd::DnssdServiceRegistration::Register(void)
 {
-    std::string         fullHostName;
-    std::string         regType            = MakeRegType(mType, mSubTypeList);
-    const char         *hostNameCString    = nullptr;
-    const char         *serviceNameCString = nullptr;
-    DNSServiceErrorType dnsError;
+    std::string           fullHostName;
+    std::string           regType            = MakeRegType(mType, mSubTypeList);
+    const char           *hostNameCString    = nullptr;
+    const char           *serviceNameCString = nullptr;
+    DnssdKeyRegistration *keyReg;
+    DNSServiceErrorType   dnsError;
 
     if (!mHostName.empty())
     {
@@ -461,6 +463,13 @@
         serviceNameCString = mName.c_str();
     }
 
+    keyReg = static_cast<DnssdKeyRegistration *>(GetPublisher().FindKeyRegistration(mName, mType));
+
+    if (keyReg != nullptr)
+    {
+        keyReg->Unregister();
+    }
+
     otbrLogInfo("Registering service %s.%s", mName.c_str(), regType.c_str());
 
     dnsError = DNSServiceRegister(&mServiceRef, kDNSServiceFlagsNoAutoRename, kDNSServiceInterfaceIndexAny,
@@ -473,17 +482,44 @@
         HandleRegisterResult(/* aFlags */ 0, dnsError);
     }
 
+    if (keyReg != nullptr)
+    {
+        keyReg->Register();
+    }
+
     return GetPublisher().DnsErrorToOtbrError(dnsError);
 }
 
 void PublisherMDnsSd::DnssdServiceRegistration::Unregister(void)
 {
-    if (mServiceRef != nullptr)
+    DnssdKeyRegistration *keyReg = mRelatedKeyReg;
+
+    VerifyOrExit(mServiceRef != nullptr);
+
+    // If we have a related key registration associated with this
+    // service registration, we first unregister it and after we
+    // deallocated the `mServiceRef` try to register it again
+    // (which will add it as an individual record not tied to a
+    // service registration). Note that the `keyReg->Unregister()`
+    // will clear the `mRelatedKeyReg` field, so we need to keep
+    // a local copy to it in `keyReg`.
+
+    if (keyReg != nullptr)
     {
-        GetPublisher().HandleServiceRefDeallocating(mServiceRef);
-        DNSServiceRefDeallocate(mServiceRef);
-        mServiceRef = nullptr;
+        keyReg->Unregister();
     }
+
+    GetPublisher().HandleServiceRefDeallocating(mServiceRef);
+    DNSServiceRefDeallocate(mServiceRef);
+    mServiceRef = nullptr;
+
+    if (keyReg != nullptr)
+    {
+        keyReg->Register();
+    }
+
+exit:
+    return;
 }
 
 void PublisherMDnsSd::DnssdServiceRegistration::HandleRegisterResult(DNSServiceRef       aServiceRef,
@@ -504,6 +540,11 @@
 
 void PublisherMDnsSd::DnssdServiceRegistration::HandleRegisterResult(DNSServiceFlags aFlags, DNSServiceErrorType aError)
 {
+    if (mRelatedKeyReg != nullptr)
+    {
+        mRelatedKeyReg->HandleRegisterResult(aError);
+    }
+
     if ((aError == kDNSServiceErr_NoError) && (aFlags & kDNSServiceFlagsAdd))
     {
         otbrLogInfo("Successfully registered service %s.%s", mName.c_str(), mType.c_str());
@@ -589,8 +630,8 @@
                                                                   DNSServiceErrorType aError,
                                                                   void               *aContext)
 {
-    OT_UNUSED_VARIABLE(aServiceRef);
-    OT_UNUSED_VARIABLE(aFlags);
+    OTBR_UNUSED_VARIABLE(aServiceRef);
+    OTBR_UNUSED_VARIABLE(aFlags);
 
     static_cast<DnssdHostRegistration *>(aContext)->HandleRegisterResult(aRecordRef, aError);
 }
@@ -629,6 +670,119 @@
     }
 }
 
+otbrError PublisherMDnsSd::DnssdKeyRegistration::Register(void)
+{
+    DNSServiceErrorType       dnsError = kDNSServiceErr_NoError;
+    DnssdServiceRegistration *serviceReg;
+
+    otbrLogInfo("Registering new key %s", mName.c_str());
+
+    serviceReg = static_cast<DnssdServiceRegistration *>(GetPublisher().FindServiceRegistration(mName));
+
+    if ((serviceReg != nullptr) && (serviceReg->mServiceRef != nullptr))
+    {
+        otbrLogInfo("Key %s is being registered as a record of an existing service registration", mName.c_str());
+
+        dnsError = DNSServiceAddRecord(serviceReg->mServiceRef, &mRecordRef, kDNSServiceFlagsUnique,
+                                       kDNSServiceType_KEY, mKeyData.size(), mKeyData.data(), /* ttl */ 0);
+
+        VerifyOrExit(dnsError == kDNSServiceErr_NoError);
+
+        mRelatedServiceReg         = serviceReg;
+        serviceReg->mRelatedKeyReg = this;
+
+        if (mRelatedServiceReg->IsCompleted())
+        {
+            HandleRegisterResult(kDNSServiceErr_NoError);
+        }
+
+        // If related service registration is not yet finished,
+        // we wait for service registration completion to signal
+        // key record registration as well.
+    }
+    else
+    {
+        otbrLogInfo("Key %s is being registered individually", mName.c_str());
+
+        dnsError = GetPublisher().CreateSharedHostsRef();
+        VerifyOrExit(dnsError == kDNSServiceErr_NoError);
+
+        dnsError = DNSServiceRegisterRecord(GetPublisher().mHostsRef, &mRecordRef, kDNSServiceFlagsUnique,
+                                            kDNSServiceInterfaceIndexAny, MakeFullKeyName(mName).c_str(),
+                                            kDNSServiceType_KEY, kDNSServiceClass_IN, mKeyData.size(), mKeyData.data(),
+                                            /* ttl */ 0, HandleRegisterResult, this);
+        VerifyOrExit(dnsError == kDNSServiceErr_NoError);
+    }
+
+exit:
+    if (dnsError != kDNSServiceErr_NoError)
+    {
+        HandleRegisterResult(dnsError);
+    }
+
+    return GetPublisher().DnsErrorToOtbrError(dnsError);
+}
+
+void PublisherMDnsSd::DnssdKeyRegistration::Unregister(void)
+{
+    DNSServiceErrorType dnsError;
+    DNSServiceRef       serviceRef;
+
+    VerifyOrExit(mRecordRef != nullptr);
+
+    if (mRelatedServiceReg != nullptr)
+    {
+        serviceRef = mRelatedServiceReg->mServiceRef;
+
+        mRelatedServiceReg->mRelatedKeyReg = nullptr;
+        mRelatedServiceReg                 = nullptr;
+
+        otbrLogInfo("Unregistering key %s (was registered as a record of a service)", mName.c_str());
+    }
+    else
+    {
+        serviceRef = GetPublisher().mHostsRef;
+
+        otbrLogInfo("Unregistering key %s (was registered individually)", mName.c_str());
+    }
+
+    VerifyOrExit(serviceRef != nullptr);
+
+    dnsError = DNSServiceRemoveRecord(serviceRef, mRecordRef, /* flags */ 0);
+
+    otbrLogInfo("Unregistered key %s: error:%s", mName.c_str(), DNSErrorToString(dnsError));
+
+exit:
+    return;
+}
+
+void PublisherMDnsSd::DnssdKeyRegistration::HandleRegisterResult(DNSServiceRef       aServiceRef,
+                                                                 DNSRecordRef        aRecordRef,
+                                                                 DNSServiceFlags     aFlags,
+                                                                 DNSServiceErrorType aError,
+                                                                 void               *aContext)
+{
+    OTBR_UNUSED_VARIABLE(aServiceRef);
+    OTBR_UNUSED_VARIABLE(aRecordRef);
+    OTBR_UNUSED_VARIABLE(aFlags);
+
+    static_cast<DnssdKeyRegistration *>(aContext)->HandleRegisterResult(aError);
+}
+
+void PublisherMDnsSd::DnssdKeyRegistration::HandleRegisterResult(DNSServiceErrorType aError)
+{
+    if (aError != kDNSServiceErr_NoError)
+    {
+        otbrLogErr("Failed to register key %s: %s", mName.c_str(), DNSErrorToString(aError));
+        GetPublisher().RemoveKeyRegistration(mName, DNSErrorToOtbrError(aError));
+    }
+    else
+    {
+        otbrLogInfo("Successfully registered key %s", mName.c_str());
+        Complete(OTBR_ERROR_NONE);
+    }
+}
+
 otbrError PublisherMDnsSd::PublishServiceImpl(const std::string &aHostName,
                                               const std::string &aName,
                                               const std::string &aType,
@@ -714,6 +868,41 @@
     std::move(aCallback)(error);
 }
 
+otbrError PublisherMDnsSd::PublishKeyImpl(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback)
+{
+    otbrError             error = OTBR_ERROR_NONE;
+    DnssdKeyRegistration *keyReg;
+
+    if (mState != State::kReady)
+    {
+        error = OTBR_ERROR_INVALID_STATE;
+        std::move(aCallback)(error);
+        ExitNow();
+    }
+
+    aCallback = HandleDuplicateKeyRegistration(aName, aKeyData, std::move(aCallback));
+    VerifyOrExit(!aCallback.IsNull());
+
+    keyReg = new DnssdKeyRegistration(aName, aKeyData, std::move(aCallback), this);
+    AddKeyRegistration(std::unique_ptr<DnssdKeyRegistration>(keyReg));
+
+    error = keyReg->Register();
+
+exit:
+    return error;
+}
+
+void PublisherMDnsSd::UnpublishKey(const std::string &aName, ResultCallback &&aCallback)
+{
+    otbrError error = OTBR_ERROR_NONE;
+
+    VerifyOrExit(mState == Publisher::State::kReady, error = OTBR_ERROR_INVALID_STATE);
+    RemoveKeyRegistration(aName, OTBR_ERROR_ABORTED);
+
+exit:
+    std::move(aCallback)(error);
+}
+
 // See `regtype` parameter of the DNSServiceRegister() function for more information.
 std::string PublisherMDnsSd::MakeRegType(const std::string &aType, SubTypeList aSubTypeList)
 {
@@ -759,7 +948,7 @@
                       [&aType, &aInstanceName](const std::unique_ptr<ServiceSubscription> &aService) {
                           return aService->mType == aType && aService->mInstanceName == aInstanceName;
                       });
-    assert(it != mSubscribedServices.end());
+    VerifyOrExit(it != mSubscribedServices.end());
 
     mSubscribedServices.erase(it);
 
@@ -809,7 +998,7 @@
         mSubscribedHosts.begin(), mSubscribedHosts.end(),
         [&aHostName](const std::unique_ptr<HostSubscription> &aHost) { return aHost->mHostName == aHostName; });
 
-    assert(it != mSubscribedHosts.end());
+    VerifyOrExit(it != mSubscribedHosts.end());
 
     mSubscribedHosts.erase(it);
 
@@ -941,19 +1130,6 @@
     mResolvingInstances.back()->Resolve();
 }
 
-void PublisherMDnsSd::ServiceSubscription::RemoveInstanceResolution(
-    PublisherMDnsSd::ServiceInstanceResolution &aInstanceResolution)
-{
-    auto it = std::find_if(mResolvingInstances.begin(), mResolvingInstances.end(),
-                           [&aInstanceResolution](const std::unique_ptr<ServiceInstanceResolution> &aElem) {
-                               return &aInstanceResolution == aElem.get();
-                           });
-
-    assert(it != mResolvingInstances.end());
-
-    mResolvingInstances.erase(it);
-}
-
 void PublisherMDnsSd::ServiceSubscription::UpdateAll(MainloopContext &aMainloop) const
 {
     Update(aMainloop);
@@ -1056,7 +1232,7 @@
 
     otbrLogInfo("DNSServiceGetAddrInfo %s inf %d", mInstanceInfo.mHostName.c_str(), aInterfaceIndex);
 
-    dnsError = DNSServiceGetAddrInfo(&mServiceRef, kDNSServiceFlagsTimeout, aInterfaceIndex,
+    dnsError = DNSServiceGetAddrInfo(&mServiceRef, /* flags */ 0, aInterfaceIndex,
                                      kDNSServiceProtocol_IPv6 | kDNSServiceProtocol_IPv4,
                                      mInstanceInfo.mHostName.c_str(), HandleGetAddrInfoResult, this);
 
@@ -1093,22 +1269,31 @@
     OTBR_UNUSED_VARIABLE(aInterfaceIndex);
 
     Ip6Address address;
+    bool       isAdd = (aFlags & kDNSServiceFlagsAdd) != 0;
 
     otbrLog(aErrorCode == kDNSServiceErr_NoError ? OTBR_LOG_INFO : OTBR_LOG_WARNING, OTBR_LOG_TAG,
             "DNSServiceGetAddrInfo reply: flags=%" PRIu32 ", host=%s, sa_family=%u, error=%" PRId32, aFlags, aHostName,
             static_cast<unsigned int>(aAddress->sa_family), aErrorCode);
 
     VerifyOrExit(aErrorCode == kDNSServiceErr_NoError);
-    VerifyOrExit((aFlags & kDNSServiceFlagsAdd) && aAddress->sa_family == AF_INET6);
+    VerifyOrExit(aAddress->sa_family == AF_INET6);
 
     address.CopyFrom(*reinterpret_cast<const struct sockaddr_in6 *>(aAddress));
     VerifyOrExit(!address.IsUnspecified() && !address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback(),
                  otbrLogDebug("DNSServiceGetAddrInfo ignores address %s", address.ToString().c_str()));
 
-    mInstanceInfo.mAddresses.push_back(address);
-    mInstanceInfo.mTtl = aTtl;
+    otbrLogInfo("DNSServiceGetAddrInfo reply: %s address=%s, ttl=%" PRIu32, isAdd ? "add" : "remove",
+                address.ToString().c_str(), aTtl);
 
-    otbrLogInfo("DNSServiceGetAddrInfo reply: address=%s, ttl=%" PRIu32, address.ToString().c_str(), aTtl);
+    if (isAdd)
+    {
+        mInstanceInfo.AddAddress(address);
+    }
+    else
+    {
+        mInstanceInfo.RemoveAddress(address);
+    }
+    mInstanceInfo.mTtl = aTtl;
 
 exit:
     if (!mInstanceInfo.mAddresses.empty() || aErrorCode != kDNSServiceErr_NoError)
@@ -1123,10 +1308,6 @@
     std::string            serviceName  = mSubscription->mType;
     DiscoveredInstanceInfo instanceInfo = mInstanceInfo;
 
-    // NOTE: `RemoveInstanceResolution` will free this `ServiceInstanceResolution` object.
-    //       So, We can't access `mSubscription` after `RemoveInstanceResolution`.
-    subscription->RemoveInstanceResolution(*this);
-
     // NOTE: The `ServiceSubscription` object may be freed in `OnServiceResolved`.
     subscription->mPublisher.OnServiceResolved(serviceName, instanceInfo);
 }
@@ -1170,25 +1351,34 @@
     OTBR_UNUSED_VARIABLE(aServiceRef);
 
     Ip6Address address;
+    bool       isAdd = (aFlags & kDNSServiceFlagsAdd) != 0;
 
     otbrLog(aErrorCode == kDNSServiceErr_NoError ? OTBR_LOG_INFO : OTBR_LOG_WARNING, OTBR_LOG_TAG,
             "DNSServiceGetAddrInfo reply: flags=%" PRIu32 ", host=%s, sa_family=%u, error=%" PRId32, aFlags, aHostName,
             static_cast<unsigned int>(aAddress->sa_family), aErrorCode);
 
     VerifyOrExit(aErrorCode == kDNSServiceErr_NoError);
-    VerifyOrExit((aFlags & kDNSServiceFlagsAdd) && aAddress->sa_family == AF_INET6);
+    VerifyOrExit(aAddress->sa_family == AF_INET6);
 
     address.CopyFrom(*reinterpret_cast<const struct sockaddr_in6 *>(aAddress));
     VerifyOrExit(!address.IsLinkLocal(),
                  otbrLogDebug("DNSServiceGetAddrInfo ignore link-local address %s", address.ToString().c_str()));
 
-    mHostInfo.mHostName = aHostName;
-    mHostInfo.mAddresses.push_back(address);
+    otbrLogInfo("DNSServiceGetAddrInfo reply: %s address=%s, ttl=%" PRIu32, isAdd ? "add" : "remove",
+                address.ToString().c_str(), aTtl);
+
+    if (isAdd)
+    {
+        mHostInfo.AddAddress(address);
+    }
+    else
+    {
+        mHostInfo.RemoveAddress(address);
+    }
+    mHostInfo.mHostName   = aHostName;
     mHostInfo.mNetifIndex = aInterfaceIndex;
     mHostInfo.mTtl        = aTtl;
 
-    otbrLogInfo("DNSServiceGetAddrInfo reply: address=%s, ttl=%" PRIu32, address.ToString().c_str(), aTtl);
-
     // NOTE: This `HostSubscription` object may be freed in `OnHostResolved`.
     mPublisher.OnHostResolved(mHostName, mHostInfo);
 
diff --git a/src/mdns/mdns_mdnssd.hpp b/src/mdns/mdns_mdnssd.hpp
index fa4bf4d..068d786 100644
--- a/src/mdns/mdns_mdnssd.hpp
+++ b/src/mdns/mdns_mdnssd.hpp
@@ -70,6 +70,7 @@
     void UnpublishService(const std::string &aName, const std::string &aType, ResultCallback &&aCallback) override;
 
     void      UnpublishHost(const std::string &aName, ResultCallback &&aCallback) override;
+    void      UnpublishKey(const std::string &aName, ResultCallback &&aCallback) override;
     void      SubscribeService(const std::string &aType, const std::string &aInstanceName) override;
     void      UnsubscribeService(const std::string &aType, const std::string &aInstanceName) override;
     void      SubscribeHost(const std::string &aHostName) override;
@@ -94,6 +95,7 @@
     otbrError PublishHostImpl(const std::string &aName,
                               const AddressList &aAddress,
                               ResultCallback   &&aCallback) override;
+    otbrError PublishKeyImpl(const std::string &aName, const KeyData &aKeyData, ResultCallback &&aCallback) override;
     void      OnServiceResolveFailedImpl(const std::string &aType,
                                          const std::string &aInstanceName,
                                          int32_t            aErrorCode) override;
@@ -109,8 +111,12 @@
         kStopOnServiceNotRunningError,
     };
 
+    class DnssdKeyRegistration;
+
     class DnssdServiceRegistration : public ServiceRegistration
     {
+        friend class DnssdKeyRegistration;
+
     public:
         using ServiceRegistration::ServiceRegistration; // Inherit base constructor
 
@@ -132,7 +138,8 @@
                                               const char         *aDomain,
                                               void               *aContext);
 
-        DNSServiceRef mServiceRef = nullptr;
+        DNSServiceRef         mServiceRef    = nullptr;
+        DnssdKeyRegistration *mRelatedKeyReg = nullptr;
     };
 
     class DnssdHostRegistration : public HostRegistration
@@ -158,6 +165,31 @@
         std::vector<bool>         mAddrRegistered;
     };
 
+    class DnssdKeyRegistration : public KeyRegistration
+    {
+        friend class DnssdServiceRegistration;
+
+    public:
+        using KeyRegistration::KeyRegistration; // Inherit base class constructor
+
+        ~DnssdKeyRegistration(void) override { Unregister(); }
+
+        otbrError Register(void);
+
+    private:
+        void             Unregister(void);
+        PublisherMDnsSd &GetPublisher(void) { return *static_cast<PublisherMDnsSd *>(mPublisher); }
+        void             HandleRegisterResult(DNSServiceErrorType aError);
+        static void      HandleRegisterResult(DNSServiceRef       aServiceRef,
+                                              DNSRecordRef        aRecordRef,
+                                              DNSServiceFlags     aFlags,
+                                              DNSServiceErrorType aErrorCode,
+                                              void               *aContext);
+
+        DNSRecordRef              mRecordRef         = nullptr;
+        DnssdServiceRegistration *mRelatedServiceReg = nullptr;
+    };
+
     struct ServiceRef : private ::NonCopyable
     {
         DNSServiceRef    mServiceRef;
@@ -256,7 +288,6 @@
                      const std::string &aInstanceName,
                      const std::string &aType,
                      const std::string &aDomain);
-        void RemoveInstanceResolution(ServiceInstanceResolution &aInstanceResolution);
         void UpdateAll(MainloopContext &aMainloop) const;
         void ProcessAll(const MainloopContext &aMainloop, std::vector<DNSServiceRef> &aReadyServices) const;
 
diff --git a/src/ncp/ncp_openthread.cpp b/src/ncp/ncp_openthread.cpp
index efd3e7f..df3dcd5 100644
--- a/src/ncp/ncp_openthread.cpp
+++ b/src/ncp/ncp_openthread.cpp
@@ -60,10 +60,10 @@
 namespace otbr {
 namespace Ncp {
 
-static const uint16_t kThreadVersion11  = 2; ///< Thread Version 1.1
-static const uint16_t kThreadVersion12  = 3; ///< Thread Version 1.2
-static const uint16_t kThreadVersion13  = 4; ///< Thread Version 1.3
-static const uint16_t kThreadVersion131 = 5; ///< Thread Version 1.3.1
+static const uint16_t kThreadVersion11 = 2; ///< Thread Version 1.1
+static const uint16_t kThreadVersion12 = 3; ///< Thread Version 1.2
+static const uint16_t kThreadVersion13 = 4; ///< Thread Version 1.3
+static const uint16_t kThreadVersion14 = 5; ///< Thread Version 1.4
 
 ControllerOpenThread::ControllerOpenThread(const char                      *aInterfaceName,
                                            const std::vector<const char *> &aRadioUrls,
@@ -296,6 +296,9 @@
 
     otSysDeinit();
     mInstance = nullptr;
+
+    mThreadStateChangedCallbacks.clear();
+    mResetHandlers.clear();
 }
 
 void ControllerOpenThread::HandleStateChanged(otChangedFlags aFlags)
@@ -385,8 +388,8 @@
     case kThreadVersion13:
         version = "1.3.0";
         break;
-    case kThreadVersion131:
-        version = "1.3.1";
+    case kThreadVersion14:
+        version = "1.4";
         break;
     default:
         otbrLogEmerg("Unexpected thread version %hu", otThreadGetVersion());
diff --git a/src/openwrt/ubus/otubus.cpp b/src/openwrt/ubus/otubus.cpp
index 4d807a9..4dfaac2 100644
--- a/src/openwrt/ubus/otubus.cpp
+++ b/src/openwrt/ubus/otubus.cpp
@@ -1190,8 +1190,10 @@
         ubus_send_reply(aContext, aRequest, mNetworkdataBuf.head);
         if (time(nullptr) - mSecond > 10)
         {
+            static constexpr uint16_t kMaxTlvs = 35;
+
             struct otIp6Address address;
-            uint8_t             tlvTypes[OT_NETWORK_DIAGNOSTIC_TYPELIST_MAX_ENTRIES];
+            uint8_t             tlvTypes[kMaxTlvs];
             uint8_t             count             = 0;
             char                multicastAddr[10] = "ff03::2";
 
diff --git a/src/proto/CMakeLists.txt b/src/proto/CMakeLists.txt
index e982d52..3659b51 100644
--- a/src/proto/CMakeLists.txt
+++ b/src/proto/CMakeLists.txt
@@ -8,6 +8,12 @@
 endif()
 find_package(Protobuf REQUIRED)
 
+# Protobuf library which >= 4.22 requires to link the absl
+if ("${Protobuf_VERSION}" VERSION_GREATER_EQUAL 4.22)
+    find_package(absl REQUIRED)
+    set(ABSL_LIBS absl::log_internal_check_op)
+endif()
+
 # Set up the output path.
 set(PROTO_GEN_DIR ${PROJECT_SOURCE_DIR}/build/src/proto)
 if(NOT (EXISTS "${PROTO_GEN_DIR}" AND IS_DIRECTORY "${PROTO_GEN_DIR}"))
@@ -61,6 +67,7 @@
 
 target_link_libraries(otbr-proto PUBLIC
     protobuf::libprotobuf-lite
+    ${ABSL_LIBS}
 )
 
 target_include_directories(otbr-proto PUBLIC
diff --git a/src/proto/thread_telemetry.proto b/src/proto/thread_telemetry.proto
index 2cc2851..b3ad934 100644
--- a/src/proto/thread_telemetry.proto
+++ b/src/proto/thread_telemetry.proto
@@ -230,6 +230,36 @@
 
     // Error counters for NAT64 translator on the border router
     optional Nat64ErrorCounters nat64_error_counters = 20;
+
+    // The counter for inbound Internet when DHCPv6 PD enabled
+    optional PacketsAndBytes inbound_internet = 21;
+
+    // The counter for outbound Internet when DHCPv6 PD enabled
+    optional PacketsAndBytes outbound_internet = 22;
+  }
+
+  enum Dhcp6PdState {
+    DHCP6_PD_STATE_UNSPECIFIED = 0;
+
+    // DHCPv6 PD is disabled on the border router.
+    DHCP6_PD_STATE_DISABLED = 1;
+
+    // DHCPv6 PD is enabled but won't try to request and publish a prefix.
+    DHCP6_PD_STATE_STOPPED = 2;
+
+    // DHCPv6 PD is enabled and will try to request and publish a prefix.
+    DHCP6_PD_STATE_RUNNING = 3;
+  }
+
+  message PdProcessedRaInfo {
+    // The number of platform generated RA handled by ApplyPlatfromGeneratedRa.
+    optional uint32 num_platform_ra_received = 1;
+
+    // The number of PIO processed for adding OMR prefixes.
+    optional uint32 num_platform_pio_processed = 2;
+
+    // The timestamp of last processed RA message.
+    optional uint32 last_platform_ra_msec = 3;
   }
 
   message SrpServerRegistrationInfo {
@@ -290,6 +320,12 @@
     SRP_SERVER_ADDRESS_MODE_STATE_ANYCAST = 2;
   }
 
+  enum UpstreamDnsQueryState {
+    UPSTREAMDNS_QUERY_STATE_UNSPECIFIED = 0;
+    UPSTREAMDNS_QUERY_STATE_ENABLED = 1;
+    UPSTREAMDNS_QUERY_STATE_DISABLED = 2;
+  }
+
   message SrpServerInfo {
     // The state of the SRP server
     optional SrpServerState state = 1;
@@ -309,6 +345,34 @@
     optional SrpServerResponseCounters response_counters = 6;
   }
 
+  message TrelPacketCounters {
+    // The number of packets successfully transmitted through TREL
+    optional uint64 trel_tx_packets = 1;
+
+    // The number of bytes successfully transmitted through TREL
+    optional uint64 trel_tx_bytes = 2;
+
+    // The number of packet transmission failures through TREL
+    optional uint64 trel_tx_packets_failed = 3;
+
+    // The number of packets successfully received through TREL
+    optional uint64 tre_rx_packets = 4;
+
+    // The number of bytes successfully received through TREL
+    optional uint64 trel_rx_bytes = 5;
+  }
+
+  message TrelInfo {
+    // Whether TREL is enabled.
+    optional bool is_trel_enabled = 1;
+
+    // The number of TREL peers.
+    optional uint32 num_trel_peers = 2;
+
+    // TREL packet counters
+    optional TrelPacketCounters counters = 3;
+  }
+
   message DnsServerResponseCounters {
     // The number of successful responses
     optional uint32 success_count = 1;
@@ -327,6 +391,15 @@
 
     // The number of other responses
     optional uint32 other_count = 6;
+
+    // The number of queries handled by Upstream DNS server.
+    optional uint32 upstream_dns_queries = 7;
+
+    // The number of responses handled by Upstream DNS server.
+    optional uint32 upstream_dns_responses = 8;
+
+    // The number of upstream DNS failures.
+    optional uint32 upstream_dns_failures = 9;
   }
 
   message DnsServerInfo {
@@ -335,6 +408,9 @@
 
     // The number of DNS queries resolved at the local SRP server
     optional uint32 resolved_by_local_srp_count = 2;
+
+    // The state of upstream DNS query
+    optional UpstreamDnsQueryState upstream_dns_query_state = 3;
   }
 
   message MdnsResponseCounters {
@@ -431,6 +507,18 @@
 
     // Information about the mappings of NAT64 translator
     repeated Nat64Mapping nat64_mappings = 7;
+
+    // DHCPv6 PD state
+    optional Dhcp6PdState dhcp6_pd_state = 8;
+
+    // DHCPv6 PD prefix
+    optional bytes hashed_pd_prefix = 9;
+
+    // DHCPv6 PD processed RA Info
+    optional PdProcessedRaInfo pd_processed_ra_info= 10;
+
+    // Information about TREL.
+    optional TrelInfo trel_info = 11;
   }
 
   message RcpStabilityStatistics {
diff --git a/src/sdp_proxy/advertising_proxy.cpp b/src/sdp_proxy/advertising_proxy.cpp
index 7eac4d0..16a93c4 100644
--- a/src/sdp_proxy/advertising_proxy.cpp
+++ b/src/sdp_proxy/advertising_proxy.cpp
@@ -96,21 +96,37 @@
 AdvertisingProxy::AdvertisingProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher)
     : mNcp(aNcp)
     , mPublisher(aPublisher)
+    , mIsEnabled(false)
 {
     mNcp.RegisterResetHandler(
         [this]() { otSrpServerSetServiceUpdateHandler(GetInstance(), AdvertisingHandler, this); });
 }
 
-otbrError AdvertisingProxy::Start(void)
+void AdvertisingProxy::SetEnabled(bool aIsEnabled)
+{
+    VerifyOrExit(aIsEnabled != IsEnabled());
+    mIsEnabled = aIsEnabled;
+    if (mIsEnabled)
+    {
+        Start();
+    }
+    else
+    {
+        Stop();
+    }
+
+exit:
+    return;
+}
+
+void AdvertisingProxy::Start(void)
 {
     otSrpServerSetServiceUpdateHandler(GetInstance(), AdvertisingHandler, this);
 
     otbrLogInfo("Started");
-
-    return OTBR_ERROR_NONE;
 }
 
-void AdvertisingProxy::Stop()
+void AdvertisingProxy::Stop(void)
 {
     // Outstanding updates will fail on the SRP server because of timeout.
     // TODO: handle this case gracefully.
@@ -141,6 +157,8 @@
     OutstandingUpdate *update = nullptr;
     otbrError          error  = OTBR_ERROR_NONE;
 
+    VerifyOrExit(IsEnabled());
+
     mOutstandingUpdates.emplace_back();
     update      = &mOutstandingUpdates.back();
     update->mId = aId;
@@ -152,6 +170,9 @@
         mOutstandingUpdates.pop_back();
         otSrpServerHandleServiceUpdateResult(GetInstance(), aId, OtbrErrorToOtError(error));
     }
+
+exit:
+    return;
 }
 
 void AdvertisingProxy::OnMdnsPublishResult(otSrpServerServiceUpdateId aUpdateId, otbrError aError)
@@ -205,11 +226,23 @@
     return addresses;
 }
 
+void AdvertisingProxy::HandleMdnsState(Mdns::Publisher::State aState)
+{
+    VerifyOrExit(IsEnabled());
+    VerifyOrExit(aState == Mdns::Publisher::State::kReady);
+
+    PublishAllHostsAndServices();
+
+exit:
+    return;
+}
+
 void AdvertisingProxy::PublishAllHostsAndServices(void)
 {
     const otSrpServerHost *host = nullptr;
 
-    VerifyOrExit(mPublisher.IsStarted(), mPublisher.Start());
+    VerifyOrExit(IsEnabled());
+    VerifyOrExit(mPublisher.IsStarted());
 
     otbrLogInfo("Publish all hosts and services");
     while ((host = otSrpServerGetNextHost(GetInstance(), host)))
diff --git a/src/sdp_proxy/advertising_proxy.hpp b/src/sdp_proxy/advertising_proxy.hpp
index 385dadd..9956693 100644
--- a/src/sdp_proxy/advertising_proxy.hpp
+++ b/src/sdp_proxy/advertising_proxy.hpp
@@ -66,19 +66,12 @@
     explicit AdvertisingProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher);
 
     /**
-     * This method starts the Advertising Proxy.
+     * This method enables/disables the Advertising Proxy.
      *
-     * @retval OTBR_ERROR_NONE  Successfully started the Advertising Proxy.
-     * @retval ...              Failed to start the Advertising Proxy.
+     * @param[in] aIsEnabled  Whether to enable the Advertising Proxy.
      *
      */
-    otbrError Start(void);
-
-    /**
-     * This method stops the Advertising Proxy.
-     *
-     */
-    void Stop();
+    void SetEnabled(bool aIsEnabled);
 
     /**
      * This method publishes all registered hosts and services.
@@ -86,6 +79,14 @@
      */
     void PublishAllHostsAndServices(void);
 
+    /**
+     * This method handles mDNS publisher's state changes.
+     *
+     * @param[in] aState  The state of mDNS publisher.
+     *
+     */
+    void HandleMdnsState(Mdns::Publisher::State aState);
+
 private:
     struct OutstandingUpdate
     {
@@ -106,6 +107,10 @@
 
     std::vector<Ip6Address> GetEligibleAddresses(const otIp6Address *aHostAddresses, uint8_t aHostAddressNum);
 
+    void Start(void);
+    void Stop(void);
+    bool IsEnabled(void) const { return mIsEnabled; }
+
     /**
      * This method publishes a specified host and its services.
      *
@@ -129,6 +134,8 @@
     // A reference to the mDNS publisher, has no ownership.
     Mdns::Publisher &mPublisher;
 
+    bool mIsEnabled;
+
     // A vector that tracks outstanding updates.
     std::vector<OutstandingUpdate> mOutstandingUpdates;
 };
diff --git a/src/sdp_proxy/discovery_proxy.cpp b/src/sdp_proxy/discovery_proxy.cpp
index d6d41c2..ea0072a 100644
--- a/src/sdp_proxy/discovery_proxy.cpp
+++ b/src/sdp_proxy/discovery_proxy.cpp
@@ -61,6 +61,7 @@
 DiscoveryProxy::DiscoveryProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher)
     : mNcp(aNcp)
     , mMdnsPublisher(aPublisher)
+    , mIsEnabled(false)
 {
     mNcp.RegisterResetHandler([this]() {
         otDnssdQuerySetCallbacks(mNcp.GetInstance(), &DiscoveryProxy::OnDiscoveryProxySubscribe,
@@ -68,6 +69,22 @@
     });
 }
 
+void DiscoveryProxy::SetEnabled(bool aIsEnabled)
+{
+    VerifyOrExit(IsEnabled() != aIsEnabled);
+    mIsEnabled = aIsEnabled;
+    if (mIsEnabled)
+    {
+        Start();
+    }
+    else
+    {
+        Stop();
+    }
+exit:
+    return;
+}
+
 void DiscoveryProxy::Start(void)
 {
     assert(mSubscriberId == 0);
@@ -197,11 +214,9 @@
         {
         case OT_DNSSD_QUERY_TYPE_BROWSE:
             splitError = SplitFullServiceName(queryName, serviceName, domain);
-            assert(splitError == OTBR_ERROR_NONE);
             break;
         case OT_DNSSD_QUERY_TYPE_RESOLVE:
             splitError = SplitFullServiceInstanceName(queryName, instanceName, serviceName, domain);
-            assert(splitError == OTBR_ERROR_NONE);
             break;
         default:
             splitError = OTBR_ERROR_NOT_FOUND;
@@ -209,6 +224,7 @@
         }
         if (splitError != OTBR_ERROR_NONE)
         {
+            // Incoming service/instance was not what current query wanted to see, move on.
             continue;
         }
 
@@ -267,8 +283,13 @@
         {
             continue;
         }
+
         splitError = SplitFullHostName(queryName, hostName, domain);
-        assert(splitError == OTBR_ERROR_NONE);
+
+        if (splitError != OTBR_ERROR_NONE)
+        {
+            continue;
+        }
 
         if (DnsLabelsEqual(hostName, aHostName))
         {
diff --git a/src/sdp_proxy/discovery_proxy.hpp b/src/sdp_proxy/discovery_proxy.hpp
index 5263ab1..c940e89 100644
--- a/src/sdp_proxy/discovery_proxy.hpp
+++ b/src/sdp_proxy/discovery_proxy.hpp
@@ -70,16 +70,26 @@
     explicit DiscoveryProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher);
 
     /**
-     * This method starts the Discovery Proxy.
+     * This method enables/disables the Discovery Proxy.
+     *
+     * @param[in] aIsEnabled  Whether to enable the Discovery Proxy.
      *
      */
-    void Start(void);
+    void SetEnabled(bool aIsEnabled);
 
     /**
-     * This method stops the Discovery Proxy.
+     * This method handles mDNS publisher's state changes.
+     *
+     * @param[in] aState  The state of mDNS publisher.
      *
      */
-    void Stop(void);
+    void HandleMdnsState(Mdns::Publisher::State aState)
+    {
+        VerifyOrExit(IsEnabled());
+        OTBR_UNUSED_VARIABLE(aState);
+    exit:
+        return;
+    }
 
 private:
     enum : uint32_t
@@ -98,8 +108,13 @@
     void OnHostDiscovered(const std::string &aHostName, const Mdns::Publisher::DiscoveredHostInfo &aHostInfo);
     static uint32_t CapTtl(uint32_t aTtl);
 
+    void Start(void);
+    void Stop(void);
+    bool IsEnabled(void) const { return mIsEnabled; }
+
     Ncp::ControllerOpenThread &mNcp;
     Mdns::Publisher           &mMdnsPublisher;
+    bool                       mIsEnabled;
     uint64_t                   mSubscriberId = 0;
 };
 
diff --git a/src/trel_dnssd/trel_dnssd.cpp b/src/trel_dnssd/trel_dnssd.cpp
index c285a83..e328ae3 100644
--- a/src/trel_dnssd/trel_dnssd.cpp
+++ b/src/trel_dnssd/trel_dnssd.cpp
@@ -188,9 +188,10 @@
     return;
 }
 
-void TrelDnssd::OnMdnsPublisherReady(void)
+void TrelDnssd::HandleMdnsState(Mdns::Publisher::State aState)
 {
     VerifyOrExit(IsInitialized());
+    VerifyOrExit(aState == Mdns::Publisher::State::kReady);
 
     otbrLogDebug("mDNS Publisher is Ready");
     mMdnsPublisherReady = true;
@@ -282,6 +283,7 @@
 void TrelDnssd::OnTrelServiceInstanceAdded(const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo)
 {
     std::string        instanceName = StringUtils::ToLowercase(aInstanceInfo.mName);
+    Ip6Address         selectedAddress;
     otPlatTrelPeerInfo peerInfo;
 
     // Remove any existing TREL service instance before adding
@@ -295,6 +297,21 @@
     for (const auto &addr : aInstanceInfo.mAddresses)
     {
         otbrLogDebug("Peer address: %s", addr.ToString().c_str());
+
+        // Skip anycast (Refer to https://datatracker.ietf.org/doc/html/rfc2373#section-2.6.1)
+        if (addr.m64[1] == 0)
+        {
+            continue;
+        }
+
+        // If there are multiple addresses, we prefer the address
+        // which is numerically smallest. This prefers GUA over ULA
+        // (`fc00::/7`) and then link-local (`fe80::/10`).
+
+        if (selectedAddress.IsUnspecified() || (addr < selectedAddress))
+        {
+            selectedAddress = addr;
+        }
     }
 
     if (aInstanceInfo.mAddresses.empty())
@@ -304,7 +321,7 @@
     }
 
     peerInfo.mRemoved = false;
-    memcpy(&peerInfo.mSockAddr.mAddress, &aInstanceInfo.mAddresses[0], sizeof(peerInfo.mSockAddr.mAddress));
+    memcpy(&peerInfo.mSockAddr.mAddress, &selectedAddress, sizeof(peerInfo.mSockAddr.mAddress));
     peerInfo.mSockAddr.mPort = aInstanceInfo.mPort;
     peerInfo.mTxtData        = aInstanceInfo.mTxtData.data();
     peerInfo.mTxtLength      = aInstanceInfo.mTxtData.size();
diff --git a/src/trel_dnssd/trel_dnssd.hpp b/src/trel_dnssd/trel_dnssd.hpp
index 8f44104..c22e7ba 100644
--- a/src/trel_dnssd/trel_dnssd.hpp
+++ b/src/trel_dnssd/trel_dnssd.hpp
@@ -109,10 +109,12 @@
     void UnregisterService(void);
 
     /**
-     * This method notifies that mDNS Publisher is ready.
+     * This method handles mDNS publisher's state changes.
+     *
+     * @param[in] aState  The state of mDNS publisher.
      *
      */
-    void OnMdnsPublisherReady(void);
+    void HandleMdnsState(Mdns::Publisher::State aState);
 
 private:
     static constexpr size_t   kPeerCacheSize             = 256;
diff --git a/src/utils/thread_helper.cpp b/src/utils/thread_helper.cpp
index b51c550..b641935 100644
--- a/src/utils/thread_helper.cpp
+++ b/src/utils/thread_helper.cpp
@@ -35,8 +35,6 @@
 #include <string.h>
 #include <time.h>
 
-#include <string>
-
 #include <openthread/border_router.h>
 #include <openthread/channel_manager.h>
 #include <openthread/dataset_ftd.h>
@@ -50,6 +48,9 @@
 #include <openthread/nat64.h>
 #include "utils/sha256.hpp"
 #endif
+#if OTBR_ENABLE_DHCP6_PD
+#include "utils/sha256.hpp"
+#endif
 #if OTBR_ENABLE_LINK_METRICS_TELEMETRY
 #include <openthread/link_metrics.h>
 #endif
@@ -57,6 +58,9 @@
 #include <openthread/srp_server.h>
 #endif
 #include <openthread/thread_ftd.h>
+#if OTBR_ENABLE_TREL
+#include <openthread/trel.h>
+#endif
 #include <openthread/platform/radio.h>
 
 #include "common/byteswap.hpp"
@@ -184,6 +188,30 @@
 }
 #endif // OTBR_ENABLE_NAT64
 
+#if OTBR_ENABLE_DHCP6_PD
+threadnetwork::TelemetryData_Dhcp6PdState Dhcp6PdStateFromOtDhcp6PdState(otBorderRoutingDhcp6PdState dhcp6PdState)
+{
+    threadnetwork::TelemetryData_Dhcp6PdState pdState = threadnetwork::TelemetryData::DHCP6_PD_STATE_UNSPECIFIED;
+
+    switch (dhcp6PdState)
+    {
+    case OT_BORDER_ROUTING_DHCP6_PD_STATE_DISABLED:
+        pdState = threadnetwork::TelemetryData::DHCP6_PD_STATE_DISABLED;
+        break;
+    case OT_BORDER_ROUTING_DHCP6_PD_STATE_STOPPED:
+        pdState = threadnetwork::TelemetryData::DHCP6_PD_STATE_STOPPED;
+        break;
+    case OT_BORDER_ROUTING_DHCP6_PD_STATE_RUNNING:
+        pdState = threadnetwork::TelemetryData::DHCP6_PD_STATE_RUNNING;
+        break;
+    default:
+        break;
+    }
+
+    return pdState;
+}
+#endif // OTBR_ENABLE_DHCP6_PD
+
 void CopyMdnsResponseCounters(const MdnsResponseCounters &from, threadnetwork::TelemetryData_MdnsResponseCounters *to)
 {
     to->set_success_count(from.mSuccess);
@@ -202,10 +230,10 @@
     : mInstance(aInstance)
     , mNcp(aNcp)
 {
-#if OTBR_ENABLE_TELEMETRY_DATA_API && OTBR_ENABLE_NAT64
+#if OTBR_ENABLE_TELEMETRY_DATA_API && (OTBR_ENABLE_NAT64 || OTBR_ENABLE_DHCP6_PD)
     otError error;
 
-    SuccessOrExit(error = otPlatCryptoRandomGet(mNat64Ipv6AddressSalt, sizeof(mNat64Ipv6AddressSalt)));
+    SuccessOrExit(error = otPlatCryptoRandomGet(mNat64PdCommonSalt, sizeof(mNat64PdCommonSalt)));
 
 exit:
     if (error != OT_ERROR_NONE)
@@ -1159,6 +1187,14 @@
         borderRoutingCouters->set_rs_rx(otBorderRoutingCounters->mRsRx);
         borderRoutingCouters->set_rs_tx_success(otBorderRoutingCounters->mRsTxSuccess);
         borderRoutingCouters->set_rs_tx_failure(otBorderRoutingCounters->mRsTxFailure);
+        borderRoutingCouters->mutable_inbound_internet()->set_packet_count(
+            otBorderRoutingCounters->mInboundInternet.mPackets);
+        borderRoutingCouters->mutable_inbound_internet()->set_byte_count(
+            otBorderRoutingCounters->mInboundInternet.mBytes);
+        borderRoutingCouters->mutable_outbound_internet()->set_packet_count(
+            otBorderRoutingCounters->mOutboundInternet.mPackets);
+        borderRoutingCouters->mutable_outbound_internet()->set_byte_count(
+            otBorderRoutingCounters->mOutboundInternet.mBytes);
 
 #if OTBR_ENABLE_NAT64
         {
@@ -1207,6 +1243,25 @@
 #endif // OTBR_ENABLE_NAT64
        // End of BorderRoutingCounters section.
 
+#if OTBR_ENABLE_TREL
+        // Begin of TrelInfo section.
+        {
+            auto trelInfo       = wpanBorderRouter->mutable_trel_info();
+            auto otTrelCounters = otTrelGetCounters(mInstance);
+            auto trelCounters   = trelInfo->mutable_counters();
+
+            trelInfo->set_is_trel_enabled(otTrelIsEnabled(mInstance));
+            trelInfo->set_num_trel_peers(otTrelGetNumberOfPeers(mInstance));
+
+            trelCounters->set_trel_tx_packets(otTrelCounters->mTxPackets);
+            trelCounters->set_trel_tx_bytes(otTrelCounters->mTxBytes);
+            trelCounters->set_trel_tx_packets_failed(otTrelCounters->mTxFailure);
+            trelCounters->set_tre_rx_packets(otTrelCounters->mRxPackets);
+            trelCounters->set_trel_rx_bytes(otTrelCounters->mRxBytes);
+        }
+        // End of TrelInfo section.
+#endif // OTBR_ENABLE_TREL
+
 #if OTBR_ENABLE_SRP_ADVERTISING_PROXY
         // Begin of SrpServerInfo section.
         {
@@ -1290,8 +1345,19 @@
             dnsServerResponseCounters->set_name_error_count(otDnssdCounters.mNameErrorResponse);
             dnsServerResponseCounters->set_not_implemented_count(otDnssdCounters.mNotImplementedResponse);
             dnsServerResponseCounters->set_other_count(otDnssdCounters.mOtherResponse);
+            // The counters of queries, responses, failures handled by upstream DNS server.
+            dnsServerResponseCounters->set_upstream_dns_queries(otDnssdCounters.mUpstreamDnsCounters.mQueries);
+            dnsServerResponseCounters->set_upstream_dns_responses(otDnssdCounters.mUpstreamDnsCounters.mResponses);
+            dnsServerResponseCounters->set_upstream_dns_failures(otDnssdCounters.mUpstreamDnsCounters.mFailures);
 
             dnsServer->set_resolved_by_local_srp_count(otDnssdCounters.mResolvedBySrp);
+
+#if OTBR_ENABLE_DNS_UPSTREAM_QUERY
+            dnsServer->set_upstream_dns_query_state(
+                otDnssdUpstreamQueryIsEnabled(mInstance)
+                    ? threadnetwork::TelemetryData::UPSTREAMDNS_QUERY_STATE_ENABLED
+                    : threadnetwork::TelemetryData::UPSTREAMDNS_QUERY_STATE_DISABLED);
+#endif // OTBR_ENABLE_DNS_UPSTREAM_QUERY
         }
         // End of DnsServerInfo section.
 #endif // OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
@@ -1342,26 +1408,77 @@
                 CopyNat64TrafficCounters(otMapping.mCounters.mUdp, nat64MappingCounters->mutable_udp());
                 CopyNat64TrafficCounters(otMapping.mCounters.mIcmp, nat64MappingCounters->mutable_icmp());
 
-                {
-                    uint8_t ipAddrShaInput[OT_IP6_ADDRESS_SIZE + kNat64SourceAddressHashSaltLength];
-                    memcpy(ipAddrShaInput, otMapping.mIp6.mFields.m8, sizeof(otMapping.mIp6.mFields.m8));
-                    memcpy(&ipAddrShaInput[sizeof(otMapping.mIp6.mFields.m8)], mNat64Ipv6AddressSalt,
-                           sizeof(mNat64Ipv6AddressSalt));
+                sha256.Start();
+                sha256.Update(otMapping.mIp6.mFields.m8, sizeof(otMapping.mIp6.mFields.m8));
+                sha256.Update(mNat64PdCommonSalt, sizeof(mNat64PdCommonSalt));
+                sha256.Finish(hash);
 
-                    sha256.Start();
-                    sha256.Update(ipAddrShaInput, sizeof(ipAddrShaInput));
-                    sha256.Finish(hash);
-
-                    nat64Mapping->mutable_hashed_ipv6_address()->append(reinterpret_cast<const char *>(hash.GetBytes()),
-                                                                        sizeof(hash.GetBytes()));
-                    // Remaining time is not included in the telemetry
-                }
+                nat64Mapping->mutable_hashed_ipv6_address()->append(reinterpret_cast<const char *>(hash.GetBytes()),
+                                                                    Sha256::Hash::kSize);
+                // Remaining time is not included in the telemetry
             }
         }
         // End of Nat64Mapping section.
 #endif // OTBR_ENABLE_NAT64
+#if OTBR_ENABLE_DHCP6_PD
+        // Start of Dhcp6PdState section.
+        wpanBorderRouter->set_dhcp6_pd_state(Dhcp6PdStateFromOtDhcp6PdState(otBorderRoutingDhcp6PdGetState(mInstance)));
+        // End of Dhcp6PdState section.
 
-        // End of WpanBorderRouter section.
+        // Start of Hashed PD prefix
+        {
+            otBorderRoutingPrefixTableEntry aPrefixInfo;
+            const uint8_t                  *prefixAddr          = nullptr;
+            const uint8_t                  *truncatedHash       = nullptr;
+            constexpr size_t                kHashPrefixLength   = 6;
+            constexpr size_t                kHashedPrefixLength = 2;
+            std::vector<uint8_t>            hashedPdHeader      = {0x20, 0x01, 0x0d, 0xb8};
+            std::vector<uint8_t>            hashedPdTailer      = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+            std::vector<uint8_t>            hashedPdPrefix;
+            hashedPdPrefix.reserve(16);
+            Sha256       sha256;
+            Sha256::Hash hash;
+
+            otBorderRoutingGetPdOmrPrefix(mInstance, &aPrefixInfo);
+            prefixAddr = aPrefixInfo.mPrefix.mPrefix.mFields.m8;
+
+            // TODO: Put below steps into a reusable function.
+            sha256.Start();
+            sha256.Update(prefixAddr, kHashPrefixLength);
+            sha256.Update(mNat64PdCommonSalt, kNat64PdCommonHashSaltLength);
+            sha256.Finish(hash);
+
+            // Append hashedPdHeader
+            hashedPdPrefix.insert(hashedPdPrefix.end(), hashedPdHeader.begin(), hashedPdHeader.end());
+
+            // Append the first 2 bytes of the hashed prefix
+            truncatedHash = hash.GetBytes();
+            hashedPdPrefix.insert(hashedPdPrefix.end(), truncatedHash, truncatedHash + kHashedPrefixLength);
+
+            // Append ip[6] and ip[7]
+            hashedPdPrefix.push_back(prefixAddr[6]);
+            hashedPdPrefix.push_back(prefixAddr[7]);
+
+            // Append hashedPdTailer
+            hashedPdPrefix.insert(hashedPdPrefix.end(), hashedPdTailer.begin(), hashedPdTailer.end());
+
+            wpanBorderRouter->mutable_hashed_pd_prefix()->append(reinterpret_cast<const char *>(hashedPdPrefix.data()),
+                                                                 hashedPdPrefix.size());
+        }
+        // End of Hashed PD prefix
+        // Start of DHCPv6 PD processed RA Info
+        {
+            auto                pdProcessedRaInfo = wpanBorderRouter->mutable_pd_processed_ra_info();
+            otPdProcessedRaInfo raInfo;
+
+            otBorderRoutingGetPdProcessedRaInfo(mInstance, &raInfo);
+            pdProcessedRaInfo->set_num_platform_ra_received(raInfo.mNumPlatformRaReceived);
+            pdProcessedRaInfo->set_num_platform_pio_processed(raInfo.mNumPlatformPioProcessed);
+            pdProcessedRaInfo->set_last_platform_ra_msec(raInfo.mLastPlatformRaMsec);
+        }
+        // End of DHCPv6 PD processed RA Info
+#endif // OTBR_ENABLE_DHCP6_PD
+       // End of WpanBorderRouter section.
 
         // Start of WpanRcp section.
         {
diff --git a/src/utils/thread_helper.hpp b/src/utils/thread_helper.hpp
index b566e66..aa5f095 100644
--- a/src/utils/thread_helper.hpp
+++ b/src/utils/thread_helper.hpp
@@ -332,9 +332,9 @@
     UpdateMeshCopTxtHandler mUpdateMeshCopTxtHandler;
 #endif
 
-#if OTBR_ENABLE_TELEMETRY_DATA_API & OTBR_ENABLE_NAT64
-    static const uint8_t kNat64SourceAddressHashSaltLength = 16;
-    uint8_t              mNat64Ipv6AddressSalt[kNat64SourceAddressHashSaltLength];
+#if OTBR_ENABLE_TELEMETRY_DATA_API && (OTBR_ENABLE_NAT64 || OTBR_ENABLE_DHCP6_PD)
+    static constexpr uint8_t kNat64PdCommonHashSaltLength = 16;
+    uint8_t                  mNat64PdCommonSalt[kNat64PdCommonHashSaltLength];
 #endif
 };
 
diff --git a/src/web/main.cpp b/src/web/main.cpp
index 4e25e05..922038d 100644
--- a/src/web/main.cpp
+++ b/src/web/main.cpp
@@ -111,7 +111,7 @@
         }
     }
 
-    otbrLogInit(argv[0], logLevel, true);
+    otbrLogInit(argv[0], logLevel, true, false);
     otbrLogInfo("Running %s", OTBR_PACKAGE_VERSION);
 
     if (interfaceName == nullptr)
diff --git a/tests/android/Android.bp b/tests/android/Android.bp
index 242eec0..d100717 100644
--- a/tests/android/Android.bp
+++ b/tests/android/Android.bp
@@ -27,6 +27,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_thread_network",
     default_applicable_licenses: ["external_ot-br-posix_license"],
 }
 
diff --git a/tests/android/java/com/android/server/thread/openthread/testing/FakeOtDaemonTest.java b/tests/android/java/com/android/server/thread/openthread/testing/FakeOtDaemonTest.java
index d97af20..b3758ac 100644
--- a/tests/android/java/com/android/server/thread/openthread/testing/FakeOtDaemonTest.java
+++ b/tests/android/java/com/android/server/thread/openthread/testing/FakeOtDaemonTest.java
@@ -28,15 +28,23 @@
 
 package com.android.server.thread.openthread.testing;
 
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
 import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLED;
 import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED;
+import static com.android.server.thread.openthread.testing.FakeOtDaemon.OT_DEVICE_ROLE_DISABLED;
 
 import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.os.Handler;
+import android.os.IBinder.DeathRecipient;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.test.TestLooper;
@@ -44,9 +52,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.thread.openthread.BackboneRouterState;
+import com.android.server.thread.openthread.IChannelMasksReceiver;
 import com.android.server.thread.openthread.INsdPublisher;
 import com.android.server.thread.openthread.IOtDaemonCallback;
 import com.android.server.thread.openthread.IOtStatusReceiver;
+import com.android.server.thread.openthread.MeshcopTxtAttributes;
 import com.android.server.thread.openthread.OtDaemonState;
 
 import org.junit.Before;
@@ -55,7 +66,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -82,10 +95,19 @@
                                     + "642D643961300102D9A00410A245479C836D551B9CA557F7"
                                     + "B9D351B40C0402A0FFF8");
 
+    private static final int DEFAULT_SUPPORTED_CHANNEL_MASK = 0x07FFF800; // from channel 11 to 26
+    private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0;
+    private static final byte[] TEST_VENDOR_OUI = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
+    private static final String TEST_VENDOR_NAME = "test vendor";
+    private static final String TEST_MODEL_NAME = "test model";
+    private static final String TEST_DEFAULT_COUNTRY_CODE = "WW";
+
     private FakeOtDaemon mFakeOtDaemon;
     private TestLooper mTestLooper;
     @Mock private ParcelFileDescriptor mMockTunFd;
     @Mock private INsdPublisher mMockNsdPublisher;
+    @Mock private IOtDaemonCallback mMockCallback;
+    private MeshcopTxtAttributes mOverriddenMeshcopTxts;
 
     @Before
     public void setUp() {
@@ -93,27 +115,54 @@
 
         mTestLooper = new TestLooper();
         mFakeOtDaemon = new FakeOtDaemon(new Handler(mTestLooper.getLooper()));
+        mOverriddenMeshcopTxts = new MeshcopTxtAttributes();
+        mOverriddenMeshcopTxts.vendorName = TEST_VENDOR_NAME;
+        mOverriddenMeshcopTxts.vendorOui = TEST_VENDOR_OUI;
+        mOverriddenMeshcopTxts.modelName = TEST_MODEL_NAME;
     }
 
     @Test
-    public void initialize_succeed_tunFdIsSet() throws Exception {
-        mFakeOtDaemon.initialize(mMockTunFd, true, mMockNsdPublisher);
+    public void initialize_succeed_argumentsAreSetAndCallbackIsInvoked() throws Exception {
+        mOverriddenMeshcopTxts.vendorName = TEST_VENDOR_NAME;
+        mOverriddenMeshcopTxts.vendorOui = TEST_VENDOR_OUI;
+        mOverriddenMeshcopTxts.modelName = TEST_MODEL_NAME;
 
+        mFakeOtDaemon.initialize(
+                mMockTunFd,
+                true,
+                mMockNsdPublisher,
+                mOverriddenMeshcopTxts,
+                mMockCallback,
+                TEST_DEFAULT_COUNTRY_CODE);
+        mTestLooper.dispatchAll();
+
+        MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
+        assertThat(meshcopTxts).isNotNull();
+        assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
+        assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI);
+        assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
         assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
-    }
-
-    @Test
-    public void initialize_succeed_NsdPublisherIsSet() throws Exception {
-        mFakeOtDaemon.initialize(mMockTunFd, true, mMockNsdPublisher);
-
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(OT_STATE_ENABLED);
         assertThat(mFakeOtDaemon.getNsdPublisher()).isEqualTo(mMockNsdPublisher);
+        assertThat(mFakeOtDaemon.getStateCallback()).isEqualTo(mMockCallback);
+        assertThat(mFakeOtDaemon.getCountryCode()).isEqualTo(TEST_DEFAULT_COUNTRY_CODE);
+        assertThat(mFakeOtDaemon.isInitialized()).isTrue();
+        verify(mMockCallback, times(1)).onStateChanged(any(), anyLong());
+        verify(mMockCallback, times(1)).onBackboneRouterStateChanged(any());
     }
 
     @Test
     public void registerStateCallback_noStateChange_callbackIsInvoked() throws Exception {
-        mFakeOtDaemon.initialize(mMockTunFd, true, mMockNsdPublisher);
+        mFakeOtDaemon.initialize(
+                mMockTunFd,
+                true,
+                mMockNsdPublisher,
+                mOverriddenMeshcopTxts,
+                mMockCallback,
+                TEST_DEFAULT_COUNTRY_CODE);
         final AtomicReference<OtDaemonState> stateRef = new AtomicReference<>();
         final AtomicLong listenerIdRef = new AtomicLong();
+        final AtomicReference<BackboneRouterState> bbrStateRef = new AtomicReference<>();
 
         mFakeOtDaemon.registerStateCallback(
                 new IOtDaemonCallback.Default() {
@@ -122,6 +171,11 @@
                         stateRef.set(newState);
                         listenerIdRef.set(listenerId);
                     }
+
+                    @Override
+                    public void onBackboneRouterStateChanged(BackboneRouterState bbrState) {
+                        bbrStateRef.set(bbrState);
+                    }
                 },
                 7 /* listenerId */);
         mTestLooper.dispatchAll();
@@ -132,8 +186,9 @@
         assertThat(state.deviceRole).isEqualTo(FakeOtDaemon.OT_DEVICE_ROLE_DISABLED);
         assertThat(state.activeDatasetTlvs).isEmpty();
         assertThat(state.pendingDatasetTlvs).isEmpty();
-        assertThat(state.multicastForwardingEnabled).isFalse();
         assertThat(listenerIdRef.get()).isEqualTo(7);
+        BackboneRouterState bbrState = bbrStateRef.get();
+        assertThat(bbrState.multicastForwardingEnabled).isFalse();
     }
 
     @Test
@@ -156,12 +211,18 @@
     public void join_succeed_statesAreSentBack() throws Exception {
         final AtomicBoolean succeedRef = new AtomicBoolean(false);
         final AtomicReference<OtDaemonState> stateRef = new AtomicReference<>();
+        final AtomicReference<BackboneRouterState> bbrStateRef = new AtomicReference<>();
         mFakeOtDaemon.registerStateCallback(
                 new IOtDaemonCallback.Default() {
                     @Override
                     public void onStateChanged(OtDaemonState newState, long listenerId) {
                         stateRef.set(newState);
                     }
+
+                    @Override
+                    public void onBackboneRouterStateChanged(BackboneRouterState bbrState) {
+                        bbrStateRef.set(bbrState);
+                    }
                 },
                 11 /* listenerId */);
 
@@ -186,16 +247,17 @@
         assertThat(state.isInterfaceUp).isTrue();
         assertThat(state.deviceRole).isEqualTo(FakeOtDaemon.OT_DEVICE_ROLE_LEADER);
         assertThat(state.activeDatasetTlvs).isEqualTo(DEFAULT_ACTIVE_DATASET_TLVS);
-        assertThat(state.multicastForwardingEnabled).isTrue();
+        final BackboneRouterState bbrState = bbrStateRef.get();
+        assertThat(bbrState.multicastForwardingEnabled).isTrue();
     }
 
     @Test
-    public void setThreadEnabled_disableThread_succeed() throws Exception {
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(OT_STATE_ENABLED);
+    public void setThreadEnabled_enableThread_succeed() throws Exception {
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(OT_STATE_DISABLED);
 
         final AtomicBoolean succeedRef = new AtomicBoolean(false);
         mFakeOtDaemon.setThreadEnabled(
-                false,
+                true,
                 new IOtStatusReceiver.Default() {
                     @Override
                     public void onSuccess() {
@@ -205,6 +267,91 @@
         mTestLooper.dispatchAll();
 
         assertThat(succeedRef.get()).isTrue();
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(OT_STATE_ENABLED);
+    }
+
+    @Test
+    public void getChannelMasks_succeed_onSuccessIsInvoked() throws Exception {
+        final AtomicInteger supportedChannelMaskRef = new AtomicInteger();
+        final AtomicInteger preferredChannelMaskRef = new AtomicInteger();
+        final AtomicBoolean errorRef = new AtomicBoolean(false);
+        mFakeOtDaemon.setChannelMasks(
+                DEFAULT_SUPPORTED_CHANNEL_MASK, DEFAULT_PREFERRED_CHANNEL_MASK);
+        mFakeOtDaemon.setChannelMasksReceiverOtError(FakeOtDaemon.OT_ERROR_NONE);
+
+        mFakeOtDaemon.getChannelMasks(
+                new IChannelMasksReceiver.Default() {
+                    @Override
+                    public void onSuccess(int supportedChannelMask, int preferredChannelMask) {
+                        supportedChannelMaskRef.set(supportedChannelMask);
+                        preferredChannelMaskRef.set(preferredChannelMask);
+                    }
+
+                    @Override
+                    public void onError(int otError, String message) {
+                        errorRef.set(true);
+                    }
+                });
+        mTestLooper.dispatchAll();
+
+        assertThat(errorRef.get()).isFalse();
+        assertThat(supportedChannelMaskRef.get()).isEqualTo(DEFAULT_SUPPORTED_CHANNEL_MASK);
+        assertThat(preferredChannelMaskRef.get()).isEqualTo(DEFAULT_PREFERRED_CHANNEL_MASK);
+    }
+
+    @Test
+    public void getChannelMasks_failed_onErrorIsInvoked() throws Exception {
+        final AtomicInteger errorRef = new AtomicInteger(FakeOtDaemon.OT_ERROR_NONE);
+        final AtomicBoolean succeedRef = new AtomicBoolean(false);
+        mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_INVALID_STATE);
+
+        mFakeOtDaemon.getChannelMasks(
+                new IChannelMasksReceiver.Default() {
+                    @Override
+                    public void onSuccess(int supportedChannelMask, int preferredChannelMask) {
+                        succeedRef.set(true);
+                    }
+
+                    @Override
+                    public void onError(int otError, String message) {
+                        errorRef.set(otError);
+                    }
+                });
+        mTestLooper.dispatchAll();
+
+        assertThat(succeedRef.get()).isFalse();
+        assertThat(errorRef.get()).isEqualTo(OT_ERROR_INVALID_STATE);
+    }
+
+    @Test
+    public void terminate_statesAreResetAndDeathCallbackIsInvoked() throws Exception {
+        DeathRecipient mockDeathRecipient = mock(DeathRecipient.class);
+        mFakeOtDaemon.linkToDeath(mockDeathRecipient, 0);
+        mFakeOtDaemon.initialize(
+                mMockTunFd,
+                true,
+                mMockNsdPublisher,
+                mOverriddenMeshcopTxts,
+                mMockCallback,
+                TEST_DEFAULT_COUNTRY_CODE);
+
+        mFakeOtDaemon.terminate();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+        OtDaemonState state = mFakeOtDaemon.getState();
+        assertThat(state.isInterfaceUp).isEqualTo(false);
+        assertThat(state.partitionId).isEqualTo(-1);
+        assertThat(state.deviceRole).isEqualTo(OT_DEVICE_ROLE_DISABLED);
+        assertThat(state.activeDatasetTlvs).isEqualTo(new byte[0]);
+        assertThat(state.pendingDatasetTlvs).isEqualTo(new byte[0]);
+        BackboneRouterState bbrState = mFakeOtDaemon.getBackboneRouterState();
+        assertThat(bbrState.multicastForwardingEnabled).isFalse();
+        assertThat(bbrState.listeningAddresses).isEqualTo(new ArrayList<>());
+        assertThat(mFakeOtDaemon.getDeathRecipient()).isNull();
+        assertThat(mFakeOtDaemon.getTunFd()).isNull();
+        assertThat(mFakeOtDaemon.getNsdPublisher()).isNull();
         assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(OT_STATE_DISABLED);
+        verify(mockDeathRecipient, times(1)).binderDied();
     }
 }
diff --git a/tests/dbus/test_dbus_client.cpp b/tests/dbus/test_dbus_client.cpp
index 9c5e977..486e5e1 100644
--- a/tests/dbus/test_dbus_client.cpp
+++ b/tests/dbus/test_dbus_client.cpp
@@ -182,6 +182,24 @@
     TEST_ASSERT(srpServerInfo.mResponseCounters.mOther == 0);
 }
 
+void CheckTrelInfo(ThreadApiDBus *aApi)
+{
+    OTBR_UNUSED_VARIABLE(aApi);
+
+#if OTBR_ENABLE_TREL
+    otbr::DBus::TrelInfo trelInfo;
+
+    TEST_ASSERT(aApi->GetTrelInfo(trelInfo) == OTBR_ERROR_NONE);
+    TEST_ASSERT(trelInfo.mEnabled);
+    TEST_ASSERT(trelInfo.mNumTrelPeers == 0);
+    TEST_ASSERT(trelInfo.mTrelCounters.mTxPackets == 0);
+    TEST_ASSERT(trelInfo.mTrelCounters.mTxBytes == 0);
+    TEST_ASSERT(trelInfo.mTrelCounters.mTxFailure == 0);
+    TEST_ASSERT(trelInfo.mTrelCounters.mRxPackets == 0);
+    TEST_ASSERT(trelInfo.mTrelCounters.mRxBytes == 0);
+#endif
+}
+
 void CheckDnssdCounters(ThreadApiDBus *aApi)
 {
     OTBR_UNUSED_VARIABLE(aApi);
@@ -281,11 +299,20 @@
 #if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
     TEST_ASSERT(telemetryData.wpan_border_router().dns_server().response_counters().server_failure_count() == 0);
 #endif
+#if OTBR_ENABLE_TREL
+    TEST_ASSERT(telemetryData.wpan_border_router().trel_info().is_trel_enabled());
+    TEST_ASSERT(telemetryData.wpan_border_router().trel_info().has_counters());
+    TEST_ASSERT(telemetryData.wpan_border_router().trel_info().counters().trel_tx_packets() == 0);
+    TEST_ASSERT(telemetryData.wpan_border_router().trel_info().counters().trel_tx_bytes() == 0);
+#endif
     TEST_ASSERT(telemetryData.wpan_border_router().mdns().service_registration_responses().success_count() > 0);
 #if OTBR_ENABLE_NAT64
     TEST_ASSERT(telemetryData.wpan_border_router().nat64_state().prefix_manager_state() ==
                 threadnetwork::TelemetryData::NAT64_STATE_NOT_RUNNING);
 #endif
+#if OTBR_ENABLE_DHCP6_PD
+    TEST_ASSERT(!telemetryData.wpan_border_router().hashed_pd_prefix().empty());
+#endif
     TEST_ASSERT(telemetryData.wpan_rcp().rcp_interface_statistics().transferred_frames_count() > 0);
     TEST_ASSERT(telemetryData.coex_metrics().count_tx_request() > 0);
 #if OTBR_ENABLE_LINK_METRICS_TELEMETRY
@@ -420,6 +447,7 @@
                             TEST_ASSERT(api->GetRadioTxPower(txPower) == OTBR_ERROR_NONE);
                             TEST_ASSERT(api->GetActiveDatasetTlvs(activeDataset) == OTBR_ERROR_NONE);
                             CheckSrpServerInfo(api.get());
+                            CheckTrelInfo(api.get());
                             CheckMdnsInfo(api.get());
                             CheckDnssdCounters(api.get());
                             CheckNat64(api.get());
diff --git a/tests/mdns/CMakeLists.txt b/tests/mdns/CMakeLists.txt
index f4778fe..861a79f 100644
--- a/tests/mdns/CMakeLists.txt
+++ b/tests/mdns/CMakeLists.txt
@@ -87,3 +87,20 @@
     PROPERTIES
         ENVIRONMENT "OTBR_MDNS=${OTBR_MDNS};OTBR_TEST_MDNS=$<TARGET_FILE:otbr-test-mdns>"
 )
+
+add_executable(otbr-test-mdns-subscribe
+    test_subscribe.cpp
+)
+
+target_link_libraries(otbr-test-mdns-subscribe PRIVATE
+    otbr-config
+    otbr-mdns
+    $<$<BOOL:${CPPUTEST_LIBRARY_DIRS}>:-L$<JOIN:${CPPUTEST_LIBRARY_DIRS}," -L">>
+    ${CPPUTEST_LIBRARIES}
+)
+
+
+add_test(
+    NAME mdns-subscribe
+    COMMAND otbr-test-mdns-subscribe
+)
diff --git a/tests/mdns/main.cpp b/tests/mdns/main.cpp
index 66f6366..acf8777 100644
--- a/tests/mdns/main.cpp
+++ b/tests/mdns/main.cpp
@@ -26,6 +26,8 @@
  *    POSSIBILITY OF SUCH DAMAGE.
  */
 
+#define OTBR_LOG_TAG "TEST"
+
 #include <assert.h>
 #include <errno.h>
 #include <limits.h>
@@ -36,6 +38,7 @@
 #include <netinet/in.h>
 #include <signal.h>
 
+#include <functional>
 #include <vector>
 
 #include "common/code_utils.hpp"
@@ -45,12 +48,11 @@
 #include "mdns/mdns.hpp"
 
 using namespace otbr;
+using namespace otbr::Mdns;
 
-static struct Context
-{
-    Mdns::Publisher *mPublisher;
-    bool             mUpdate;
-} sContext;
+static Publisher *sPublisher = nullptr;
+
+typedef std::function<void(void)> TestRunner;
 
 int RunMainloop(void)
 {
@@ -82,327 +84,300 @@
     return rval;
 }
 
-void PublishSingleServiceWithCustomHost(void *aContext, Mdns::Publisher::State aState)
+std::function<void(otbrError aError)> ErrorChecker(std::string aMessage)
 {
-    uint8_t    xpanid[kSizeExtPanId]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t    extAddr[kSizeExtAddr]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t    hostAddr[OTBR_IP6_ADDRESS_SIZE] = {0};
-    const char hostName[]                      = "custom-host";
+    return [aMessage](otbrError aError) {
+        if (aError == OTBR_ERROR_NONE)
+        {
+            otbrLogInfo("Got success callback: %s", aMessage.c_str());
+        }
+        else
+        {
+            otbrLogEmerg("Got error %d callback: %s", aError, aMessage.c_str());
+            exit(-1);
+        }
+    };
+}
+
+void PublishSingleServiceWithCustomHost(void)
+{
+    uint8_t              xpanid[kSizeExtPanId]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t              extAddr[kSizeExtAddr]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t              hostAddr[OTBR_IP6_ADDRESS_SIZE] = {0};
+    const char           hostName[]                      = "custom-host";
+    std::vector<uint8_t> keyData                         = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+    Publisher::TxtData   txtData;
+    Publisher::TxtList   txtList{
+        {"nn", "cool"},
+        {"xp", xpanid, sizeof(xpanid)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+
+    otbrLogInfo("PublishSingleServiceWithCustomHost");
 
     hostAddr[0]  = 0x20;
     hostAddr[1]  = 0x02;
     hostAddr[15] = 0x01;
 
-    VerifyOrDie(aContext == &sContext, "unexpected context");
-    if (aState == Mdns::Publisher::State::kReady)
-    {
-        Mdns::Publisher::TxtData txtData;
-        Mdns::Publisher::TxtList txtList{
-            {"nn", "cool"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"xa", extAddr, sizeof(extAddr)}};
+    Publisher::EncodeTxtData(txtList, txtData);
 
-        Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-        sContext.mPublisher->PublishHost(hostName, {Ip6Address(hostAddr)},
-                                         [](otbrError aError) { SuccessOrDie(aError, "cannot publish the host"); });
-
-        sContext.mPublisher->PublishService(
-            hostName, "SingleService", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "cannot publish the service"); });
-    }
+    sPublisher->PublishKey(hostName, keyData, ErrorChecker("publish key for host"));
+    sPublisher->PublishHost(hostName, {Ip6Address(hostAddr)}, ErrorChecker("publish the host"));
+    sPublisher->PublishService(hostName, "SingleService", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData,
+                               ErrorChecker("publish the service"));
+    sPublisher->PublishKey("SingleService._meshcop._udp", keyData, ErrorChecker("publish key for service"));
 }
 
-void PublishMultipleServicesWithCustomHost(void *aContext, Mdns::Publisher::State aState)
+void PublishSingleServiceWithKeyAfterwards(void)
 {
-    uint8_t    xpanid[kSizeExtPanId]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t    extAddr[kSizeExtAddr]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t    hostAddr[OTBR_IP6_ADDRESS_SIZE] = {0};
-    const char hostName1[]                     = "custom-host-1";
-    const char hostName2[]                     = "custom-host-2";
+    uint8_t            hostAddr[OTBR_IP6_ADDRESS_SIZE] = {0};
+    const char         hostName[]                      = "custom-host";
+    Publisher::TxtData txtData;
+
+    otbrLogInfo("PublishSingleServiceWithKeyAfterwards");
 
     hostAddr[0]  = 0x20;
     hostAddr[1]  = 0x02;
     hostAddr[15] = 0x01;
 
-    VerifyOrDie(aContext == &sContext, "unexpected context");
-    if (aState == Mdns::Publisher::State::kReady)
-    {
-        Mdns::Publisher::TxtData txtData;
-        Mdns::Publisher::TxtList txtList{
-            {"nn", "cool"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"xa", extAddr, sizeof(extAddr)}};
-
-        Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-        sContext.mPublisher->PublishHost(hostName1, {Ip6Address(hostAddr)},
-                                         [](otbrError aError) { SuccessOrDie(aError, "cannot publish the host"); });
-
-        sContext.mPublisher->PublishService(
-            hostName1, "MultipleService11", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "cannot publish the first service"); });
-
-        sContext.mPublisher->PublishService(
-            hostName1, "MultipleService12", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "cannot publish the second service"); });
-
-        sContext.mPublisher->PublishHost(hostName2, {Ip6Address(hostAddr)}, [](otbrError aError) {
-            SuccessOrDie(aError, "cannot publish the second host");
-        });
-
-        sContext.mPublisher->PublishService(
-            hostName2, "MultipleService21", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "cannot publish the first service"); });
-
-        sContext.mPublisher->PublishService(
-            hostName2, "MultipleService22", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "cannot publish the second service"); });
-    }
-}
-
-void PublishSingleService(void *aContext, Mdns::Publisher::State aState)
-{
-    OT_UNUSED_VARIABLE(aContext);
-
-    uint8_t                  xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t                  extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    Mdns::Publisher::TxtData txtData;
-    Mdns::Publisher::TxtList txtList{
-        {"nn", "cool"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"xa", extAddr, sizeof(extAddr)}};
-
-    Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-    assert(aContext == &sContext);
-    if (aState == Mdns::Publisher::State::kReady)
-    {
-        sContext.mPublisher->PublishService(
-            "", "SingleService", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "SingleService._meshcop._udp"); });
-    }
-}
-
-void PublishSingleServiceWithEmptyName(void *aContext, Mdns::Publisher::State aState)
-{
-    OT_UNUSED_VARIABLE(aContext);
-
-    uint8_t                  xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t                  extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    Mdns::Publisher::TxtData txtData;
-    Mdns::Publisher::TxtList txtList{
-        {"nn", "cool"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"xa", extAddr, sizeof(extAddr)}};
-
-    Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-    assert(aContext == &sContext);
-    if (aState == Mdns::Publisher::State::kReady)
-    {
-        sContext.mPublisher->PublishService("", "", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-                                            [](otbrError aError) { SuccessOrDie(aError, "(empty)._meshcop._udp"); });
-    }
-}
-
-void PublishMultipleServices(void *aContext, Mdns::Publisher::State aState)
-{
-    OT_UNUSED_VARIABLE(aContext);
-
-    uint8_t xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-
-    assert(aContext == &sContext);
-    if (aState == Mdns::Publisher::State::kReady)
-    {
-        Mdns::Publisher::TxtData txtData;
-        Mdns::Publisher::TxtList txtList{
-            {"nn", "cool1"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"xa", extAddr, sizeof(extAddr)}};
-
-        Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-        sContext.mPublisher->PublishService(
-            "", "MultipleService1", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "MultipleService1._meshcop._udp"); });
-    }
-
-    if (aState == Mdns::Publisher::State::kReady)
-    {
-        Mdns::Publisher::TxtData txtData;
-        Mdns::Publisher::TxtList txtList{
-            {"nn", "cool2"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"xa", extAddr, sizeof(extAddr)}};
-
-        Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-        sContext.mPublisher->PublishService(
-            "", "MultipleService2", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "MultipleService2._meshcop._udp"); });
-    }
-}
-
-void PublishUpdateServices(void *aContext)
-{
-    OT_UNUSED_VARIABLE(aContext);
-
-    uint8_t xpanidOld[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-    uint8_t xpanidNew[kSizeExtPanId] = {0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41};
-    uint8_t extAddr[kSizeExtAddr]    = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
-
-    assert(aContext == &sContext);
-    if (!sContext.mUpdate)
-    {
-        Mdns::Publisher::TxtData txtData;
-        Mdns::Publisher::TxtList txtList{
-            {"nn", "cool"}, {"xp", xpanidOld, sizeof(xpanidOld)}, {"tv", "1.1.1"}, {"xa", extAddr, sizeof(extAddr)}};
-
-        Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-        sContext.mPublisher->PublishService(
-            "", "UpdateService", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { otbrLogResult(aError, "UpdateService._meshcop._udp"); });
-    }
-    else
-    {
-        Mdns::Publisher::TxtData txtData;
-        Mdns::Publisher::TxtList txtList{{"nn", "coolcool"},
-                                         {"xp", xpanidNew, sizeof(xpanidNew)},
-                                         {"tv", "1.1.1"},
-                                         {"xa", extAddr, sizeof(extAddr)}};
-
-        Mdns::Publisher::EncodeTxtData(txtList, txtData);
-
-        sContext.mPublisher->PublishService(
-            "", "UpdateService", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
-            [](otbrError aError) { SuccessOrDie(aError, "UpdateService._meshcop._udp"); });
-    }
-}
-
-void PublishServiceSubTypes(void *aContext)
-{
-    Mdns::Publisher::TxtData txtData;
-
     txtData.push_back(0);
 
-    OT_UNUSED_VARIABLE(aContext);
+    sPublisher->PublishHost(hostName, {Ip6Address(hostAddr)}, ErrorChecker("publish the host"));
 
-    assert(aContext == &sContext);
+    sPublisher->PublishService(
+        hostName, "SingleService", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData, [](otbrError aError) {
+            std::vector<uint8_t> keyData = {0x55, 0xaa, 0xbb, 0xcc, 0x77, 0x33};
 
-    Mdns::Publisher::SubTypeList subTypeList{"_subtype1", "_SUBTYPE2"};
+            SuccessOrDie(aError, "publish the service");
+
+            sPublisher->PublishKey("SingleService._meshcop._udp", keyData, ErrorChecker("publish key for service"));
+        });
+}
+
+void PublishMultipleServicesWithCustomHost(void)
+{
+    uint8_t              xpanid[kSizeExtPanId]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t              extAddr[kSizeExtAddr]           = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t              hostAddr[OTBR_IP6_ADDRESS_SIZE] = {0};
+    const char           hostName1[]                     = "custom-host-1";
+    const char           hostName2[]                     = "custom-host-2";
+    std::vector<uint8_t> keyData1                        = {0x10, 0x20, 0x03, 0x15};
+    std::vector<uint8_t> keyData2                        = {0xCA, 0xFE, 0xBE, 0xEF};
+    Publisher::TxtData   txtData;
+    Publisher::TxtList   txtList{
+        {"nn", "cool"},
+        {"xp", xpanid, sizeof(xpanid)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+
+    otbrLogInfo("PublishMultipleServicesWithCustomHost");
+
+    hostAddr[0]  = 0x20;
+    hostAddr[1]  = 0x02;
+    hostAddr[15] = 0x01;
+
+    Publisher::EncodeTxtData(txtList, txtData);
+
+    // For host1 and its services we register keys first, then host/services
+
+    sPublisher->PublishKey(hostName1, keyData1, ErrorChecker("publish key for host1"));
+    sPublisher->PublishKey("MultipleService11._meshcop._udp", keyData1, ErrorChecker("publish key for service11"));
+    sPublisher->PublishKey("MultipleService12._meshcop._udp", keyData1, ErrorChecker("publish key for service12"));
+
+    sPublisher->PublishHost(hostName1, {Ip6Address(hostAddr)}, ErrorChecker("publish the host1"));
+    sPublisher->PublishService(hostName1, "MultipleService11", "_meshcop._udp", Publisher::SubTypeList{}, 12345,
+                               txtData, ErrorChecker("publish service11"));
+    sPublisher->PublishService(hostName1, "MultipleService12", "_meshcop._udp", Publisher::SubTypeList{}, 12345,
+                               txtData, ErrorChecker("publish service12"));
+
+    // For host2 and its services we register host and services first, then keys.
+
+    sPublisher->PublishHost(hostName2, {Ip6Address(hostAddr)}, ErrorChecker("publish host2"));
+    sPublisher->PublishService(hostName2, "MultipleService21", "_meshcop._udp", Publisher::SubTypeList{}, 12345,
+                               txtData, ErrorChecker("publish service21"));
+    sPublisher->PublishService(hostName2, "MultipleService22", "_meshcop._udp", Publisher::SubTypeList{}, 12345,
+                               txtData, ErrorChecker("publish service22"));
+
+    sPublisher->PublishKey(hostName2, keyData2, ErrorChecker("publish key for host2"));
+    sPublisher->PublishKey("MultipleService21._meshcop._udp", keyData2, ErrorChecker("publish key for service21"));
+    sPublisher->PublishKey("MultipleService22._meshcop._udp", keyData2, ErrorChecker("publish key for service22"));
+}
+
+void PublishSingleService(void)
+{
+    uint8_t            xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t            extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    Publisher::TxtData txtData;
+    Publisher::TxtList txtList{
+        {"nn", "cool"},
+        {"xp", xpanid, sizeof(xpanid)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+
+    otbrLogInfo("PublishSingleService");
+
+    Publisher::EncodeTxtData(txtList, txtData);
+
+    sPublisher->PublishService("", "SingleService", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData,
+                               ErrorChecker("publish service"));
+}
+
+void PublishSingleServiceWithEmptyName(void)
+{
+    uint8_t            xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t            extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    Publisher::TxtData txtData;
+    Publisher::TxtList txtList{
+        {"nn", "cool"},
+        {"xp", xpanid, sizeof(xpanid)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+
+    otbrLogInfo("PublishSingleServiceWithEmptyName");
+
+    Publisher::EncodeTxtData(txtList, txtData);
+
+    sPublisher->PublishService("", "", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData,
+                               ErrorChecker("publish (empty)._meshcop._udp"));
+}
+
+void PublishMultipleServices(void)
+{
+    uint8_t            xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t            extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    Publisher::TxtData txtData;
+    Publisher::TxtList txtList1{
+        {"nn", "cool1"},
+        {"xp", xpanid, sizeof(xpanid)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+    Publisher::TxtList txtList2{
+        {"nn", "cool2"},
+        {"xp", xpanid, sizeof(xpanid)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+
+    otbrLogInfo("PublishMultipleServices");
+
+    Publisher::EncodeTxtData(txtList1, txtData);
+
+    sPublisher->PublishService("", "MultipleService1", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData,
+                               ErrorChecker("publish MultipleService1._meshcop._udp"));
+
+    Publisher::EncodeTxtData(txtList2, txtData);
+
+    sPublisher->PublishService("", "MultipleService2", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData,
+                               ErrorChecker("publish MultipleService2._meshcop._udp"));
+}
+
+void PublishUpdateServices(void)
+{
+    uint8_t            xpanidOld[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    uint8_t            xpanidNew[kSizeExtPanId] = {0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41};
+    uint8_t            extAddr[kSizeExtAddr]    = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};
+    Publisher::TxtData txtData;
+    Publisher::TxtList txtList1{
+        {"nn", "cool"},
+        {"xp", xpanidOld, sizeof(xpanidOld)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+    Publisher::TxtList txtList2{
+        {"nn", "coolcool"},
+        {"xp", xpanidNew, sizeof(xpanidNew)},
+        {"tv", "1.1.1"},
+        {"xa", extAddr, sizeof(extAddr)},
+    };
+
+    otbrLogInfo("PublishUpdateServices");
+
+    Publisher::EncodeTxtData(txtList1, txtData);
+
+    sPublisher->PublishService("", "UpdateService", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData,
+                               [](otbrError aError) { otbrLogResult(aError, "UpdateService._meshcop._udp"); });
+
+    Publisher::EncodeTxtData(txtList2, txtData);
+
+    sPublisher->PublishService("", "UpdateService", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData,
+                               ErrorChecker("publish UpdateService._meshcop._udp"));
+}
+
+void PublishServiceSubTypes(void)
+{
+    Publisher::TxtData     txtData;
+    Publisher::SubTypeList subTypeList{"_subtype1", "_SUBTYPE2"};
+
+    otbrLogInfo("PublishServiceSubTypes");
+
+    txtData.push_back(0);
 
     subTypeList.back() = "_SUBTYPE3";
 
-    sContext.mPublisher->PublishService(
-        "", "ServiceWithSubTypes", "_meshcop._udp", subTypeList, 12345, txtData,
-        [](otbrError aError) { SuccessOrDie(aError, "ServiceWithSubTypes._meshcop._udp"); });
+    sPublisher->PublishService("", "ServiceWithSubTypes", "_meshcop._udp", subTypeList, 12345, txtData,
+                               ErrorChecker("publish ServiceWithSubTypes._meshcop._udp"));
 }
 
-otbrError TestSingleServiceWithCustomHost(void)
+void PublishKey(void)
+{
+    std::vector<uint8_t> keyData = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
+
+    otbrLogInfo("PublishKey");
+
+    sPublisher->PublishKey("SingleService._meshcop._udp", keyData, ErrorChecker("publish key for service"));
+}
+
+void PublishKeyWithServiceRemoved(void)
+{
+    uint8_t            hostAddr[OTBR_IP6_ADDRESS_SIZE] = {0};
+    const char         hostName[]                      = "custom-host";
+    Publisher::TxtData txtData;
+
+    otbrLogInfo("PublishKeyWithServiceRemoved");
+
+    hostAddr[0]  = 0x20;
+    hostAddr[1]  = 0x02;
+    hostAddr[15] = 0x01;
+
+    txtData.push_back(0);
+
+    sPublisher->PublishHost(hostName, {Ip6Address(hostAddr)}, ErrorChecker("publish the host"));
+
+    sPublisher->PublishService(
+        hostName, "SingleService", "_meshcop._udp", Publisher::SubTypeList{}, 12345, txtData, [](otbrError aError) {
+            std::vector<uint8_t> keyData = {0x55, 0xaa, 0xbb, 0xcc, 0x77, 0x33};
+
+            SuccessOrDie(aError, "publish the service");
+
+            sPublisher->PublishKey("SingleService._meshcop._udp", keyData, [](otbrError aError) {
+                SuccessOrDie(aError, "publish key for service");
+
+                sPublisher->UnpublishService("SingleService", "_meshcop._udp", ErrorChecker("unpublish service"));
+            });
+        });
+}
+
+otbrError Test(TestRunner aTestRunner)
 {
     otbrError error = OTBR_ERROR_NONE;
 
-    Mdns::Publisher *pub = Mdns::Publisher::Create(
-        [](Mdns::Publisher::State aState) { PublishSingleServiceWithCustomHost(&sContext, aState); });
-    sContext.mPublisher = pub;
-    SuccessOrExit(error = pub->Start());
-    RunMainloop();
-
-exit:
-    Mdns::Publisher::Destroy(pub);
-    return error;
-}
-
-otbrError TestMultipleServicesWithCustomHost(void)
-{
-    otbrError error = OTBR_ERROR_NONE;
-
-    Mdns::Publisher *pub = Mdns::Publisher::Create(
-        [](Mdns::Publisher::State aState) { PublishMultipleServicesWithCustomHost(&sContext, aState); });
-    sContext.mPublisher = pub;
-    SuccessOrExit(error = pub->Start());
-    RunMainloop();
-
-exit:
-    Mdns::Publisher::Destroy(pub);
-    return error;
-}
-
-otbrError TestSingleService(void)
-{
-    otbrError ret = OTBR_ERROR_NONE;
-
-    Mdns::Publisher *pub =
-        Mdns::Publisher::Create([](Mdns::Publisher::State aState) { PublishSingleService(&sContext, aState); });
-    sContext.mPublisher = pub;
-    SuccessOrExit(ret = pub->Start());
-    RunMainloop();
-
-exit:
-    Mdns::Publisher::Destroy(pub);
-    return ret;
-}
-
-otbrError TestSingleServiceWithEmptyName(void)
-{
-    otbrError ret = OTBR_ERROR_NONE;
-
-    Mdns::Publisher *pub = Mdns::Publisher::Create(
-        [](Mdns::Publisher::State aState) { PublishSingleServiceWithEmptyName(&sContext, aState); });
-    sContext.mPublisher = pub;
-    SuccessOrExit(ret = pub->Start());
-    RunMainloop();
-
-exit:
-    Mdns::Publisher::Destroy(pub);
-    return ret;
-}
-
-otbrError TestMultipleServices(void)
-{
-    otbrError ret = OTBR_ERROR_NONE;
-
-    Mdns::Publisher *pub =
-        Mdns::Publisher::Create([](Mdns::Publisher::State aState) { PublishMultipleServices(&sContext, aState); });
-    sContext.mPublisher = pub;
-    SuccessOrExit(ret = pub->Start());
-    RunMainloop();
-
-exit:
-    Mdns::Publisher::Destroy(pub);
-    return ret;
-}
-
-otbrError TestUpdateService(void)
-{
-    otbrError ret = OTBR_ERROR_NONE;
-
-    Mdns::Publisher *pub = Mdns::Publisher::Create([](Mdns::Publisher::State aState) {
-        if (aState == Mdns::Publisher::State::kReady)
+    sPublisher = Publisher::Create([aTestRunner](Publisher::State aState) {
+        if (aState == Publisher::State::kReady)
         {
-            sContext.mUpdate = false;
-            PublishUpdateServices(&sContext);
-            sContext.mUpdate = true;
-            PublishUpdateServices(&sContext);
+            aTestRunner();
         }
     });
-    sContext.mPublisher  = pub;
-    SuccessOrExit(ret = pub->Start());
+    SuccessOrExit(error = sPublisher->Start());
     RunMainloop();
 
 exit:
-    Mdns::Publisher::Destroy(pub);
-    return ret;
-}
-
-otbrError TestServiceSubTypes(void)
-{
-    otbrError ret = OTBR_ERROR_NONE;
-
-    Mdns::Publisher *pub = Mdns::Publisher::Create([](Mdns::Publisher::State aState) {
-        if (aState == Mdns::Publisher::State::kReady)
-        {
-            PublishServiceSubTypes(&sContext);
-        }
-    });
-    sContext.mPublisher  = pub;
-    SuccessOrExit(ret = pub->Start());
-    RunMainloop();
-
-exit:
-    Mdns::Publisher::Destroy(pub);
-    return ret;
+    Publisher::Destroy(sPublisher);
+    return error;
 }
 
 void RecoverSignal(int aSignal)
@@ -421,37 +396,42 @@
 {
     otbrError ret = OTBR_ERROR_NONE;
 
-    Mdns::Publisher *pub =
-        Mdns::Publisher::Create([](Mdns::Publisher::State aState) { PublishSingleService(&sContext, aState); });
-    sContext.mPublisher = pub;
-    SuccessOrExit(ret = pub->Start());
+    otbrLogInfo("TestStopService");
+
+    sPublisher = Publisher::Create([](Publisher::State aState) {
+        if (aState == Publisher::State::kReady)
+        {
+            PublishSingleService();
+        }
+    });
+    SuccessOrExit(ret = sPublisher->Start());
     signal(SIGUSR1, RecoverSignal);
     signal(SIGUSR2, RecoverSignal);
     RunMainloop();
-    sContext.mPublisher->Stop();
+    sPublisher->Stop();
     RunMainloop();
-    SuccessOrExit(ret = sContext.mPublisher->Start());
+    SuccessOrExit(ret = sPublisher->Start());
     RunMainloop();
 
 exit:
-    Mdns::Publisher::Destroy(pub);
+    Publisher::Destroy(sPublisher);
     return ret;
 }
 
 otbrError CheckTxtDataEncoderDecoder(void)
 {
-    otbrError                error = OTBR_ERROR_NONE;
-    Mdns::Publisher::TxtList txtList;
-    Mdns::Publisher::TxtList parsedTxtList;
-    std::vector<uint8_t>     txtData;
+    otbrError            error = OTBR_ERROR_NONE;
+    Publisher::TxtList   txtList;
+    Publisher::TxtList   parsedTxtList;
+    std::vector<uint8_t> txtData;
 
     // Encode empty `TxtList`
 
-    SuccessOrExit(error = Mdns::Publisher::EncodeTxtData(txtList, txtData));
+    SuccessOrExit(error = Publisher::EncodeTxtData(txtList, txtData));
     VerifyOrExit(txtData.size() == 1, error = OTBR_ERROR_PARSE);
     VerifyOrExit(txtData[0] == 0, error = OTBR_ERROR_PARSE);
 
-    SuccessOrExit(error = Mdns::Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
+    SuccessOrExit(error = Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
     VerifyOrExit(parsedTxtList.size() == 0, error = OTBR_ERROR_PARSE);
 
     // TxtList with one bool attribute
@@ -459,8 +439,8 @@
     txtList.clear();
     txtList.emplace_back("b1");
 
-    SuccessOrExit(error = Mdns::Publisher::EncodeTxtData(txtList, txtData));
-    SuccessOrExit(error = Mdns::Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
+    SuccessOrExit(error = Publisher::EncodeTxtData(txtList, txtData));
+    SuccessOrExit(error = Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
     VerifyOrExit(parsedTxtList == txtList, error = OTBR_ERROR_PARSE);
 
     // TxtList with one one key/value
@@ -468,8 +448,8 @@
     txtList.clear();
     txtList.emplace_back("k1", "v1");
 
-    SuccessOrExit(error = Mdns::Publisher::EncodeTxtData(txtList, txtData));
-    SuccessOrExit(error = Mdns::Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
+    SuccessOrExit(error = Publisher::EncodeTxtData(txtList, txtData));
+    SuccessOrExit(error = Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
     VerifyOrExit(parsedTxtList == txtList, error = OTBR_ERROR_PARSE);
 
     // TxtList with multiple entries
@@ -480,8 +460,8 @@
     txtList.emplace_back("b2");
     txtList.emplace_back("k2", "valu2");
 
-    SuccessOrExit(error = Mdns::Publisher::EncodeTxtData(txtList, txtData));
-    SuccessOrExit(error = Mdns::Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
+    SuccessOrExit(error = Publisher::EncodeTxtData(txtList, txtData));
+    SuccessOrExit(error = Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
     VerifyOrExit(parsedTxtList == txtList, error = OTBR_ERROR_PARSE);
 
 exit:
@@ -502,7 +482,7 @@
         return 1;
     }
 
-    otbrLogInit("otbr-mdns", OTBR_LOG_DEBUG, true);
+    otbrLogInit("otbr-mdns", OTBR_LOG_DEBUG, true, false);
     // allow quitting elegantly
     signal(SIGTERM, RecoverSignal);
     switch (argv[1][0])
@@ -511,33 +491,44 @@
         switch (argv[1][1])
         {
         case 'c':
-            ret = TestSingleServiceWithCustomHost();
+            ret = Test(PublishSingleServiceWithCustomHost);
             break;
         case 'e':
-            ret = TestSingleServiceWithEmptyName();
+            ret = Test(PublishSingleServiceWithEmptyName);
+            break;
+        case 'k':
+            ret = Test(PublishSingleServiceWithKeyAfterwards);
             break;
         default:
-            ret = TestSingleService();
+            ret = Test(PublishSingleService);
             break;
         }
         break;
 
     case 'm':
-        ret = argv[1][1] == 'c' ? TestMultipleServicesWithCustomHost() : TestMultipleServices();
+        ret = argv[1][1] == 'c' ? Test(PublishMultipleServicesWithCustomHost) : Test(PublishMultipleServices);
         break;
 
     case 'u':
-        ret = TestUpdateService();
+        ret = Test(PublishUpdateServices);
         break;
 
     case 't':
-        ret = TestServiceSubTypes();
+        ret = Test(PublishServiceSubTypes);
         break;
 
     case 'k':
         ret = TestStopService();
         break;
 
+    case 'y':
+        ret = Test(PublishKey);
+        break;
+
+    case 'z':
+        ret = Test(PublishKeyWithServiceRemoved);
+        break;
+
     default:
         ret = 1;
         break;
diff --git a/tests/mdns/test_subscribe.cpp b/tests/mdns/test_subscribe.cpp
new file mode 100644
index 0000000..b01be3e
--- /dev/null
+++ b/tests/mdns/test_subscribe.cpp
@@ -0,0 +1,347 @@
+/*
+ *    Copyright (c) 2023, The OpenThread Authors.
+ *    All rights reserved.
+ *
+ *    Redistribution and use in source and binary forms, with or without
+ *    modification, are permitted provided that the following conditions are met:
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *    3. Neither the name of the copyright holder nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ *    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *    POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <netinet/in.h>
+#include <signal.h>
+
+#include <set>
+#include <vector>
+
+#include "common/mainloop.hpp"
+#include "common/mainloop_manager.hpp"
+#include "mdns/mdns.hpp"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+using namespace otbr;
+using namespace otbr::Mdns;
+
+TEST_GROUP(Mdns){};
+
+static constexpr int kTimeoutSeconds = 3;
+
+SimpleString StringFrom(const std::set<Ip6Address> &aAddresses)
+{
+    std::string result = "[";
+
+    for (const auto &address : aAddresses)
+    {
+        result += address.ToString() + ",";
+    }
+    result.back() = ']';
+
+    return SimpleString(result.c_str());
+}
+
+int RunMainloopUntilTimeout(int aSeconds)
+{
+    using namespace otbr;
+
+    int  rval      = 0;
+    auto beginTime = Clock::now();
+
+    while (true)
+    {
+        MainloopContext mainloop;
+
+        mainloop.mMaxFd   = -1;
+        mainloop.mTimeout = {1, 0};
+        FD_ZERO(&mainloop.mReadFdSet);
+        FD_ZERO(&mainloop.mWriteFdSet);
+        FD_ZERO(&mainloop.mErrorFdSet);
+
+        MainloopManager::GetInstance().Update(mainloop);
+        rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet,
+                      (mainloop.mTimeout.tv_sec == INT_MAX ? nullptr : &mainloop.mTimeout));
+
+        if (rval < 0)
+        {
+            perror("select");
+            break;
+        }
+
+        MainloopManager::GetInstance().Process(mainloop);
+
+        if (Clock::now() - beginTime >= std::chrono::seconds(aSeconds))
+        {
+            break;
+        }
+    }
+
+    return rval;
+}
+
+template <typename Container> std::set<typename Container::value_type> AsSet(const Container &aContainer)
+{
+    return std::set<typename Container::value_type>(aContainer.begin(), aContainer.end());
+}
+
+Publisher::ResultCallback NoOpCallback(void)
+{
+    return [](otbrError aError) { OTBR_UNUSED_VARIABLE(aError); };
+}
+
+std::map<std::string, std::vector<uint8_t>> AsTxtMap(const Publisher::TxtData &aTxtData)
+{
+    Publisher::TxtList                          txtList;
+    std::map<std::string, std::vector<uint8_t>> map;
+
+    Publisher::DecodeTxtData(txtList, aTxtData.data(), aTxtData.size());
+    for (const auto &entry : txtList)
+    {
+        map[entry.mKey] = entry.mValue;
+    }
+
+    return map;
+}
+
+Publisher::TxtList sTxtList1{{"a", "1"}, {"b", "2"}};
+Publisher::TxtData sTxtData1;
+Ip6Address         sAddr1;
+Ip6Address         sAddr2;
+Ip6Address         sAddr3;
+Ip6Address         sAddr4;
+
+void SetUp(void)
+{
+    otbrLogInit("test-mdns-subscriber", OTBR_LOG_INFO, true, false);
+    SuccessOrDie(Ip6Address::FromString("2002::1", sAddr1), "");
+    SuccessOrDie(Ip6Address::FromString("2002::2", sAddr2), "");
+    SuccessOrDie(Ip6Address::FromString("2002::3", sAddr3), "");
+    SuccessOrDie(Ip6Address::FromString("2002::4", sAddr4), "");
+    SuccessOrDie(Publisher::EncodeTxtData(sTxtList1, sTxtData1), "");
+}
+
+std::unique_ptr<Publisher> CreatePublisher(void)
+{
+    bool                       ready = false;
+    std::unique_ptr<Publisher> publisher{Publisher::Create([&publisher, &ready](Mdns::Publisher::State aState) {
+        if (aState == Publisher::State::kReady)
+        {
+            ready = true;
+        }
+    })};
+
+    publisher->Start();
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_TRUE(ready);
+
+    return publisher;
+}
+
+void CheckServiceInstance(const Publisher::DiscoveredInstanceInfo aInstanceInfo,
+                          bool                                    aRemoved,
+                          const std::string                      &aHostName,
+                          const std::vector<Ip6Address>          &aAddresses,
+                          const std::string                      &aServiceName,
+                          uint16_t                                aPort,
+                          const Publisher::TxtData                aTxtData)
+{
+    CHECK_EQUAL(aRemoved, aInstanceInfo.mRemoved);
+    CHECK_EQUAL(aServiceName, aInstanceInfo.mName);
+    if (!aRemoved)
+    {
+        CHECK_EQUAL(aHostName, aInstanceInfo.mHostName);
+        CHECK_EQUAL(AsSet(aAddresses), AsSet(aInstanceInfo.mAddresses));
+        CHECK_EQUAL(aPort, aInstanceInfo.mPort);
+        CHECK(AsTxtMap(aTxtData) == AsTxtMap(aInstanceInfo.mTxtData));
+    }
+}
+
+void CheckServiceInstanceAdded(const Publisher::DiscoveredInstanceInfo aInstanceInfo,
+                               const std::string                      &aHostName,
+                               const std::vector<Ip6Address>          &aAddresses,
+                               const std::string                      &aServiceName,
+                               uint16_t                                aPort,
+                               const Publisher::TxtData                aTxtData)
+{
+    CheckServiceInstance(aInstanceInfo, false, aHostName, aAddresses, aServiceName, aPort, aTxtData);
+}
+
+void CheckServiceInstanceRemoved(const Publisher::DiscoveredInstanceInfo aInstanceInfo, const std::string &aServiceName)
+{
+    CheckServiceInstance(aInstanceInfo, true, "", {}, aServiceName, 0, {});
+}
+
+void CheckHostAdded(const Publisher::DiscoveredHostInfo &aHostInfo,
+                    const std::string                   &aHostName,
+                    const std::vector<Ip6Address>       &aAddresses)
+{
+    CHECK_EQUAL(aHostName, aHostInfo.mHostName);
+    CHECK_EQUAL(AsSet(aAddresses), AsSet(aHostInfo.mAddresses));
+}
+
+TEST(Mdns, SubscribeHost)
+{
+    std::unique_ptr<Publisher>    pub = CreatePublisher();
+    std::string                   lastHostName;
+    Publisher::DiscoveredHostInfo lastHostInfo{};
+
+    auto clearLastHost = [&lastHostName, &lastHostInfo] {
+        lastHostName = "";
+        lastHostInfo = {};
+    };
+
+    pub->AddSubscriptionCallbacks(
+        nullptr,
+        [&lastHostName, &lastHostInfo](const std::string &aHostName, const Publisher::DiscoveredHostInfo &aHostInfo) {
+            lastHostName = aHostName;
+            lastHostInfo = aHostInfo;
+        });
+    pub->SubscribeHost("host1");
+
+    pub->PublishHost("host1", Publisher::AddressList{sAddr1, sAddr2}, NoOpCallback());
+    pub->PublishService("host1", "service1", "_test._tcp", Publisher::SubTypeList{"_sub1", "_sub2"}, 11111, sTxtData1,
+                        NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("host1", lastHostName);
+    CheckHostAdded(lastHostInfo, "host1.local.", {sAddr1, sAddr2});
+    clearLastHost();
+
+    pub->PublishService("host1", "service2", "_test._tcp", {}, 22222, {}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("", lastHostName);
+    clearLastHost();
+
+    pub->PublishHost("host2", Publisher::AddressList{sAddr3}, NoOpCallback());
+    pub->PublishService("host2", "service3", "_test._tcp", {}, 33333, {}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("", lastHostName);
+    clearLastHost();
+}
+
+TEST(Mdns, SubscribeServiceInstance)
+{
+    std::unique_ptr<Publisher>        pub = CreatePublisher();
+    std::string                       lastServiceType;
+    Publisher::DiscoveredInstanceInfo lastInstanceInfo{};
+
+    auto clearLastInstance = [&lastServiceType, &lastInstanceInfo] {
+        lastServiceType  = "";
+        lastInstanceInfo = {};
+    };
+
+    pub->AddSubscriptionCallbacks(
+        [&lastServiceType, &lastInstanceInfo](const std::string                &aType,
+                                              Publisher::DiscoveredInstanceInfo aInstanceInfo) {
+            lastServiceType  = aType;
+            lastInstanceInfo = aInstanceInfo;
+        },
+        nullptr);
+    pub->SubscribeService("_test._tcp", "service1");
+
+    pub->PublishHost("host1", Publisher::AddressList{sAddr1, sAddr2}, NoOpCallback());
+    pub->PublishService("host1", "service1", "_test._tcp", Publisher::SubTypeList{"_sub1", "_sub2"}, 11111, sTxtData1,
+                        NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("_test._tcp", lastServiceType);
+    CheckServiceInstanceAdded(lastInstanceInfo, "host1.local.", {sAddr1, sAddr2}, "service1", 11111, sTxtData1);
+    clearLastInstance();
+
+    pub->PublishService("host1", "service2", "_test._tcp", {}, 22222, {}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("", lastServiceType);
+    clearLastInstance();
+
+    pub->PublishHost("host2", Publisher::AddressList{sAddr3}, NoOpCallback());
+    pub->PublishService("host2", "service3", "_test._tcp", {}, 33333, {}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("", lastServiceType);
+    clearLastInstance();
+}
+
+TEST(Mdns, SubscribeServiceType)
+{
+    std::unique_ptr<Publisher>        pub = CreatePublisher();
+    std::string                       lastServiceType;
+    Publisher::DiscoveredInstanceInfo lastInstanceInfo{};
+
+    auto clearLastInstance = [&lastServiceType, &lastInstanceInfo] {
+        lastServiceType  = "";
+        lastInstanceInfo = {};
+    };
+
+    pub->AddSubscriptionCallbacks(
+        [&lastServiceType, &lastInstanceInfo](const std::string                &aType,
+                                              Publisher::DiscoveredInstanceInfo aInstanceInfo) {
+            lastServiceType  = aType;
+            lastInstanceInfo = aInstanceInfo;
+        },
+        nullptr);
+    pub->SubscribeService("_test._tcp", "");
+
+    pub->PublishHost("host1", Publisher::AddressList{sAddr1, sAddr2}, NoOpCallback());
+    pub->PublishService("host1", "service1", "_test._tcp", Publisher::SubTypeList{"_sub1", "_sub2"}, 11111, sTxtData1,
+                        NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("_test._tcp", lastServiceType);
+    CheckServiceInstanceAdded(lastInstanceInfo, "host1.local.", {sAddr1, sAddr2}, "service1", 11111, sTxtData1);
+    clearLastInstance();
+
+    pub->PublishService("host1", "service2", "_test._tcp", {}, 22222, {}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("_test._tcp", lastServiceType);
+    CheckServiceInstanceAdded(lastInstanceInfo, "host1.local.", {sAddr1, sAddr2}, "service2", 22222, {});
+    clearLastInstance();
+
+    pub->PublishHost("host2", Publisher::AddressList{sAddr3}, NoOpCallback());
+    pub->PublishService("host2", "service3", "_test._tcp", {}, 33333, {}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("_test._tcp", lastServiceType);
+    CheckServiceInstanceAdded(lastInstanceInfo, "host2.local.", {sAddr3}, "service3", 33333, {});
+    clearLastInstance();
+
+    pub->UnpublishHost("host2", NoOpCallback());
+    pub->UnpublishService("service3", "_test._tcp", NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("_test._tcp", lastServiceType);
+    CheckServiceInstanceRemoved(lastInstanceInfo, "service3");
+    clearLastInstance();
+
+    pub->PublishHost("host2", {sAddr3}, NoOpCallback());
+    pub->PublishService("host2", "service3", "_test._tcp", {}, 44444, {}, NoOpCallback());
+    pub->PublishHost("host2", {sAddr3, sAddr4}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("_test._tcp", lastServiceType);
+    CheckServiceInstanceAdded(lastInstanceInfo, "host2.local.", {sAddr3, sAddr4}, "service3", 44444, {});
+    clearLastInstance();
+
+    pub->PublishHost("host2", {sAddr4}, NoOpCallback());
+    RunMainloopUntilTimeout(kTimeoutSeconds);
+    CHECK_EQUAL("_test._tcp", lastServiceType);
+    CheckServiceInstanceAdded(lastInstanceInfo, "host2.local.", {sAddr4}, "service3", 44444, {});
+    clearLastInstance();
+}
+
+int main(int argc, const char *argv[])
+{
+    SetUp();
+
+    return RUN_ALL_TESTS(argc, argv);
+}
diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp
index 30bd476..c299703 100644
--- a/tests/unit/test_logging.cpp
+++ b/tests/unit/test_logging.cpp
@@ -43,7 +43,7 @@
     char ident[20];
 
     snprintf(ident, sizeof(ident), "otbr-test-%ld", clock());
-    otbrLogInit(ident, OTBR_LOG_INFO, true);
+    otbrLogInit(ident, OTBR_LOG_INFO, true, false);
     otbrLog(OTBR_LOG_DEBUG, OTBR_LOG_TAG, "cool-higher");
     otbrLogDeinit();
     sleep(0);
@@ -58,7 +58,7 @@
     char ident[20];
 
     snprintf(ident, sizeof(ident), "otbr-test-%ld", clock());
-    otbrLogInit(ident, OTBR_LOG_INFO, true);
+    otbrLogInit(ident, OTBR_LOG_INFO, true, false);
     otbrLog(OTBR_LOG_INFO, OTBR_LOG_TAG, "cool-equal");
     otbrLogDeinit();
     sleep(0);
@@ -69,13 +69,29 @@
     CHECK(0 == system(cmd));
 }
 
+TEST(Logging, TestLoggingEqualLevelNoSyslog)
+{
+    char ident[20];
+
+    snprintf(ident, sizeof(ident), "otbr-test-%ld", clock());
+    otbrLogInit(ident, OTBR_LOG_INFO, true, true);
+    otbrLog(OTBR_LOG_INFO, OTBR_LOG_TAG, "cool-equal");
+    otbrLogDeinit();
+    sleep(0);
+
+    char cmd[128];
+    snprintf(cmd, sizeof(cmd), "grep '%s.*cool-equal' /var/log/syslog", ident);
+    printf("CMD = %s\n", cmd);
+    CHECK(0 != system(cmd));
+}
+
 TEST(Logging, TestLoggingLowerLevel)
 {
     char ident[20];
     char cmd[128];
 
     snprintf(ident, sizeof(ident), "otbr-test-%ld", clock());
-    otbrLogInit(ident, OTBR_LOG_INFO, true);
+    otbrLogInit(ident, OTBR_LOG_INFO, true, false);
     otbrLog(OTBR_LOG_WARNING, OTBR_LOG_TAG, "cool-lower");
     otbrLogDeinit();
     sleep(0);
@@ -90,7 +106,7 @@
     char cmd[128];
 
     snprintf(ident, sizeof(ident), "otbr-test-%ld", clock());
-    otbrLogInit(ident, OTBR_LOG_DEBUG, true);
+    otbrLogInit(ident, OTBR_LOG_DEBUG, true, false);
     const char s[] = "one super long string with lots of text";
     otbrDump(OTBR_LOG_INFO, "Test", "foobar", s, sizeof(s));
     otbrLogDeinit();
diff --git a/third_party/openthread/mbedtls-config.h b/third_party/openthread/mbedtls-config.h
index 33b9f68..7646a77 100644
--- a/third_party/openthread/mbedtls-config.h
+++ b/third_party/openthread/mbedtls-config.h
@@ -52,6 +52,7 @@
 #define MBEDTLS_ASN1_PARSE_C
 #define MBEDTLS_ASN1_WRITE_C
 #define MBEDTLS_BIGNUM_C
+#define MBEDTLS_CAN_ECDH
 #define MBEDTLS_CCM_C
 #define MBEDTLS_CIPHER_C
 #define MBEDTLS_CTR_DRBG_C
@@ -63,6 +64,7 @@
 #define MBEDTLS_MD_C
 #define MBEDTLS_OID_C
 #define MBEDTLS_PK_C
+#define MBEDTLS_PK_HAVE_ECC_KEYS
 #define MBEDTLS_PK_PARSE_C
 #define MBEDTLS_SHA256_C
 #define MBEDTLS_SHA256_SMALLER
@@ -82,6 +84,7 @@
 #define MBEDTLS_ECDSA_DETERMINISTIC
 #define MBEDTLS_OID_C
 #define MBEDTLS_PEM_PARSE_C
+#define MBEDTLS_PK_CAN_ECDSA_SIGN
 #define MBEDTLS_PK_WRITE_C
 
 #define MBEDTLS_X509_USE_C