Merge remote-tracking branch 'upstream/main' into aosp ot-br-posix am: 907e336fb8 am: 6820911863

Original change: https://android-review.googlesource.com/c/platform/external/ot-br-posix/+/2775733

Change-Id: If15eccbb7645810d0ad6e7e86e685ed52cacb4ff
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/.github/workflows/border_router.yml b/.github/workflows/border_router.yml
index 9e754e1..591853c 100644
--- a/.github/workflows/border_router.yml
+++ b/.github/workflows/border_router.yml
@@ -99,7 +99,7 @@
             cert_scripts: ./tests/scripts/thread-cert/backbone/*.py
             packet_verification: 1
           - name: "Border Router TREL with FEATURE_FLAG (avahi)"
-            otbr_options: "-DOT_DUA=ON -DOT_ECDSA=ON -DOT_MLR=ON -DOT_SERVICE=ON -DOT_SRP_SERVER=ON -DOTBR_COVERAGE=ON -DOTBR_DUA_ROUTING=ON -DOTBR_FEATURE_FLAGS=ON -DOTBR_TREL=ON -DOTBR_DNS_UPSTREAM_QUERY=ON"
+            otbr_options: "-DOT_DUA=ON -DOT_ECDSA=ON -DOT_MLR=ON -DOT_SERVICE=ON -DOT_SRP_SERVER=ON -DOTBR_COVERAGE=ON -DOTBR_DUA_ROUTING=ON -DOTBR_FEATURE_FLAGS=ON -DOTBR_TELEMETRY_DATA_API=ON -DOTBR_TREL=ON -DOTBR_DNS_UPSTREAM_QUERY=ON"
             border_routing: 1
             nat64: 0
             otbr_mdns: "avahi"
@@ -122,7 +122,7 @@
       NAT64: ${{ matrix.nat64 }}
       MAX_JOBS: 3
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Get Border Router Test ID
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 195c26e..01e02e4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -45,7 +45,7 @@
   pretty:
     runs-on: ubuntu-22.04
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Bootstrap
       run: BUILD_TARGET=pretty-check tests/scripts/bootstrap.sh
     - name: Check
@@ -65,7 +65,7 @@
       OTBR_OPTIONS: "-DOTBR_SRP_ADVERTISING_PROXY=ON -DOTBR_BORDER_ROUTING=ON -DOTBR_NAT64=1 -DOTBR_SRP_SERVER_AUTO_ENABLE=OFF"
       OTBR_COVERAGE: 1
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
@@ -88,7 +88,7 @@
       OTBR_OPTIONS: "-DOTBR_SRP_ADVERTISING_PROXY=ON -DOTBR_DNSSD_DISCOVERY_PROXY=ON"
       OTBR_COVERAGE: 1
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
@@ -104,7 +104,7 @@
       BUILD_TARGET: script-check
       OTBR_COVERAGE: 1
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
@@ -121,7 +121,7 @@
       CC: clang
       CXX: clang++
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
@@ -134,7 +134,7 @@
     env:
       BUILD_TARGET: package
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
@@ -151,7 +151,7 @@
     env:
       BUILD_TARGET: check
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 9c794b1..bac8c4c 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -53,7 +53,7 @@
       OTBR_COVERAGE: 1
       VERBOSE: 1
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
@@ -106,7 +106,7 @@
             platforms: "linux/amd64,linux/arm/v7,linux/arm64"
             push: no
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
 
@@ -130,12 +130,12 @@
           ${TAGS} --file etc/docker/Dockerfile ." >> $GITHUB_OUTPUT
 
     - name: Set up QEMU
-      uses: docker/setup-qemu-action@v2
+      uses: docker/setup-qemu-action@v3
       with:
         platforms: all
 
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v2
+      uses: docker/setup-buildx-action@v3
 
     - name: Docker Buildx (build)
       run: |
@@ -143,7 +143,7 @@
 
     - name: Login to DockerHub
       if: success() && github.repository == 'openthread/ot-br-posix' && github.event_name != 'pull_request' && matrix.push
-      uses: docker/login-action@v2
+      uses: docker/login-action@v3
       with:
         username: ${{ secrets.DOCKER_USERNAME }}
         password: ${{ secrets.DOCKER_PASSWORD }}
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 93065f4..639ba7b 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -40,14 +40,16 @@
 jobs:
   doxygen:
     runs-on: ubuntu-latest
+    env:
+      BUILD_TARGET: check
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
       run: |
-        sudo apt-get update
-        sudo apt-get install -y doxygen libdbus-1-dev libglib2.0-dev-bin xmlto
+        tests/scripts/bootstrap.sh
+        sudo apt-get install -y libglib2.0-dev-bin xmlto
     - name: Generate
       run: |
         mkdir build-doc
diff --git a/.github/workflows/macOS.yml b/.github/workflows/macOS.yml
index 7e473ac..46f9893 100644
--- a/.github/workflows/macOS.yml
+++ b/.github/workflows/macOS.yml
@@ -44,7 +44,7 @@
   build-check:
     runs-on: macos-12
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
@@ -60,7 +60,7 @@
         rm -f /usr/local/bin/python3-config
         rm -f /usr/local/bin/python3.11-config
         brew update
-        brew reinstall boost cmake cpputest dbus jsoncpp ninja protobuf pkg-config
+        brew reinstall boost cmake cpputest dbus jsoncpp ninja protobuf@21 pkg-config
     - name: Build
       run: |
         OTBR_OPTIONS='-DOTBR_BORDER_AGENT=OFF -DOTBR_MDNS=OFF -DOT_FIREWALL=OFF -DOTBR_DBUS=OFF' ./script/test build
diff --git a/.github/workflows/meshcop.yml b/.github/workflows/meshcop.yml
index 5837f4f..e3aaae5 100644
--- a/.github/workflows/meshcop.yml
+++ b/.github/workflows/meshcop.yml
@@ -49,7 +49,7 @@
       matrix:
         mdns: ["avahi"]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
diff --git a/.github/workflows/openwrt.yml b/.github/workflows/openwrt.yml
index 273142a..04eaa6f 100644
--- a/.github/workflows/openwrt.yml
+++ b/.github/workflows/openwrt.yml
@@ -44,7 +44,7 @@
   openwrt:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - name: Bootstrap
diff --git a/.github/workflows/raspbian.yml b/.github/workflows/raspbian.yml
index c878aaa..47a1490 100644
--- a/.github/workflows/raspbian.yml
+++ b/.github/workflows/raspbian.yml
@@ -48,7 +48,7 @@
       IMAGE_URL: https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-01-12/2021-01-11-raspios-buster-armhf-lite.zip
       BUILD_TARGET: raspbian-gcc
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: recursive
     - name: Bootstrap
diff --git a/CMakeLists.txt b/CMakeLists.txt
index df7c230..4fefd27 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -57,6 +57,7 @@
 endif()
 
 set(CMAKE_CXX_EXTENSIONS OFF)
+set(CMAKE_EXE_LINKER_FLAGS "-rdynamic ${CMAKE_EXE_LINKER_FLAGS}")
 
 if (OTBR_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
     message(STATUS "Coverage: ON")
diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake
index e85e99f..a8d3905 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -63,6 +63,11 @@
     target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_FEATURE_FLAGS=1)
 endif()
 
+option(OTBR_TELEMETRY_DATA_API "Enable telemetry data API support" OFF)
+if (OTBR_TELEMETRY_DATA_API)
+    target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_TELEMETRY_DATA_API=1)
+endif()
+
 option(OTBR_DUA_ROUTING "Enable Backbone Router DUA Routing" OFF)
 if (OTBR_DUA_ROUTING)
     target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_DUA_ROUTING=1)
@@ -114,6 +119,8 @@
 option(OTBR_NAT64 "Enable NAT64 support" OFF)
 if(OTBR_NAT64)
     target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_NAT64=1)
+else()
+    target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_NAT64=0)
 endif()
 
 option(OTBR_VENDOR_INFRA_LINK_SELECT "Enable Vendor-specific infrastructure link selection rules" OFF)
@@ -132,3 +139,15 @@
 if (OTBR_PUBLISH_MESHCOP_BA_ID)
     target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_PUBLISH_MESHCOP_BA_ID=1)
 endif()
+
+option(OTBR_DHCP6_PD "Prefix delegation support" OFF)
+if (OTBR_DHCP6_PD)
+    target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_DHCP6_PD=1)
+else()
+    target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_DHCP6_PD=0)
+endif()
+
+option(OTBR_VENDOR_SERVER "Enable vendor server" OFF)
+if (OTBR_VENDOR_SERVER)
+    target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_VENDOR_SERVER=1)
+endif()
diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile
index 193716b..eef7a94 100644
--- a/etc/docker/Dockerfile
+++ b/etc/docker/Dockerfile
@@ -42,6 +42,7 @@
 ARG REST_API
 ARG WEB_GUI
 ARG MDNS
+ARG FIREWALL
 
 ENV INFRA_IF_NAME=${INFRA_IF_NAME:-eth0}
 ENV BORDER_ROUTING=${BORDER_ROUTING:-1}
@@ -59,6 +60,7 @@
 ENV DNS64=${DNS64:-0}
 ENV WEB_GUI=${WEB_GUI:-1}
 ENV REST_API=${REST_API:-1}
+ENV FIREWALL=${FIREWALL:-1}
 ENV DOCKER 1
 
 RUN env
@@ -88,6 +90,7 @@
   && ln -fs /usr/share/zoneinfo/UTC /etc/localtime
 
 COPY ./script /app/script
+COPY ./third_party/mDNSResponder /app/third_party/mDNSResponder
 WORKDIR /app
 
 RUN ./script/bootstrap
@@ -106,6 +109,7 @@
     && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $OTBR_BUILD_DEPS  \
     && ([ "${RELEASE}" = 1 ] ||  apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false "$OTBR_NORELEASE_DEPS";) \
     && rm -rf /var/lib/apt/lists/* \
+    && rm -rf /tmp/* \
   ))
 
 ENTRYPOINT ["/app/etc/docker/docker_entrypoint.sh"]
diff --git a/etc/openwrt/openthread-br/Makefile b/etc/openwrt/openthread-br/Makefile
index 1a328c0..cf0fdfb 100644
--- a/etc/openwrt/openthread-br/Makefile
+++ b/etc/openwrt/openthread-br/Makefile
@@ -51,7 +51,9 @@
 	-DOTBR_SRP_ADVERTISING_PROXY=ON \
 	-DOT_FIREWALL=ON \
 	-DOT_POSIX_SETTINGS_PATH=\"/etc/openthread\" \
-	-DOT_READLINE=OFF
+	-DOT_READLINE=OFF \
+	-DOTBR_NAT64=ON \
+	-DNAT64_SERVICE=\"openthread\"
 
 TARGET_CFLAGS += -DOPENTHREAD_POSIX_CONFIG_DAEMON_SOCKET_BASENAME=\\\"/var/run/openthread-%s\\\"
 
diff --git a/script/_firewall b/script/_firewall
index 0a29c36..28344d2 100755
--- a/script/_firewall
+++ b/script/_firewall
@@ -31,8 +31,12 @@
 
 sudo modprobe ip6table_filter || true
 
+FIREWALL="${FIREWALL:-1}"
+
 firewall_uninstall()
 {
+    with FIREWALL || return 0
+
     firewall_stop
     if have systemctl; then
         sudo systemctl disable otbr-firewall || true
@@ -46,6 +50,8 @@
 
 firewall_install()
 {
+    with FIREWALL || return 0
+
     sudo cp script/otbr-firewall $FIREWALL_SERVICE
     sudo chmod a+x $FIREWALL_SERVICE
     if have systemctl; then
@@ -56,6 +62,8 @@
 
 firewall_start()
 {
+    with FIREWALL || return 0
+
     if with DOCKER; then
         service otbr-firewall start || die 'Failed to start firewall service'
     elif have systemctl; then
@@ -65,6 +73,8 @@
 
 firewall_stop()
 {
+    with FIREWALL || return 0
+
     if with DOCKER; then
         service otbr-firewall stop || true
     elif have systemctl; then
diff --git a/script/_nat64 b/script/_nat64
index 9268134..521c4d2 100644
--- a/script/_nat64
+++ b/script/_nat64
@@ -40,6 +40,7 @@
 DYNAMIC_POOL="${NAT64_DYNAMIC_POOL:-192.168.255.0/24}"
 NAT44_SERVICE=/etc/init.d/otbr-nat44
 WLAN_IFNAMES="${INFRA_IF_NAME:-eth0}"
+THREAD_IF="${THREAD_IF:-wpan0}"
 
 # Currently solution was verified only on raspbian and ubuntu.
 #
@@ -156,7 +157,9 @@
             echo "        iptables -t nat -A POSTROUTING -o $IFNAME -j MASQUERADE" | sudo tee -a $NAT44_SERVICE
         done
     else
-        echo "        iptables -t nat -A POSTROUTING -s \"$DYNAMIC_POOL\" -j MASQUERADE" | sudo tee -a $NAT44_SERVICE
+        # Just a random fwmark bits.
+        echo "        iptables -t mangle -A PREROUTING -i $THREAD_IF -j MARK --set-mark 0x1001" | sudo tee -a $NAT44_SERVICE
+        echo "        iptables -t nat -A POSTROUTING -m mark --mark 0x1001 -j MASQUERADE" | sudo tee -a $NAT44_SERVICE
         for IFNAME in $WLAN_IFNAMES; do
             echo "        iptables -t filter -A FORWARD -o $IFNAME -j ACCEPT" | sudo tee -a $NAT44_SERVICE
             echo "        iptables -t filter -A FORWARD -i $IFNAME -j ACCEPT" | sudo tee -a $NAT44_SERVICE
diff --git a/script/_otbr b/script/_otbr
index 790ab10..dbccc73 100644
--- a/script/_otbr
+++ b/script/_otbr
@@ -128,6 +128,16 @@
         )
     fi
 
+    if with FIREWALL; then
+        otbr_options+=(
+            "-DOT_FIREWALL=ON"
+        )
+    else
+        otbr_options+=(
+            "-DOT_FIREWALL=OFF"
+        )
+    fi
+
     (./script/cmake-build "${otbr_options[@]}" \
         && cd "${OTBR_TOP_BUILDDIR}" \
         && ninja \
diff --git a/script/bootstrap b/script/bootstrap
index 806cc1c..67d194b 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -35,6 +35,8 @@
 
 NAT64_SERVICE="${NAT64_SERVICE:-openthread}"
 
+FIREWALL="${FIREWALL:-1}"
+
 install_packages_apt()
 {
     sudo apt-get update
@@ -56,16 +58,20 @@
 
     # mDNS
     sudo apt-get install --no-install-recommends -y libavahi-client3 libavahi-common-dev libavahi-client-dev avahi-daemon
-    (MDNS_RESPONDER_SOURCE_NAME=mDNSResponder-1310.80.1 \
+    (MDNS_RESPONDER_SOURCE_NAME=mDNSResponder-1790.80.10 \
+        && MDNS_RESPONDER_PATCH_PATH=$(realpath "$(dirname "$0")"/../third_party/mDNSResponder) \
         && cd /tmp \
         && wget --no-check-certificate https://github.com/apple-oss-distributions/mDNSResponder/archive/refs/tags/$MDNS_RESPONDER_SOURCE_NAME.tar.gz \
         && mkdir -p $MDNS_RESPONDER_SOURCE_NAME \
         && tar xvf $MDNS_RESPONDER_SOURCE_NAME.tar.gz -C $MDNS_RESPONDER_SOURCE_NAME --strip-components=1 \
-        && cd /tmp/$MDNS_RESPONDER_SOURCE_NAME/Clients \
-        && sed -i '/#include <ctype.h>/a #include <stdarg.h>' dns-sd.c \
-        && sed -i '/#include <ctype.h>/a #include <sys/param.h>' dns-sd.c \
-        && cd /tmp/$MDNS_RESPONDER_SOURCE_NAME/mDNSPosix \
-        && make os=linux && sudo make install os=linux)
+        && cd /tmp/"$MDNS_RESPONDER_SOURCE_NAME" \
+        && (
+            for patch in "$MDNS_RESPONDER_PATCH_PATH"/*.patch; do
+                patch -p1 <"$patch"
+            done
+        ) \
+        && cd mDNSPosix \
+        && make os=linux tls=no && sudo make install os=linux tls=no)
 
     # Boost
     sudo apt-get install --no-install-recommends -y libboost-dev libboost-filesystem-dev libboost-system-dev
@@ -100,7 +106,7 @@
     sudo apt-get install --no-install-recommends -y libjsoncpp-dev
 
     # reference device
-    without REFERENCE_DEVICE || sudo apt-get install --no-install-recommends -y radvd dnsutils
+    without REFERENCE_DEVICE || sudo apt-get install --no-install-recommends -y radvd dnsutils avahi-utils
 
     # backbone-router
     without BACKBONE_ROUTER || sudo apt-get install --no-install-recommends -y libnetfilter-queue1 libnetfilter-queue-dev
diff --git a/script/test b/script/test
index 13a7c5e..f5b955c 100755
--- a/script/test
+++ b/script/test
@@ -131,6 +131,7 @@
         "-DOT_THREAD_VERSION=1.3"
         "-DOTBR_DBUS=ON"
         "-DOTBR_FEATURE_FLAGS=ON"
+        "-DOTBR_TELEMETRY_DATA_API=ON"
         "-DOTBR_WEB=ON"
         "-DOTBR_UNSECURE_JOIN=ON"
         "-DOTBR_TREL=ON"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ba8f05d..15790a4 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -31,7 +31,7 @@
     add_subdirectory(border_agent)
 endif()
 add_subdirectory(common)
-if(OTBR_FEATURE_FLAGS)
+if(OTBR_DBUS OR OTBR_FEATURE_FLAGS OR OTBR_TELEMETRY_DATA_API)
     add_subdirectory(proto)
 endif()
 add_subdirectory(ncp)
diff --git a/src/agent/CMakeLists.txt b/src/agent/CMakeLists.txt
index 9a8b710..60d19b7 100644
--- a/src/agent/CMakeLists.txt
+++ b/src/agent/CMakeLists.txt
@@ -68,9 +68,9 @@
 set(OTBR_AGENT_GROUP "root" CACHE STRING "set the group using otbr-agent client")
 
 if(OTBR_MDNS STREQUAL "mDNSResponder")
-    set(EXEC_START_PRE "ExecStartPre=service mdns start\n")
+    set(EXEC_START_PRE "ExecStartPre=/usr/sbin/service mdns start\n")
 elseif(OTBR_MDNS STREQUAL "avahi")
-    set(EXEC_START_PRE "ExecStartPre=service avahi-daemon start\n")
+    set(EXEC_START_PRE "ExecStartPre=/usr/sbin/service avahi-daemon start\n")
 else()
     message(WARNING "OTBR_MDNS=\"${OTBR_MDNS}\" is not supported")
 endif()
diff --git a/src/agent/application.cpp b/src/agent/application.cpp
index c9a5598..3369602 100644
--- a/src/agent/application.cpp
+++ b/src/agent/application.cpp
@@ -77,7 +77,7 @@
     , mDBusAgent(mNcp, mBorderAgent.GetPublisher())
 #endif
 #if OTBR_ENABLE_VENDOR_SERVER
-    , mVendorServer(vendor::VendorServer::newInstance(mNcp))
+    , mVendorServer(vendor::VendorServer::newInstance(*this))
 #endif
 {
     OTBR_UNUSED_VARIABLE(aRestListenAddress);
diff --git a/src/agent/application.hpp b/src/agent/application.hpp
index 487206e..55b39ee 100644
--- a/src/agent/application.hpp
+++ b/src/agent/application.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_APPLICATION_HPP_
 #define OTBR_AGENT_APPLICATION_HPP_
 
+#include "openthread-br/config.h"
+
 #include <atomic>
 #include <signal.h>
 #include <stdint.h>
@@ -62,6 +64,14 @@
 
 namespace otbr {
 
+#if OTBR_ENABLE_VENDOR_SERVER
+namespace vendor {
+
+class VendorServer;
+
+}
+#endif
+
 /**
  * @addtogroup border-router-agent
  *
@@ -117,6 +127,73 @@
      */
     otbrError Run(void);
 
+    /**
+     * Get the OpenThread controller object the application is using.
+     *
+     * @returns The OpenThread controller object.
+     */
+    Ncp::ControllerOpenThread &GetNcp(void) { return mNcp; }
+
+#if OTBR_ENABLE_BORDER_AGENT
+    /**
+     * Get the border agent the application is using.
+     *
+     * @returns The border agent.
+     */
+    BorderAgent &GetBorderAgent(void)
+    {
+        return mBorderAgent;
+    }
+#endif
+
+#if OTBR_ENABLE_BACKBONE_ROUTER
+    /**
+     * Get the backbone agent the application is using.
+     *
+     * @returns The backbone agent.
+     */
+    BackboneRouter::BackboneAgent &GetBackboneAgent(void)
+    {
+        return mBackboneAgent;
+    }
+#endif
+
+#if OTBR_ENABLE_OPENWRT
+    /**
+     * Get the UBus agent the application is using.
+     *
+     * @returns The UBus agent.
+     */
+    ubus::UBusAgent &GetUBusAgent(void)
+    {
+        return mUbusAgent;
+    }
+#endif
+
+#if OTBR_ENABLE_REST_SERVER
+    /**
+     * Get the rest web server the application is using.
+     *
+     * @returns The rest web server.
+     */
+    rest::RestWebServer &GetRestWebServer(void)
+    {
+        return mRestWebServer;
+    }
+#endif
+
+#if OTBR_ENABLE_DBUS_SERVER
+    /**
+     * Get the DBus agent the application is using.
+     *
+     * @returns The DBus agent.
+     */
+    DBus::DBusAgent &GetDBusAgent(void)
+    {
+        return mDBusAgent;
+    }
+#endif
+
 private:
     // Default poll timeout.
     static const struct timeval kPollTimeout;
diff --git a/src/agent/vendor.hpp b/src/agent/vendor.hpp
index d99b87a..8e2ecb3 100644
--- a/src/agent/vendor.hpp
+++ b/src/agent/vendor.hpp
@@ -34,9 +34,14 @@
 #ifndef OTBR_AGENT_VENDOR_HPP_
 #define OTBR_AGENT_VENDOR_HPP_
 
-#include "ncp/ncp_openthread.hpp"
+#include "openthread-br/config.h"
+
+#include "agent/application.hpp"
 
 namespace otbr {
+
+class Application;
+
 namespace vendor {
 
 /**
@@ -52,11 +57,11 @@
      *
      * Custom vendor servers should implement this method to return an object of the derived class.
      *
-     * @param[in]  aNcp  The OpenThread controller object.
+     * @param[in]  aApplication  The OTBR application.
      *
      * @returns  New derived VendorServer instance.
      */
-    static std::shared_ptr<VendorServer> newInstance(otbr::Ncp::ControllerOpenThread &aNcp);
+    static std::shared_ptr<VendorServer> newInstance(Application &aApplication);
 
     /**
      * Initializes the vendor server.
diff --git a/src/backbone_router/backbone_agent.hpp b/src/backbone_router/backbone_agent.hpp
index 6ec62c7..0ff7808 100644
--- a/src/backbone_router/backbone_agent.hpp
+++ b/src/backbone_router/backbone_agent.hpp
@@ -34,6 +34,8 @@
 #ifndef BACKBONE_ROUTER_BACKBONE_AGENT_HPP_
 #define BACKBONE_ROUTER_BACKBONE_AGENT_HPP_
 
+#include "openthread-br/config.h"
+
 #if OTBR_ENABLE_BACKBONE_ROUTER
 
 #include <openthread/backbone_router_ftd.h>
diff --git a/src/backbone_router/constants.hpp b/src/backbone_router/constants.hpp
index c853e12..363e34c 100644
--- a/src/backbone_router/constants.hpp
+++ b/src/backbone_router/constants.hpp
@@ -34,6 +34,8 @@
 #ifndef BACKBONE_CONSTANTS_HPP_
 #define BACKBONE_CONSTANTS_HPP_
 
+#include "openthread-br/config.h"
+
 namespace otbr {
 namespace BackboneRouter {
 
diff --git a/src/backbone_router/dua_routing_manager.hpp b/src/backbone_router/dua_routing_manager.hpp
index 97220ca..daf341d 100644
--- a/src/backbone_router/dua_routing_manager.hpp
+++ b/src/backbone_router/dua_routing_manager.hpp
@@ -34,6 +34,8 @@
 #ifndef BACKBONE_ROUTER_DUA_ROUTING_MANAGER
 #define BACKBONE_ROUTER_DUA_ROUTING_MANAGER
 
+#include "openthread-br/config.h"
+
 #if OTBR_ENABLE_DUA_ROUTING
 
 #include <set>
diff --git a/src/backbone_router/nd_proxy.hpp b/src/backbone_router/nd_proxy.hpp
index 48a820c..3a7a2d2 100644
--- a/src/backbone_router/nd_proxy.hpp
+++ b/src/backbone_router/nd_proxy.hpp
@@ -34,6 +34,8 @@
 #ifndef ND_PROXY_HPP_
 #define ND_PROXY_HPP_
 
+#include "openthread-br/config.h"
+
 #if OTBR_ENABLE_DUA_ROUTING
 
 #ifdef __APPLE__
diff --git a/src/border_agent/border_agent.cpp b/src/border_agent/border_agent.cpp
index 1303687..78053b1 100644
--- a/src/border_agent/border_agent.cpp
+++ b/src/border_agent/border_agent.cpp
@@ -343,10 +343,11 @@
 
         for (auto &addedEntry : aTxtList)
         {
-            if (addedEntry.mName == key)
+            if (addedEntry.mKey == key)
             {
-                addedEntry.mValue = value;
-                found             = true;
+                addedEntry.mValue              = value;
+                addedEntry.mIsBooleanAttribute = false;
+                found                          = true;
                 break;
             }
         }
@@ -367,27 +368,23 @@
     const otExtAddress      *extAddr     = otLinkGetExtendedAddress(instance);
     const char              *networkName = otThreadGetNetworkName(instance);
     Mdns::Publisher::TxtList txtList{{"rv", "1"}};
+    Mdns::Publisher::TxtData txtData;
     int                      port;
+    otbrError                error;
+
+    OTBR_UNUSED_VARIABLE(error);
 
     otbrLogInfo("Publish meshcop service %s.%s.local.", mServiceInstanceName.c_str(), kBorderAgentServiceType);
 
 #if OTBR_ENABLE_PUBLISH_MESHCOP_BA_ID
     {
-        otError  error;
-        uint8_t  id[OT_BORDER_AGENT_ID_LENGTH];
-        uint16_t idLength = OT_BORDER_AGENT_ID_LENGTH;
+        otError         error;
+        otBorderAgentId id;
 
-        error = otBorderAgentGetId(instance, id, &idLength);
+        error = otBorderAgentGetId(instance, &id);
         if (error == OT_ERROR_NONE)
         {
-            if (idLength == OT_BORDER_AGENT_ID_LENGTH)
-            {
-                txtList.emplace_back("id", id, idLength);
-            }
-            else
-            {
-                otbrLogWarning("Border Agent ID length is %d, but expect %d", idLength, OT_BORDER_AGENT_ID_LENGTH);
-            }
+            txtList.emplace_back("id", id.mId, sizeof(id));
         }
         else
         {
@@ -441,8 +438,11 @@
         port = kBorderAgentServiceDummyPort;
     }
 
+    error = Mdns::Publisher::EncodeTxtData(txtList, txtData);
+    assert(error == OTBR_ERROR_NONE);
+
     mPublisher->PublishService(/* aHostName */ "", mServiceInstanceName, kBorderAgentServiceType,
-                               Mdns::Publisher::SubTypeList{}, port, txtList, [this](otbrError aError) {
+                               Mdns::Publisher::SubTypeList{}, port, txtData, [this](otbrError aError) {
                                    if (aError == OTBR_ERROR_ABORTED)
                                    {
                                        // OTBR_ERROR_ABORTED is thrown when an ongoing service registration is
diff --git a/src/border_agent/border_agent.hpp b/src/border_agent/border_agent.hpp
index 896281e..0274ae1 100644
--- a/src/border_agent/border_agent.hpp
+++ b/src/border_agent/border_agent.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_BORDER_AGENT_HPP_
 #define OTBR_AGENT_BORDER_AGENT_HPP_
 
+#include "openthread-br/config.h"
+
 #if !(OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO)
 #error "Border Agent feature requires at least one `OTBR_MDNS` implementation"
 #endif
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 1f3aec7..5badd6f 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -27,7 +27,9 @@
 #
 
 add_library(otbr-common
+    api_strings.cpp
     byteswap.hpp
+    code_utils.cpp
     code_utils.hpp
     dns_utils.cpp
     logging.cpp
@@ -48,4 +50,6 @@
     PUBLIC otbr-config
     openthread-ftd
     openthread-posix
+    $<$<BOOL:${OTBR_FEATURE_FLAGS}>:otbr-proto>
+    $<$<BOOL:${OTBR_TELEMETRY_DATA_API}>:otbr-proto>
 )
diff --git a/src/common/api_strings.cpp b/src/common/api_strings.cpp
new file mode 100644
index 0000000..fcb429f
--- /dev/null
+++ b/src/common/api_strings.cpp
@@ -0,0 +1,55 @@
+/*
+ *    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 "common/api_strings.hpp"
+
+std::string GetDeviceRoleName(otDeviceRole aRole)
+{
+    std::string roleName;
+
+    switch (aRole)
+    {
+    case OT_DEVICE_ROLE_DISABLED:
+        roleName = OTBR_ROLE_NAME_DISABLED;
+        break;
+    case OT_DEVICE_ROLE_DETACHED:
+        roleName = OTBR_ROLE_NAME_DETACHED;
+        break;
+    case OT_DEVICE_ROLE_CHILD:
+        roleName = OTBR_ROLE_NAME_CHILD;
+        break;
+    case OT_DEVICE_ROLE_ROUTER:
+        roleName = OTBR_ROLE_NAME_ROUTER;
+        break;
+    case OT_DEVICE_ROLE_LEADER:
+        roleName = OTBR_ROLE_NAME_LEADER;
+        break;
+    }
+
+    return roleName;
+}
diff --git a/src/common/api_strings.hpp b/src/common/api_strings.hpp
new file mode 100644
index 0000000..8b1eb9f
--- /dev/null
+++ b/src/common/api_strings.hpp
@@ -0,0 +1,52 @@
+/*
+ *    Copyright (c) 2023, The OpenThread Authors.
+ *    All rights reserved.
+ *
+ *    Redistribution and use in source and binary forms, with or without
+ *    modification, are permitted provided that the following conditions are met:
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *    3. Neither the name of the copyright holder nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ *    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *    POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ * This file has helper functions to convert internal state representations
+ * to string (useful for APIs).
+ *
+ */
+#ifndef OTBR_COMMON_API_STRINGS_HPP_
+#define OTBR_COMMON_API_STRINGS_HPP_
+
+#include "openthread-br/config.h"
+
+#include <string>
+
+#include <openthread/thread.h>
+
+#define OTBR_ROLE_NAME_DISABLED "disabled"
+#define OTBR_ROLE_NAME_DETACHED "detached"
+#define OTBR_ROLE_NAME_CHILD "child"
+#define OTBR_ROLE_NAME_ROUTER "router"
+#define OTBR_ROLE_NAME_LEADER "leader"
+
+std::string GetDeviceRoleName(otDeviceRole aRole);
+
+#endif // OTBR_COMMON_API_STRINGS_HPP_
diff --git a/src/common/byteswap.hpp b/src/common/byteswap.hpp
index 7c3485c..8c59abc 100644
--- a/src/common/byteswap.hpp
+++ b/src/common/byteswap.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_COMMON_BYTESWAP_HPP_
 #define OTBR_COMMON_BYTESWAP_HPP_
 
+#include "openthread-br/config.h"
+
 #if __APPLE__
 #include <libkern/OSByteOrder.h>
 #define bswap_16 OSSwapInt16
diff --git a/src/common/callback.hpp b/src/common/callback.hpp
index 45bad8f..7df19e5 100644
--- a/src/common/callback.hpp
+++ b/src/common/callback.hpp
@@ -29,6 +29,8 @@
 #ifndef OTBR_COMMON_CALLBACK_HPP_
 #define OTBR_COMMON_CALLBACK_HPP_
 
+#include "openthread-br/config.h"
+
 #include <functional>
 #include <type_traits>
 
@@ -61,15 +63,27 @@
     // compiling issue which trying to instantiate this template constructor
     // for use cases like `::mOnceCallback(aOnceCallback)`.
     template <typename T, typename = typename std::enable_if<!std::is_same<OnceCallback, T>::value>::type>
-    OnceCallback(T &&func)
-        : mFunc(std::forward<T>(func))
+    OnceCallback(T &&aFunc)
+        : mFunc(std::forward<T>(aFunc))
     {
     }
 
+    OnceCallback(OnceCallback &&aCallback)
+        : mFunc(std::move(aCallback.mFunc))
+    {
+        aCallback.mFunc = nullptr;
+    }
+
+    OnceCallback &operator=(OnceCallback &&aCallback)
+    {
+        mFunc           = std::move(aCallback.mFunc);
+        aCallback.mFunc = nullptr;
+
+        return *this;
+    }
+
     OnceCallback(const OnceCallback &)            = delete;
     OnceCallback &operator=(const OnceCallback &) = delete;
-    OnceCallback(OnceCallback &&)                 = default;
-    OnceCallback &operator=(OnceCallback &&)      = default;
 
     R operator()(Args...) const &
     {
@@ -77,13 +91,13 @@
                                       "rvalue, i.e. std::move(callback)().");
     }
 
-    R operator()(Args... args) &&
+    R operator()(Args... aArgs) &&
     {
         // Move `this` to a local variable to clear internal state
         // before invoking the callback function.
         OnceCallback cb = std::move(*this);
 
-        return cb.mFunc(std::forward<Args>(args)...);
+        return cb.mFunc(std::forward<Args>(aArgs)...);
     }
 
     bool IsNull() const { return mFunc == nullptr; }
diff --git a/src/common/code_utils.cpp b/src/common/code_utils.cpp
new file mode 100644
index 0000000..35b8a25
--- /dev/null
+++ b/src/common/code_utils.cpp
@@ -0,0 +1,40 @@
+/*
+ *    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 "common/code_utils.hpp"
+
+uint64_t ConvertOpenThreadUint64(const uint8_t *aValue)
+{
+    uint64_t val = 0;
+
+    for (size_t i = 0; i < sizeof(uint64_t); i++)
+    {
+        val = (val << 8) | aValue[i];
+    }
+    return val;
+}
diff --git a/src/common/code_utils.hpp b/src/common/code_utils.hpp
index ec89113..87791a2 100644
--- a/src/common/code_utils.hpp
+++ b/src/common/code_utils.hpp
@@ -33,6 +33,8 @@
 #ifndef OTBR_COMMON_CODE_UTILS_HPP_
 #define OTBR_COMMON_CODE_UTILS_HPP_
 
+#include "openthread-br/config.h"
+
 #ifndef OTBR_LOG_TAG
 #define OTBR_LOG_TAG "UTILS"
 #endif
@@ -56,6 +58,10 @@
     reinterpret_cast<aAlignType>(      \
         ((reinterpret_cast<unsigned long>(aMem) + sizeof(aAlignType) - 1) / sizeof(aAlignType)) * sizeof(aAlignType))
 
+// Allocate the structure using "raw" storage.
+#define OT_DEFINE_ALIGNED_VAR(name, size, align_type) \
+    align_type name[(((size) + (sizeof(align_type) - 1)) / sizeof(align_type))]
+
 #ifndef CONTAINING_RECORD
 #define BASE 0x1
 #define myoffsetof(s, m) (((size_t) & (((s *)BASE)->m)) - BASE)
@@ -164,6 +170,14 @@
 }
 
 /**
+ * This method converts 8 uint8_t bytes into uint64_t using big-endian.
+ *
+ * @param[in] aValue  The input 8 uint8_t bytes.
+ * @returns The converted uint64_t.
+ */
+uint64_t ConvertOpenThreadUint64(const uint8_t *aValue);
+
+/**
  * This class makes any class that derives from it non-copyable. It is intended to be used as a private base class.
  *
  */
diff --git a/src/common/dns_utils.hpp b/src/common/dns_utils.hpp
index 0b3ef5b..13d8e8c 100644
--- a/src/common/dns_utils.hpp
+++ b/src/common/dns_utils.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_COMMON_DNS_UTILS_HPP_
 #define OTBR_COMMON_DNS_UTILS_HPP_
 
+#include "openthread-br/config.h"
+
 #include "common/types.hpp"
 
 /**
diff --git a/src/dbus/client/client_error.hpp b/src/dbus/client/client_error.hpp
index ad033a2..7f499bd 100644
--- a/src/dbus/client/client_error.hpp
+++ b/src/dbus/client/client_error.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_CLIENT_CLIENT_ERROR_HPP_
 #define OTBR_DBUS_CLIENT_CLIENT_ERROR_HPP_
 
+#include "openthread-br/config.h"
+
 #include <string>
 
 #include <dbus/dbus.h>
diff --git a/src/dbus/client/thread_api_dbus.cpp b/src/dbus/client/thread_api_dbus.cpp
index 8e64d15..0a6c36e 100644
--- a/src/dbus/client/thread_api_dbus.cpp
+++ b/src/dbus/client/thread_api_dbus.cpp
@@ -29,6 +29,7 @@
 #include <map>
 #include <string.h>
 
+#include "common/api_strings.hpp"
 #include "common/code_utils.hpp"
 #include "dbus/client/client_error.hpp"
 #include "dbus/client/thread_api_dbus.hpp"
@@ -638,6 +639,11 @@
     return GetProperty(OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS, aDataset);
 }
 
+ClientError ThreadApiDBus::GetPendingDatasetTlvs(std::vector<uint8_t> &aDataset)
+{
+    return GetProperty(OTBR_DBUS_PROPERTY_PENDING_DATASET_TLVS, aDataset);
+}
+
 ClientError ThreadApiDBus::GetFeatureFlagListData(std::vector<uint8_t> &aFeatureFlagListData)
 {
     return GetProperty(OTBR_DBUS_PROPERTY_FEATURE_FLAG_LIST_DATA, aFeatureFlagListData);
@@ -685,6 +691,16 @@
 }
 #endif
 
+ClientError ThreadApiDBus::GetTelemetryData(std::vector<uint8_t> &aTelemetryData)
+{
+    return GetProperty(OTBR_DBUS_PROPERTY_TELEMETRY_DATA, aTelemetryData);
+}
+
+ClientError ThreadApiDBus::GetCapabilities(std::vector<uint8_t> &aCapabilities)
+{
+    return GetProperty(OTBR_DBUS_PROPERTY_CAPABILITIES, aCapabilities);
+}
+
 std::string ThreadApiDBus::GetInterfaceName(void)
 {
     return mInterfaceName;
diff --git a/src/dbus/client/thread_api_dbus.hpp b/src/dbus/client/thread_api_dbus.hpp
index 0a0d149..622faec 100644
--- a/src/dbus/client/thread_api_dbus.hpp
+++ b/src/dbus/client/thread_api_dbus.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_THREAD_API_DBUS_HPP_
 #define OTBR_THREAD_API_DBUS_HPP_
 
+#include "openthread-br/config.h"
+
 #include <functional>
 
 #include <dbus/dbus.h>
@@ -719,6 +721,18 @@
     ClientError GetActiveDatasetTlvs(std::vector<uint8_t> &aDataset);
 
     /**
+     * This method gets the pending operational dataset
+     *
+     * @param[out] aDataset  The pending operational dataset
+     *
+     * @retval ERROR_NONE  Successfully performed the dbus function call
+     * @retval ERROR_DBUS  dbus encode/decode error
+     * @retval ...         OpenThread defined error value otherwise
+     *
+     */
+    ClientError GetPendingDatasetTlvs(std::vector<uint8_t> &aDataset);
+
+    /**
      * This method gets the feature flag list proto serialized byte data.
      *
      * @param[out] aFeatureFlagListData  The feature flag list proto serialized
@@ -856,6 +870,32 @@
      */
     ClientError GetNat64ErrorCounters(Nat64ErrorCounters &aCounters);
 
+    /**
+     * This method gets the telemetry data proto serialized byte data.
+     *
+     * @param[out] aTelemetryData  The telemetry data proto serialized
+     *                             byte data (see proto/thread_telemetry.proto)
+     *
+     * @retval ERROR_NONE  Successfully performed the dbus function call
+     * @retval ERROR_DBUS  dbus encode/decode error
+     * @retval ...         OpenThread defined error value otherwise
+     *
+     */
+    ClientError GetTelemetryData(std::vector<uint8_t> &aTelemetryData);
+
+    /**
+     * This method gets the capabilities data proto serialized byte data.
+     *
+     * @param[out] aCapabilities The capabilities proto serialized byte data
+     *                           (see proto/capabilities.proto)
+     *
+     * @retval ERROR_NONE  Successfully performed the dbus function call
+     * @retval ERROR_DBUS  dbus encode/decode error
+     * @retval ...         OpenThread defined error value otherwise
+     *
+     */
+    ClientError GetCapabilities(std::vector<uint8_t> &aCapabilities);
+
 private:
     ClientError CallDBusMethodSync(const std::string &aMethodName);
     ClientError CallDBusMethodAsync(const std::string &aMethodName, DBusPendingCallNotifyFunction aFunction);
diff --git a/src/dbus/common/constants.hpp b/src/dbus/common/constants.hpp
index 1daee19..1d99f1d 100644
--- a/src/dbus/common/constants.hpp
+++ b/src/dbus/common/constants.hpp
@@ -93,6 +93,7 @@
 #define OTBR_DBUS_PROPERTY_EXTERNAL_ROUTES "ExternalRoutes"
 #define OTBR_DBUS_PROPERTY_ON_MESH_PREFIXES "OnMeshPrefixes"
 #define OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS "ActiveDatasetTlvs"
+#define OTBR_DBUS_PROPERTY_PENDING_DATASET_TLVS "PendingDatasetTlvs"
 #define OTBR_DBUS_PROPERTY_FEATURE_FLAG_LIST_DATA "FeatureFlagListData"
 #define OTBR_DBUS_PROPERTY_RADIO_REGION "RadioRegion"
 #define OTBR_DBUS_PROPERTY_SRP_SERVER_INFO "SrpServerInfo"
@@ -108,17 +109,14 @@
 #define OTBR_DBUS_PROPERTY_RADIO_COEX_METRICS "RadioCoexMetrics"
 #define OTBR_DBUS_PROPERTY_BORDER_ROUTING_COUNTERS "BorderRoutingCounters"
 #define OTBR_DBUS_PROPERTY_NAT64_STATE "Nat64State"
+#define OTBR_DBUS_PROPERTY_NAT64_CIDR "Nat64Cidr"
 #define OTBR_DBUS_PROPERTY_NAT64_MAPPINGS "Nat64Mappings"
 #define OTBR_DBUS_PROPERTY_NAT64_PROTOCOL_COUNTERS "Nat64ProtocolCounters"
 #define OTBR_DBUS_PROPERTY_NAT64_ERROR_COUNTERS "Nat64ErrorCounters"
 #define OTBR_DBUS_PROPERTY_INFRA_LINK_INFO "InfraLinkInfo"
 #define OTBR_DBUS_PROPERTY_DNS_UPSTREAM_QUERY_STATE "DnsUpstreamQueryState"
-
-#define OTBR_ROLE_NAME_DISABLED "disabled"
-#define OTBR_ROLE_NAME_DETACHED "detached"
-#define OTBR_ROLE_NAME_CHILD "child"
-#define OTBR_ROLE_NAME_ROUTER "router"
-#define OTBR_ROLE_NAME_LEADER "leader"
+#define OTBR_DBUS_PROPERTY_TELEMETRY_DATA "TelemetryData"
+#define OTBR_DBUS_PROPERTY_CAPABILITIES "Capabilities"
 
 #define OTBR_NAT64_STATE_NAME_DISABLED "disabled"
 #define OTBR_NAT64_STATE_NAME_NOT_RUNNING "not_running"
diff --git a/src/dbus/common/dbus_message_dump.hpp b/src/dbus/common/dbus_message_dump.hpp
index d4b462f..7715c8d 100644
--- a/src/dbus/common/dbus_message_dump.hpp
+++ b/src/dbus/common/dbus_message_dump.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_DBUS_MESSAGE_DUMP_HPP_
 #define OTBR_AGENT_DBUS_MESSAGE_DUMP_HPP_
 
+#include "openthread-br/config.h"
+
 #include <dbus/dbus.h>
 
 namespace otbr {
diff --git a/src/dbus/common/dbus_message_helper.hpp b/src/dbus/common/dbus_message_helper.hpp
index 40c6168..03c8f14 100644
--- a/src/dbus/common/dbus_message_helper.hpp
+++ b/src/dbus/common/dbus_message_helper.hpp
@@ -34,6 +34,8 @@
 #ifndef DBUS_MESSAGE_HELPER_HPP_
 #define DBUS_MESSAGE_HELPER_HPP_
 
+#include "openthread-br/config.h"
+
 #include <array>
 #include <string>
 #include <tuple>
@@ -212,8 +214,8 @@
 template <> struct DBusTypeTrait<ActiveScanResult>
 {
     // struct of { uint64, string, uint64, array<uint8>, uint16, uint16, uint8,
-    //             uint8, uint8, uint8, bool, bool }
-    static constexpr const char *TYPE_AS_STRING = "(tstayqqyyyybb)";
+    //             int16, uint8, uint8, bool, bool }
+    static constexpr const char *TYPE_AS_STRING = "(tstayqqynyybb)";
 };
 
 template <> struct DBusTypeTrait<EnergyScanResult>
diff --git a/src/dbus/common/dbus_message_helper_openthread.cpp b/src/dbus/common/dbus_message_helper_openthread.cpp
index beee83d..192279b 100644
--- a/src/dbus/common/dbus_message_helper_openthread.cpp
+++ b/src/dbus/common/dbus_message_helper_openthread.cpp
@@ -54,6 +54,10 @@
     DBusMessageIter sub;
     otbrError       error = OTBR_ERROR_NONE;
 
+    // Local variable to help convert an RSSI value.
+    // 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);
 
@@ -64,12 +68,16 @@
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mPanId));
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mJoinerUdpPort));
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mChannel));
-    SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mRssi));
+    SuccessOrExit(error = DBusMessageExtract<int16_t>(&sub, rssi));
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mLqi));
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mVersion));
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mIsNative));
     SuccessOrExit(error = DBusMessageExtract(&sub, aScanResult.mDiscover));
 
+    // Double check the value is within int8 bounds and cast back
+    VerifyOrExit((rssi <= INT8_MAX) && (rssi >= INT8_MIN), error = OTBR_ERROR_PARSE);
+    aScanResult.mRssi = static_cast<int8_t>(rssi);
+
     dbus_message_iter_next(aIter);
     error = OTBR_ERROR_NONE;
 exit:
@@ -89,7 +97,10 @@
     SuccessOrExit(error = DBusMessageEncode(&sub, aScanResult.mPanId));
     SuccessOrExit(error = DBusMessageEncode(&sub, aScanResult.mJoinerUdpPort));
     SuccessOrExit(error = DBusMessageEncode(&sub, aScanResult.mChannel));
-    SuccessOrExit(error = DBusMessageEncode(&sub, aScanResult.mRssi));
+
+    // Dbus doesn't have a signed byte, cast into an int16
+    SuccessOrExit(error = DBusMessageEncode(&sub, static_cast<int16_t>(aScanResult.mRssi)));
+
     SuccessOrExit(error = DBusMessageEncode(&sub, aScanResult.mLqi));
     SuccessOrExit(error = DBusMessageEncode(&sub, aScanResult.mVersion));
     SuccessOrExit(error = DBusMessageEncode(&sub, aScanResult.mIsNative));
diff --git a/src/dbus/common/dbus_resources.hpp b/src/dbus/common/dbus_resources.hpp
index 06ba338..edc7dba 100644
--- a/src/dbus/common/dbus_resources.hpp
+++ b/src/dbus/common/dbus_resources.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_DBUS_RESOURCES_HPP_
 #define OTBR_DBUS_DBUS_RESOURCES_HPP_
 
+#include "openthread-br/config.h"
+
 #include <memory>
 #include <utility>
 
diff --git a/src/dbus/common/error.hpp b/src/dbus/common/error.hpp
index 8eed9d6..8b7a223 100644
--- a/src/dbus/common/error.hpp
+++ b/src/dbus/common/error.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_COMMON_ERROR_HPP_
 #define OTBR_DBUS_COMMON_ERROR_HPP_
 
+#include "openthread-br/config.h"
+
 #include <string>
 
 #include <dbus/dbus.h>
diff --git a/src/dbus/common/types.hpp b/src/dbus/common/types.hpp
index 2510e90..15a49ab 100644
--- a/src/dbus/common/types.hpp
+++ b/src/dbus/common/types.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_COMMON_TYPES_HPP_
 #define OTBR_DBUS_COMMON_TYPES_HPP_
 
+#include "openthread-br/config.h"
+
 #include "dbus/common/error.hpp"
 
 #include <stdint.h>
diff --git a/src/dbus/server/CMakeLists.txt b/src/dbus/server/CMakeLists.txt
index 5136fc0..2d0ae64 100644
--- a/src/dbus/server/CMakeLists.txt
+++ b/src/dbus/server/CMakeLists.txt
@@ -43,14 +43,14 @@
 
 target_include_directories(otbr-dbus-server PRIVATE
     ${PROJECT_BINARY_DIR}/src
-    $<$<BOOL:${OTBR_FEATURE_FLAGS}>:${PROJECT_SOURCE_DIR}/build/src>
+    ${PROJECT_SOURCE_DIR}/build/src
 )
 
 add_dependencies(otbr-dbus-server otbr-dbus-introspect-header)
 
 target_link_libraries(otbr-dbus-server PUBLIC
     otbr-dbus-common
-    $<$<BOOL:${OTBR_FEATURE_FLAGS}>:otbr-proto>
+    otbr-proto
 )
 
 if(OTBR_DOC)
diff --git a/src/dbus/server/dbus_agent.hpp b/src/dbus/server/dbus_agent.hpp
index 51f815d..c9308ec 100644
--- a/src/dbus/server/dbus_agent.hpp
+++ b/src/dbus/server/dbus_agent.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_AGENT_HPP_
 #define OTBR_DBUS_AGENT_HPP_
 
+#include "openthread-br/config.h"
+
 #include <functional>
 #include <set>
 #include <string>
diff --git a/src/dbus/server/dbus_object.cpp b/src/dbus/server/dbus_object.cpp
index 65e0935..64c384a 100644
--- a/src/dbus/server/dbus_object.cpp
+++ b/src/dbus/server/dbus_object.cpp
@@ -115,7 +115,7 @@
 
     if (dbus_message_get_type(aMessage) == DBUS_MESSAGE_TYPE_METHOD_CALL && iter != mMethodHandlers.end())
     {
-        otbrLogInfo("Handling method %s", memberName.c_str());
+        otbrLogDebug("Handling method %s", memberName.c_str());
         if (otbrLogGetLevel() >= OTBR_LOG_DEBUG)
         {
             DumpDBusMessage(*aMessage);
@@ -144,7 +144,7 @@
     {
         auto propertyIter = mGetPropertyHandlers.find(interfaceName);
 
-        otbrLogInfo("GetProperty %s.%s", interfaceName.c_str(), propertyName.c_str());
+        otbrLogDebug("GetProperty %s.%s", interfaceName.c_str(), propertyName.c_str());
         VerifyOrExit(propertyIter != mGetPropertyHandlers.end(), error = OT_ERROR_NOT_FOUND);
         {
             DBusMessageIter replyIter;
diff --git a/src/dbus/server/dbus_object.hpp b/src/dbus/server/dbus_object.hpp
index ebe334f..8aa335b 100644
--- a/src/dbus/server/dbus_object.hpp
+++ b/src/dbus/server/dbus_object.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_DBUS_OBJECT_HPP_
 #define OTBR_DBUS_DBUS_OBJECT_HPP_
 
+#include "openthread-br/config.h"
+
 #ifndef OTBR_LOG_TAG
 #define OTBR_LOG_TAG "DBUS"
 #endif
diff --git a/src/dbus/server/dbus_request.hpp b/src/dbus/server/dbus_request.hpp
index 6f67122..2e71286 100644
--- a/src/dbus/server/dbus_request.hpp
+++ b/src/dbus/server/dbus_request.hpp
@@ -31,6 +31,11 @@
  * This file includes definitions for a d-bus request.
  */
 
+#ifndef DBUS_SERVER_DBUS_REQUEST_HPP_
+#define DBUS_SERVER_DBUS_REQUEST_HPP_
+
+#include "openthread-br/config.h"
+
 #ifndef OTBR_LOG_TAG
 #define OTBR_LOG_TAG "DBUS"
 #endif
@@ -218,3 +223,5 @@
 
 } // namespace DBus
 } // namespace otbr
+
+#endif // DBUS_SERVER_DBUS_REQUEST_HPP_
diff --git a/src/dbus/server/dbus_thread_object.cpp b/src/dbus/server/dbus_thread_object.cpp
index 8a5019f..b183c65 100644
--- a/src/dbus/server/dbus_thread_object.cpp
+++ b/src/dbus/server/dbus_thread_object.cpp
@@ -44,43 +44,23 @@
 #include <openthread/thread_ftd.h>
 #include <openthread/platform/radio.h>
 
+#include "common/api_strings.hpp"
 #include "common/byteswap.hpp"
+#include "common/code_utils.hpp"
 #include "dbus/common/constants.hpp"
 #include "dbus/server/dbus_agent.hpp"
 #include "dbus/server/dbus_thread_object.hpp"
 #if OTBR_ENABLE_FEATURE_FLAGS
 #include "proto/feature_flag.pb.h"
 #endif
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+#include "proto/thread_telemetry.pb.h"
+#endif
+#include "proto/capabilities.pb.h"
 
 using std::placeholders::_1;
 using std::placeholders::_2;
 
-static std::string GetDeviceRoleName(otDeviceRole aRole)
-{
-    std::string roleName;
-
-    switch (aRole)
-    {
-    case OT_DEVICE_ROLE_DISABLED:
-        roleName = OTBR_ROLE_NAME_DISABLED;
-        break;
-    case OT_DEVICE_ROLE_DETACHED:
-        roleName = OTBR_ROLE_NAME_DETACHED;
-        break;
-    case OT_DEVICE_ROLE_CHILD:
-        roleName = OTBR_ROLE_NAME_CHILD;
-        break;
-    case OT_DEVICE_ROLE_ROUTER:
-        roleName = OTBR_ROLE_NAME_ROUTER;
-        break;
-    case OT_DEVICE_ROLE_LEADER:
-        roleName = OTBR_ROLE_NAME_LEADER;
-        break;
-    }
-
-    return roleName;
-}
-
 #if OTBR_ENABLE_NAT64
 static std::string GetNat64StateName(otNat64State aState)
 {
@@ -106,17 +86,6 @@
 }
 #endif // OTBR_ENABLE_NAT64
 
-static uint64_t ConvertOpenThreadUint64(const uint8_t *aValue)
-{
-    uint64_t val = 0;
-
-    for (size_t i = 0; i < sizeof(uint64_t); i++)
-    {
-        val = (val << 8) | aValue[i];
-    }
-    return val;
-}
-
 namespace otbr {
 namespace DBus {
 
@@ -193,6 +162,8 @@
                                std::bind(&DBusThreadObject::SetRadioRegionHandler, this, _1));
     RegisterSetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_DNS_UPSTREAM_QUERY_STATE,
                                std::bind(&DBusThreadObject::SetDnsUpstreamQueryState, this, _1));
+    RegisterSetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_NAT64_CIDR,
+                               std::bind(&DBusThreadObject::SetNat64Cidr, this, _1));
 
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_LINK_MODE,
                                std::bind(&DBusThreadObject::GetLinkModeHandler, this, _1));
@@ -257,6 +228,8 @@
                                std::bind(&DBusThreadObject::GetOnMeshPrefixesHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS,
                                std::bind(&DBusThreadObject::GetActiveDatasetTlvsHandler, this, _1));
+    RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_PENDING_DATASET_TLVS,
+                               std::bind(&DBusThreadObject::GetPendingDatasetTlvsHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_FEATURE_FLAG_LIST_DATA,
                                std::bind(&DBusThreadObject::GetFeatureFlagListDataHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_RADIO_REGION,
@@ -291,10 +264,16 @@
                                std::bind(&DBusThreadObject::GetNat64ProtocolCounters, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_NAT64_ERROR_COUNTERS,
                                std::bind(&DBusThreadObject::GetNat64ErrorCounters, this, _1));
+    RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_NAT64_CIDR,
+                               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_DNS_UPSTREAM_QUERY_STATE,
                                std::bind(&DBusThreadObject::GetDnsUpstreamQueryState, this, _1));
+    RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_TELEMETRY_DATA,
+                               std::bind(&DBusThreadObject::GetTelemetryDataHandler, this, _1));
+    RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_CAPABILITIES,
+                               std::bind(&DBusThreadObject::GetCapabilitiesHandler, this, _1));
 
     SuccessOrExit(error = Signal(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_SIGNAL_READY, std::make_tuple()));
 
@@ -336,7 +315,7 @@
     {
         for (const auto &r : aResult)
         {
-            ActiveScanResult result;
+            ActiveScanResult result = {};
 
             result.mExtAddress = ConvertOpenThreadUint64(r.mExtAddress.m8);
             result.mPanId      = r.mPanId;
@@ -1209,6 +1188,22 @@
     return error;
 }
 
+otError DBusThreadObject::GetPendingDatasetTlvsHandler(DBusMessageIter &aIter)
+{
+    auto                     threadHelper = mNcp->GetThreadHelper();
+    otError                  error        = OT_ERROR_NONE;
+    std::vector<uint8_t>     data;
+    otOperationalDatasetTlvs datasetTlvs;
+
+    SuccessOrExit(error = otDatasetGetPendingTlvs(threadHelper->GetInstance(), &datasetTlvs));
+    data = std::vector<uint8_t>{std::begin(datasetTlvs.mTlvs), std::begin(datasetTlvs.mTlvs) + datasetTlvs.mLength};
+
+    VerifyOrExit(DBusMessageEncodeToVariant(&aIter, data) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS);
+
+exit:
+    return error;
+}
+
 otError DBusThreadObject::SetFeatureFlagListDataHandler(DBusMessageIter &aIter)
 {
 #if OTBR_ENABLE_FEATURE_FLAGS
@@ -1410,6 +1405,48 @@
 #endif // OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
 }
 
+otError DBusThreadObject::GetTelemetryDataHandler(DBusMessageIter &aIter)
+{
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+    otError                      error = OT_ERROR_NONE;
+    threadnetwork::TelemetryData telemetryData;
+    auto                         threadHelper = mNcp->GetThreadHelper();
+
+    VerifyOrExit(threadHelper->RetrieveTelemetryData(mPublisher, telemetryData) == OT_ERROR_NONE);
+
+    {
+        const std::string    telemetryDataBytes = telemetryData.SerializeAsString();
+        std::vector<uint8_t> data(telemetryDataBytes.begin(), telemetryDataBytes.end());
+
+        VerifyOrExit(DBusMessageEncodeToVariant(&aIter, data) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS);
+    }
+
+exit:
+    return error;
+#else
+    OTBR_UNUSED_VARIABLE(aIter);
+    return OT_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+otError DBusThreadObject::GetCapabilitiesHandler(DBusMessageIter &aIter)
+{
+    otError            error = OT_ERROR_NONE;
+    otbr::Capabilities capabilities;
+
+    capabilities.set_nat64(OTBR_ENABLE_NAT64);
+
+    {
+        const std::string    dataBytes = capabilities.SerializeAsString();
+        std::vector<uint8_t> data(dataBytes.begin(), dataBytes.end());
+
+        VerifyOrExit(DBusMessageEncodeToVariant(&aIter, data) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS);
+    }
+
+exit:
+    return error;
+}
+
 void DBusThreadObject::GetPropertiesHandler(DBusRequest &aRequest)
 {
     UniqueDBusMessage        reply(dbus_message_new_method_return(aRequest.GetMessage()));
@@ -1787,6 +1824,36 @@
     return error;
 }
 
+otError DBusThreadObject::GetNat64Cidr(DBusMessageIter &aIter)
+{
+    otError error = OT_ERROR_NONE;
+
+    otIp4Cidr cidr;
+    char      cidrString[OT_IP4_CIDR_STRING_SIZE];
+
+    otNat64GetCidr(mNcp->GetThreadHelper()->GetInstance(), &cidr);
+    otIp4CidrToString(&cidr, cidrString, sizeof(cidrString));
+
+    VerifyOrExit(DBusMessageEncodeToVariant(&aIter, std::string(cidrString)) == OTBR_ERROR_NONE,
+                 error = OT_ERROR_INVALID_ARGS);
+
+exit:
+    return error;
+}
+
+otError DBusThreadObject::SetNat64Cidr(DBusMessageIter &aIter)
+{
+    otError     error = OT_ERROR_NONE;
+    std::string cidrString;
+    otIp4Cidr   cidr;
+
+    VerifyOrExit(DBusMessageExtractFromVariant(&aIter, cidrString) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS);
+    otIp4CidrFromString(cidrString.c_str(), &cidr);
+    SuccessOrExit(error = otNat64SetIp4Cidr(mNcp->GetThreadHelper()->GetInstance(), &cidr));
+
+exit:
+    return error;
+}
 #else  // OTBR_ENABLE_NAT64
 void DBusThreadObject::SetNat64Enabled(DBusRequest &aRequest)
 {
@@ -1817,6 +1884,18 @@
     OTBR_UNUSED_VARIABLE(aIter);
     return OT_ERROR_NOT_IMPLEMENTED;
 }
+
+otError DBusThreadObject::GetNat64Cidr(DBusMessageIter &aIter)
+{
+    OTBR_UNUSED_VARIABLE(aIter);
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+otError DBusThreadObject::SetNat64Cidr(DBusMessageIter &aIter)
+{
+    OTBR_UNUSED_VARIABLE(aIter);
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
 #endif // OTBR_ENABLE_NAT64
 
 otError DBusThreadObject::GetInfraLinkInfo(DBusMessageIter &aIter)
diff --git a/src/dbus/server/dbus_thread_object.hpp b/src/dbus/server/dbus_thread_object.hpp
index 6fefece..9b23bd6 100644
--- a/src/dbus/server/dbus_thread_object.hpp
+++ b/src/dbus/server/dbus_thread_object.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_THREAD_OBJECT_HPP_
 #define OTBR_DBUS_THREAD_OBJECT_HPP_
 
+#include "openthread-br/config.h"
+
 #include <string>
 
 #include <openthread/link.h>
@@ -115,6 +117,7 @@
     otError SetFeatureFlagListDataHandler(DBusMessageIter &aIter);
     otError SetRadioRegionHandler(DBusMessageIter &aIter);
     otError SetDnsUpstreamQueryState(DBusMessageIter &aIter);
+    otError SetNat64Cidr(DBusMessageIter &aIter);
 
     otError GetLinkModeHandler(DBusMessageIter &aIter);
     otError GetDeviceRoleHandler(DBusMessageIter &aIter);
@@ -146,6 +149,7 @@
     otError GetExternalRoutesHandler(DBusMessageIter &aIter);
     otError GetOnMeshPrefixesHandler(DBusMessageIter &aIter);
     otError GetActiveDatasetTlvsHandler(DBusMessageIter &aIter);
+    otError GetPendingDatasetTlvsHandler(DBusMessageIter &aIter);
     otError GetFeatureFlagListDataHandler(DBusMessageIter &aIter);
     otError GetRadioRegionHandler(DBusMessageIter &aIter);
     otError GetSrpServerInfoHandler(DBusMessageIter &aIter);
@@ -160,11 +164,14 @@
     otError GetRadioCoexMetrics(DBusMessageIter &aIter);
     otError GetBorderRoutingCountersHandler(DBusMessageIter &aIter);
     otError GetNat64State(DBusMessageIter &aIter);
+    otError GetNat64Cidr(DBusMessageIter &aIter);
     otError GetNat64Mappings(DBusMessageIter &aIter);
     otError GetNat64ProtocolCounters(DBusMessageIter &aIter);
     otError GetNat64ErrorCounters(DBusMessageIter &aIter);
     otError GetInfraLinkInfo(DBusMessageIter &aIter);
     otError GetDnsUpstreamQueryState(DBusMessageIter &aIter);
+    otError GetTelemetryDataHandler(DBusMessageIter &aIter);
+    otError GetCapabilitiesHandler(DBusMessageIter &aIter);
 
     void ReplyScanResult(DBusRequest &aRequest, otError aError, const std::vector<otActiveScanResult> &aResult);
     void ReplyEnergyScanResult(DBusRequest &aRequest, otError aError, const std::vector<otEnergyScanResult> &aResult);
diff --git a/src/dbus/server/error_helper.hpp b/src/dbus/server/error_helper.hpp
index 1016639..6b35ac1 100644
--- a/src/dbus/server/error_helper.hpp
+++ b/src/dbus/server/error_helper.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_DBUS_SERVER_ERROR_HELPER_HPP_
 #define OTBR_DBUS_SERVER_ERROR_HELPER_HPP_
 
+#include "openthread-br/config.h"
+
 #include <string>
 
 #include <dbus/dbus.h>
diff --git a/src/dbus/server/introspect.xml b/src/dbus/server/introspect.xml
index 227e8af..0a89818 100644
--- a/src/dbus/server/introspect.xml
+++ b/src/dbus/server/introspect.xml
@@ -9,15 +9,22 @@
       <literallayout>
         struct {
           uint64 ext_address
+          string network_name
+          uint64 ext_panid
+          uint8[] steering_data
           uint16 panid
-          uint16 channel
-          uint16 rssi
+          uint16 joiner_udp_port
+          uint8 channel
+          int16 rssi
           uint8 lqi
+          uint8 version
+          bool is_native
+          bool is_joinable
         }
       </literallayout>
     -->
     <method name="Scan">
-      <arg name="scan_result" type="a(tqqqy)" direction="out"/>
+      <arg name="scan_result" type="a(tstayqqynyybb)" direction="out"/>
     </method>
 
     <!-- Energy Scan: Perform a Thread energy scan.
@@ -529,6 +536,11 @@
       <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
     </property>
 
+    <!-- PendingDatasetTlvs: The Thread pending dataset tlv in binary form. -->
+    <property name="PendingDatasetTlvs" type="ay" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
+    </property>
+
      <!-- FeatureFlagListData: The Thread feature flags (defined as proto/feature_flag.proto)
       in binary form. -->
     <property name="FeatureFlagListData" type="ay" access="readwrite">
@@ -861,6 +873,11 @@
       <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
     </property>
 
+    <!-- Nat64Cidr: The CIDR used for NAT64 -->
+    <property name="Nat64Cidr" type="s" access="readwrite">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
+    </property>
+
     <!-- BorderRoutingCounters: The counters for inbound/outbound packets
     <literallayout>
         struct {
@@ -916,6 +933,18 @@
       <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
     </property>
 
+    <!-- TelemetryData: The Thread telemetry data (defined as proto/thread_telemetry.proto)
+      in binary form. -->
+    <property name="TelemetryData" type="ay" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
+    </property>
+
+    <!-- Capabilities: The Thread capabilities data (defined as proto/capabilities.proto)
+      in binary form. -->
+    <property name="Capabilities" type="ay" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
+    </property>
+
     <!-- The Ready signal is sent on start -->
     <signal name="Ready">
     </signal>
diff --git a/src/mdns/mdns.cpp b/src/mdns/mdns.cpp
index 9646d55..d7e0935 100644
--- a/src/mdns/mdns.cpp
+++ b/src/mdns/mdns.cpp
@@ -52,23 +52,21 @@
                                const std::string &aType,
                                const SubTypeList &aSubTypeList,
                                uint16_t           aPort,
-                               const TxtList     &aTxtList,
+                               const TxtData     &aTxtData,
                                ResultCallback   &&aCallback)
 {
     otbrError error;
 
     mServiceRegistrationBeginTime[std::make_pair(aName, aType)] = Clock::now();
 
-    error = PublishServiceImpl(aHostName, aName, aType, aSubTypeList, aPort, aTxtList, std::move(aCallback));
+    error = PublishServiceImpl(aHostName, aName, aType, aSubTypeList, aPort, aTxtData, std::move(aCallback));
     if (error != OTBR_ERROR_NONE)
     {
         UpdateMdnsResponseCounters(mTelemetryInfo.mServiceRegistrations, error);
     }
 }
 
-void Publisher::PublishHost(const std::string             &aName,
-                            const std::vector<Ip6Address> &aAddresses,
-                            ResultCallback               &&aCallback)
+void Publisher::PublishHost(const std::string &aName, const AddressList &aAddresses, ResultCallback &&aCallback)
 {
     otbrError error;
 
@@ -81,14 +79,14 @@
     }
 }
 
-void Publisher::OnServiceResolveFailed(const std::string &aType, const std::string &aInstanceName, int32_t aErrorCode)
+void Publisher::OnServiceResolveFailed(std::string aType, std::string aInstanceName, int32_t aErrorCode)
 {
     UpdateMdnsResponseCounters(mTelemetryInfo.mServiceResolutions, DnsErrorToOtbrError(aErrorCode));
     UpdateServiceInstanceResolutionEmaLatency(aInstanceName, aType, DnsErrorToOtbrError(aErrorCode));
     OnServiceResolveFailedImpl(aType, aInstanceName, aErrorCode);
 }
 
-void Publisher::OnHostResolveFailed(const std::string &aHostName, int32_t aErrorCode)
+void Publisher::OnHostResolveFailed(std::string aHostName, int32_t aErrorCode)
 {
     UpdateMdnsResponseCounters(mTelemetryInfo.mHostResolutions, DnsErrorToOtbrError(aErrorCode));
     UpdateHostResolutionEmaLatency(aHostName, DnsErrorToOtbrError(aErrorCode));
@@ -99,18 +97,32 @@
 {
     otbrError error = OTBR_ERROR_NONE;
 
-    for (const auto &txtEntry : aTxtList)
+    aTxtData.clear();
+
+    for (const TxtEntry &txtEntry : aTxtList)
     {
-        const auto  &name        = txtEntry.mName;
-        const auto  &value       = txtEntry.mValue;
-        const size_t entryLength = name.length() + 1 + value.size();
+        size_t entryLength = txtEntry.mKey.length();
+
+        if (!txtEntry.mIsBooleanAttribute)
+        {
+            entryLength += txtEntry.mValue.size() + sizeof(uint8_t); // for `=` char.
+        }
 
         VerifyOrExit(entryLength <= kMaxTextEntrySize, error = OTBR_ERROR_INVALID_ARGS);
 
         aTxtData.push_back(static_cast<uint8_t>(entryLength));
-        aTxtData.insert(aTxtData.end(), name.begin(), name.end());
-        aTxtData.push_back('=');
-        aTxtData.insert(aTxtData.end(), value.begin(), value.end());
+        aTxtData.insert(aTxtData.end(), txtEntry.mKey.begin(), txtEntry.mKey.end());
+
+        if (!txtEntry.mIsBooleanAttribute)
+        {
+            aTxtData.push_back('=');
+            aTxtData.insert(aTxtData.end(), txtEntry.mValue.begin(), txtEntry.mValue.end());
+        }
+    }
+
+    if (aTxtData.empty())
+    {
+        aTxtData.push_back(0);
     }
 
 exit:
@@ -121,30 +133,39 @@
 {
     otbrError error = OTBR_ERROR_NONE;
 
+    aTxtList.clear();
+
     for (uint16_t r = 0; r < aTxtLength;)
     {
         uint16_t entrySize = aTxtData[r];
         uint16_t keyStart  = r + 1;
         uint16_t entryEnd  = keyStart + entrySize;
         uint16_t keyEnd    = keyStart;
-        uint16_t valStart;
+
+        VerifyOrExit(entryEnd <= aTxtLength, error = OTBR_ERROR_PARSE);
 
         while (keyEnd < entryEnd && aTxtData[keyEnd] != '=')
         {
             keyEnd++;
         }
 
-        valStart = keyEnd;
-        if (valStart < entryEnd && aTxtData[valStart] == '=')
+        if (keyEnd == entryEnd)
         {
-            valStart++;
+            if (keyEnd > keyStart)
+            {
+                // No `=`, treat as a boolean attribute.
+                aTxtList.emplace_back(reinterpret_cast<const char *>(&aTxtData[keyStart]), keyEnd - keyStart);
+            }
+        }
+        else
+        {
+            uint16_t valStart = keyEnd + 1; // To skip over `=`
+
+            aTxtList.emplace_back(reinterpret_cast<const char *>(&aTxtData[keyStart]), keyEnd - keyStart,
+                                  &aTxtData[valStart], entryEnd - valStart);
         }
 
-        aTxtList.emplace_back(reinterpret_cast<const char *>(&aTxtData[keyStart]), keyEnd - keyStart,
-                              &aTxtData[valStart], entryEnd - valStart);
-
         r += entrySize + 1;
-        VerifyOrExit(r <= aTxtLength, error = OTBR_ERROR_PARSE);
     }
 
 exit:
@@ -175,7 +196,7 @@
     return subscriberId;
 }
 
-void Publisher::OnServiceResolved(const std::string &aType, const DiscoveredInstanceInfo &aInstanceInfo)
+void Publisher::OnServiceResolved(std::string aType, DiscoveredInstanceInfo aInstanceInfo)
 {
     std::vector<uint64_t> subscriberIds;
 
@@ -216,7 +237,7 @@
     }
 }
 
-void Publisher::OnServiceRemoved(uint32_t aNetifIndex, const std::string &aType, const std::string &aInstanceName)
+void Publisher::OnServiceRemoved(uint32_t aNetifIndex, std::string aType, std::string aInstanceName)
 {
     DiscoveredInstanceInfo instanceInfo;
 
@@ -229,7 +250,7 @@
     OnServiceResolved(aType, instanceInfo);
 }
 
-void Publisher::OnHostResolved(const std::string &aHostName, const Publisher::DiscoveredHostInfo &aHostInfo)
+void Publisher::OnHostResolved(std::string aHostName, Publisher::DiscoveredHostInfo aHostInfo)
 {
     otbrLogInfo("Host %s is resolved successfully: host %s addresses %zu ttl %u", aHostName.c_str(),
                 aHostInfo.mHostName.c_str(), aHostInfo.mAddresses.size(), aHostInfo.mTtl);
@@ -257,13 +278,6 @@
     return aSubTypeList;
 }
 
-Publisher::TxtList Publisher::SortTxtList(TxtList aTxtList)
-{
-    std::sort(aTxtList.begin(), aTxtList.end(),
-              [](const TxtEntry &aLhs, const TxtEntry &aRhs) { return aLhs.mName < aRhs.mName; });
-    return aTxtList;
-}
-
 Publisher::AddressList Publisher::SortAddressList(AddressList aAddressList)
 {
     std::sort(aAddressList.begin(), aAddressList.end());
@@ -316,14 +330,14 @@
                                                                         const std::string &aType,
                                                                         const SubTypeList &aSubTypeList,
                                                                         uint16_t           aPort,
-                                                                        const TxtList     &aTxtList,
+                                                                        const TxtData     &aTxtData,
                                                                         ResultCallback   &&aCallback)
 {
     ServiceRegistration *serviceReg = FindServiceRegistration(aName, aType);
 
     VerifyOrExit(serviceReg != nullptr);
 
-    if (serviceReg->IsOutdated(aHostName, aName, aType, aSubTypeList, aPort, aTxtList))
+    if (serviceReg->IsOutdated(aHostName, aName, aType, aSubTypeList, aPort, aTxtData))
     {
         otbrLogInfo("Removing existing service %s.%s: outdated", aName.c_str(), aType.c_str());
         RemoveServiceRegistration(aName, aType, OTBR_ERROR_ABORTED);
@@ -352,9 +366,9 @@
     return std::move(aCallback);
 }
 
-Publisher::ResultCallback Publisher::HandleDuplicateHostRegistration(const std::string             &aName,
-                                                                     const std::vector<Ip6Address> &aAddresses,
-                                                                     ResultCallback               &&aCallback)
+Publisher::ResultCallback Publisher::HandleDuplicateHostRegistration(const std::string &aName,
+                                                                     const AddressList &aAddresses,
+                                                                     ResultCallback   &&aCallback)
 {
     HostRegistration *hostReg = FindHostRegistration(aName);
 
@@ -431,10 +445,10 @@
                                                 const std::string &aType,
                                                 const SubTypeList &aSubTypeList,
                                                 uint16_t           aPort,
-                                                const TxtList     &aTxtList) const
+                                                const TxtData     &aTxtData) const
 {
     return !(mHostName == aHostName && mName == aName && mType == aType && mSubTypeList == aSubTypeList &&
-             mPort == aPort && mTxtList == aTxtList);
+             mPort == aPort && mTxtData == aTxtData);
 }
 
 void Publisher::ServiceRegistration::Complete(otbrError aError)
@@ -452,7 +466,7 @@
     }
 }
 
-bool Publisher::HostRegistration::IsOutdated(const std::string &aName, const std::vector<Ip6Address> &aAddresses) const
+bool Publisher::HostRegistration::IsOutdated(const std::string &aName, const AddressList &aAddresses) const
 {
     return !(mName == aName && mAddresses == aAddresses);
 }
@@ -577,5 +591,4 @@
 }
 
 } // namespace Mdns
-
 } // namespace otbr
diff --git a/src/mdns/mdns.hpp b/src/mdns/mdns.hpp
index a06578a..9a28b88 100644
--- a/src/mdns/mdns.hpp
+++ b/src/mdns/mdns.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_MDNS_HPP_
 #define OTBR_AGENT_MDNS_HPP_
 
+#include "openthread-br/config.h"
+
 #include <functional>
 #include <map>
 #include <memory>
@@ -68,33 +70,51 @@
 {
 public:
     /**
-     * This structure represents a name/value pair of the TXT record.
+     * This structure represents a key/value pair of the TXT record.
      *
      */
     struct TxtEntry
     {
-        std::string          mName;  ///< The name of the TXT entry.
-        std::vector<uint8_t> mValue; ///< The value of the TXT entry.
+        std::string          mKey;                ///< The key of the TXT entry.
+        std::vector<uint8_t> mValue;              ///< The value of the TXT entry. Can be empty.
+        bool                 mIsBooleanAttribute; ///< This entry is boolean attribute (encoded as `key` without `=`).
 
-        TxtEntry(const char *aName, const char *aValue)
-            : TxtEntry(aName, reinterpret_cast<const uint8_t *>(aValue), strlen(aValue))
+        TxtEntry(const char *aKey, const char *aValue)
+            : TxtEntry(aKey, reinterpret_cast<const uint8_t *>(aValue), strlen(aValue))
         {
         }
 
-        TxtEntry(const char *aName, const uint8_t *aValue, size_t aValueLength)
-            : TxtEntry(aName, strlen(aName), aValue, aValueLength)
+        TxtEntry(const char *aKey, const uint8_t *aValue, size_t aValueLength)
+            : TxtEntry(aKey, strlen(aKey), aValue, aValueLength)
         {
         }
 
-        TxtEntry(const char *aName, size_t aNameLength, const uint8_t *aValue, size_t aValueLength)
-            : mName(aName, aNameLength)
+        TxtEntry(const char *aKey, size_t aKeyLength, const uint8_t *aValue, size_t aValueLength)
+            : mKey(aKey, aKeyLength)
             , mValue(aValue, aValue + aValueLength)
+            , mIsBooleanAttribute(false)
         {
         }
 
-        bool operator==(const TxtEntry &aOther) const { return mName == aOther.mName && mValue == aOther.mValue; }
+        TxtEntry(const char *aKey)
+            : TxtEntry(aKey, strlen(aKey))
+        {
+        }
+
+        TxtEntry(const char *aKey, size_t aKeyLength)
+            : mKey(aKey, aKeyLength)
+            , mIsBooleanAttribute(true)
+        {
+        }
+
+        bool operator==(const TxtEntry &aOther) const
+        {
+            return (mKey == aOther.mKey) && (mValue == aOther.mValue) &&
+                   (mIsBooleanAttribute == aOther.mIsBooleanAttribute);
+        }
     };
 
+    typedef std::vector<uint8_t>     TxtData;
     typedef std::vector<TxtEntry>    TxtList;
     typedef std::vector<std::string> SubTypeList;
     typedef std::vector<Ip6Address>  AddressList;
@@ -105,16 +125,16 @@
      */
     struct DiscoveredInstanceInfo
     {
-        bool                    mRemoved    = false; ///< The Service Instance is removed.
-        uint32_t                mNetifIndex = 0;     ///< Network interface.
-        std::string             mName;               ///< Instance name.
-        std::string             mHostName;           ///< Full host name.
-        std::vector<Ip6Address> mAddresses;          ///< IPv6 addresses.
-        uint16_t                mPort     = 0;       ///< Port.
-        uint16_t                mPriority = 0;       ///< Service priority.
-        uint16_t                mWeight   = 0;       ///< Service weight.
-        std::vector<uint8_t>    mTxtData;            ///< TXT RDATA bytes.
-        uint32_t                mTtl = 0;            ///< Service TTL.
+        bool        mRemoved    = false; ///< The Service Instance is removed.
+        uint32_t    mNetifIndex = 0;     ///< Network interface.
+        std::string mName;               ///< Instance name.
+        std::string mHostName;           ///< Full host name.
+        AddressList mAddresses;          ///< IPv6 addresses.
+        uint16_t    mPort     = 0;       ///< Port.
+        uint16_t    mPriority = 0;       ///< Service priority.
+        uint16_t    mWeight   = 0;       ///< Service weight.
+        TxtData     mTxtData;            ///< TXT RDATA bytes.
+        uint32_t    mTtl = 0;            ///< Service TTL.
     };
 
     /**
@@ -123,9 +143,9 @@
      */
     struct DiscoveredHostInfo
     {
-        std::string             mHostName;  ///< Full host name.
-        std::vector<Ip6Address> mAddresses; ///< IP6 addresses.
-        uint32_t                mTtl = 0;   ///< Host TTL.
+        std::string mHostName;  ///< Full host name.
+        AddressList mAddresses; ///< IP6 addresses.
+        uint32_t    mTtl = 0;   ///< Host TTL.
     };
 
     /**
@@ -191,10 +211,10 @@
      *                          with method PublishHost.
      * @param[in] aName         The name of this service. If an empty string is provided, the service's name will be the
      *                          same as the platform's hostname.
-     * @param[in] aType         The type of this service.
+     * @param[in] aType         The type of this service, e.g., "_srv._udp" (MUST NOT end with dot).
      * @param[in] aSubTypeList  A list of service subtypes.
      * @param[in] aPort         The port number of this service.
-     * @param[in] aTxtList      A list of TXT name/value pairs.
+     * @param[in] aTxtData      The encoded TXT data for this service.
      * @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
@@ -207,14 +227,14 @@
                         const std::string &aType,
                         const SubTypeList &aSubTypeList,
                         uint16_t           aPort,
-                        const TxtList     &aTxtList,
+                        const TxtData     &aTxtData,
                         ResultCallback   &&aCallback);
 
     /**
      * This method un-publishes a service.
      *
      * @param[in] aName      The name of this service.
-     * @param[in] aType      The type of this service.
+     * @param[in] aType      The type of this service, e.g., "_srv._udp" (MUST NOT end with dot).
      * @param[in] aCallback  The callback for receiving the publishing result.
      *
      */
@@ -235,12 +255,12 @@
      *                        alternative name is available/acceptable.
      *
      */
-    void PublishHost(const std::string &aName, const std::vector<Ip6Address> &aAddresses, ResultCallback &&aCallback);
+    void PublishHost(const std::string &aName, const AddressList &aAddresses, ResultCallback &&aCallback);
 
     /**
      * This method un-publishes a host.
      *
-     * @param[in] aName      A host name.
+     * @param[in] aName      A host name (MUST not end with dot).
      * @param[in] aCallback  The callback for receiving the publishing result.
      *
      */
@@ -256,7 +276,7 @@
      * @note Discovery Proxy implementation guarantees no duplicate subscriptions for the same service or service
      * instance.
      *
-     * @param[in] aType          The service type.
+     * @param[in] aType          The service type, e.g., "_srv._udp" (MUST NOT end with dot).
      * @param[in] aInstanceName  The service instance to subscribe, or empty to subscribe the service.
      *
      */
@@ -270,7 +290,7 @@
      *
      * @note Discovery Proxy implementation guarantees no redundant unsubscription for a service or service instance.
      *
-     * @param[in] aType          The service type.
+     * @param[in] aType          The service type, e.g., "_srv._udp" (MUST NOT end with dot).
      * @param[in] aInstanceName  The service instance to unsubscribe, or empty to unsubscribe the service.
      *
      */
@@ -324,7 +344,7 @@
      * @returns  The MdnsTelemetryInfo of the publisher.
      *
      */
-    const MdnsTelemetryInfo &GetMdnsTelemetryInfo() const { return mTelemetryInfo; }
+    const MdnsTelemetryInfo &GetMdnsTelemetryInfo(void) const { return mTelemetryInfo; }
 
     virtual ~Publisher(void) = default;
 
@@ -354,7 +374,7 @@
      * See RFC 6763 for details: https://tools.ietf.org/html/rfc6763#section-6.
      *
      * @param[in]  aTxtList  A TXT entry list.
-     * @param[out] aTxtData  A TXT data buffer.
+     * @param[out] aTxtData  A TXT data buffer. Will be cleared.
      *
      * @retval OTBR_ERROR_NONE          Successfully write the TXT entry list.
      * @retval OTBR_ERROR_INVALID_ARGS  The @p aTxtList includes invalid TXT entry.
@@ -362,7 +382,7 @@
      * @sa DecodeTxtData
      *
      */
-    static otbrError EncodeTxtData(const TxtList &aTxtList, std::vector<uint8_t> &aTxtData);
+    static otbrError EncodeTxtData(const TxtList &aTxtList, TxtData &aTxtData);
 
     /**
      * This function decodes a TXT entry list from a TXT data buffer.
@@ -422,14 +442,14 @@
         std::string mType;
         SubTypeList mSubTypeList;
         uint16_t    mPort;
-        TxtList     mTxtList;
+        TxtData     mTxtData;
 
         ServiceRegistration(std::string      aHostName,
                             std::string      aName,
                             std::string      aType,
                             SubTypeList      aSubTypeList,
                             uint16_t         aPort,
-                            TxtList          aTxtList,
+                            TxtData          aTxtData,
                             ResultCallback &&aCallback,
                             Publisher       *aPublisher)
             : Registration(std::move(aCallback), aPublisher)
@@ -438,29 +458,30 @@
             , mType(std::move(aType))
             , mSubTypeList(SortSubTypeList(std::move(aSubTypeList)))
             , mPort(aPort)
-            , mTxtList(SortTxtList(std::move(aTxtList)))
+            , mTxtData(std::move(aTxtData))
         {
         }
         ~ServiceRegistration(void) override { OnComplete(OTBR_ERROR_ABORTED); }
 
         void Complete(otbrError aError);
 
-        void OnComplete(otbrError aError);
-
         // Tells whether this `ServiceRegistration` object is outdated comparing to the given parameters.
         bool IsOutdated(const std::string &aHostName,
                         const std::string &aName,
                         const std::string &aType,
                         const SubTypeList &aSubTypeList,
                         uint16_t           aPort,
-                        const TxtList     &aTxtList) const;
+                        const TxtData     &aTxtData) const;
+
+    private:
+        void OnComplete(otbrError aError);
     };
 
     class HostRegistration : public Registration
     {
     public:
-        std::string             mName;
-        std::vector<Ip6Address> mAddresses;
+        std::string mName;
+        AddressList mAddresses;
 
         HostRegistration(std::string aName, AddressList aAddresses, ResultCallback &&aCallback, Publisher *aPublisher)
             : Registration(std::move(aCallback), aPublisher)
@@ -469,14 +490,15 @@
         {
         }
 
-        ~HostRegistration(void) { OnComplete(OTBR_ERROR_ABORTED); }
+        ~HostRegistration(void) override { OnComplete(OTBR_ERROR_ABORTED); }
 
         void Complete(otbrError aError);
 
-        void OnComplete(otbrError);
-
         // Tells whether this `HostRegistration` object is outdated comparing to the given parameters.
-        bool IsOutdated(const std::string &aName, const std::vector<Ip6Address> &aAddresses) const;
+        bool IsOutdated(const std::string &aName, const AddressList &aAddresses) const;
+
+    private:
+        void OnComplete(otbrError aError);
     };
 
     using ServiceRegistrationPtr = std::unique_ptr<ServiceRegistration>;
@@ -485,7 +507,6 @@
     using HostRegistrationMap    = std::map<std::string, HostRegistrationPtr>;
 
     static SubTypeList SortSubTypeList(SubTypeList aSubTypeList);
-    static TxtList     SortTxtList(TxtList aTxtList);
     static AddressList SortAddressList(AddressList aAddressList);
     static std::string MakeFullServiceName(const std::string &aName, const std::string &aType);
     static std::string MakeFullHostName(const std::string &aName);
@@ -495,26 +516,30 @@
                                          const std::string &aType,
                                          const SubTypeList &aSubTypeList,
                                          uint16_t           aPort,
-                                         const TxtList     &aTxtList,
-                                         ResultCallback   &&aCallback)                            = 0;
-    virtual otbrError PublishHostImpl(const std::string             &aName,
-                                      const std::vector<Ip6Address> &aAddresses,
-                                      ResultCallback               &&aCallback)                               = 0;
-    virtual void      OnServiceResolveFailedImpl(const std::string &aType,
-                                                 const std::string &aInstanceName,
-                                                 int32_t            aErrorCode)                            = 0;
-    virtual void      OnHostResolveFailedImpl(const std::string &aHostName, int32_t aErrorCode) = 0;
+                                         const TxtData     &aTxtData,
+                                         ResultCallback   &&aCallback) = 0;
+
+    virtual otbrError PublishHostImpl(const std::string &aName,
+                                      const AddressList &aAddresses,
+                                      ResultCallback   &&aCallback) = 0;
+
+    virtual void OnServiceResolveFailedImpl(const std::string &aType,
+                                            const std::string &aInstanceName,
+                                            int32_t            aErrorCode) = 0;
+
+    virtual void OnHostResolveFailedImpl(const std::string &aHostName, int32_t aErrorCode) = 0;
 
     virtual otbrError DnsErrorToOtbrError(int32_t aError) = 0;
 
     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);
-    void                 OnServiceResolved(const std::string &aType, const DiscoveredInstanceInfo &aInstanceInfo);
-    void OnServiceResolveFailed(const std::string &aType, const std::string &aInstanceName, int32_t aErrorCode);
-    void OnServiceRemoved(uint32_t aNetifIndex, const std::string &aType, const std::string &aInstanceName);
-    void OnHostResolved(const std::string &aHostName, const DiscoveredHostInfo &aHostInfo);
-    void OnHostResolveFailed(const std::string &aHostName, int32_t aErrorCode);
+
+    void OnServiceResolved(std::string aType, DiscoveredInstanceInfo aInstanceInfo);
+    void OnServiceResolveFailed(std::string aType, std::string aInstanceName, int32_t aErrorCode);
+    void OnServiceRemoved(uint32_t aNetifIndex, std::string aType, std::string aInstanceName);
+    void OnHostResolved(std::string aHostName, DiscoveredHostInfo aHostInfo);
+    void OnHostResolveFailed(std::string aHostName, int32_t aErrorCode);
 
     // Handles the cases that there is already a registration for the same service.
     // If the returned callback is completed, current registration should be considered
@@ -524,18 +549,18 @@
                                                       const std::string &aType,
                                                       const SubTypeList &aSubTypeList,
                                                       uint16_t           aPort,
-                                                      const TxtList     &aTxtList,
+                                                      const TxtData     &aTxtData,
                                                       ResultCallback   &&aCallback);
 
-    ResultCallback HandleDuplicateHostRegistration(const std::string             &aName,
-                                                   const std::vector<Ip6Address> &aAddresses,
-                                                   ResultCallback               &&aCallback);
+    ResultCallback HandleDuplicateHostRegistration(const std::string &aName,
+                                                   const AddressList &aAddresses,
+                                                   ResultCallback   &&aCallback);
 
     void              AddHostRegistration(HostRegistrationPtr &&aHostReg);
     void              RemoveHostRegistration(const std::string &aName, otbrError aError);
     HostRegistration *FindHostRegistration(const std::string &aName);
 
-    static void UpdateMdnsResponseCounters(otbr::MdnsResponseCounters &aCounters, otbrError aError);
+    static void UpdateMdnsResponseCounters(MdnsResponseCounters &aCounters, otbrError aError);
     static void UpdateEmaLatency(uint32_t &aEmaLatency, uint32_t aLatency, otbrError aError);
 
     void UpdateServiceRegistrationEmaLatency(const std::string &aInstanceName,
@@ -562,7 +587,7 @@
     // host name -> the timepoint to begin host resolution
     std::map<std::string, Timepoint> mHostResolutionBeginTime;
 
-    otbr::MdnsTelemetryInfo mTelemetryInfo{};
+    MdnsTelemetryInfo mTelemetryInfo{};
 };
 
 /**
diff --git a/src/mdns/mdns_avahi.cpp b/src/mdns/mdns_avahi.cpp
index cba9081..898591f 100644
--- a/src/mdns/mdns_avahi.cpp
+++ b/src/mdns/mdns_avahi.cpp
@@ -53,30 +53,42 @@
 #include "common/logging.hpp"
 #include "common/time.hpp"
 
+namespace otbr {
+namespace Mdns {
+
+class AvahiPoller;
+
+} // namespace Mdns
+} // namespace otbr
+
 struct AvahiWatch
 {
-    int                mFd;       ///< The file descriptor to watch.
-    AvahiWatchEvent    mEvents;   ///< The interested events.
-    int                mHappened; ///< The events happened.
-    AvahiWatchCallback mCallback; ///< The function to be called when interested events happened on mFd.
-    void              *mContext;  ///< A pointer to application-specific context.
-    void              *mPoller;   ///< The poller created this watch.
+    typedef otbr::Mdns::AvahiPoller AvahiPoller;
+
+    int                mFd;           ///< The file descriptor to watch.
+    AvahiWatchEvent    mEvents;       ///< The interested events.
+    int                mHappened;     ///< The events happened.
+    AvahiWatchCallback mCallback;     ///< The function to be called to report events happened on `mFd`.
+    void              *mContext;      ///< A pointer to application-specific context to use with `mCallback`.
+    bool               mShouldReport; ///< Whether or not we need to report events (invoking callback).
+    AvahiPoller       &mPoller;       ///< The poller owning this watch.
 
     /**
      * The constructor to initialize an Avahi watch.
      *
      * @param[in] aFd        The file descriptor to watch.
      * @param[in] aEvents    The events to watch.
-     * @param[in] aCallback  The function to be called when events happend on this file descriptor.
+     * @param[in] aCallback  The function to be called when events happened on this file descriptor.
      * @param[in] aContext   A pointer to application-specific context.
      * @param[in] aPoller    The AvahiPoller this watcher belongs to.
      *
      */
-    AvahiWatch(int aFd, AvahiWatchEvent aEvents, AvahiWatchCallback aCallback, void *aContext, void *aPoller)
+    AvahiWatch(int aFd, AvahiWatchEvent aEvents, AvahiWatchCallback aCallback, void *aContext, AvahiPoller &aPoller)
         : mFd(aFd)
         , mEvents(aEvents)
         , mCallback(aCallback)
         , mContext(aContext)
+        , mShouldReport(false)
         , mPoller(aPoller)
     {
     }
@@ -88,10 +100,13 @@
  */
 struct AvahiTimeout
 {
-    otbr::Timepoint      mTimeout;  ///< Absolute time when this timer timeout.
-    AvahiTimeoutCallback mCallback; ///< The function to be called when timeout.
-    void                *mContext;  ///< The pointer to application-specific context.
-    void                *mPoller;   ///< The poller created this timer.
+    typedef otbr::Mdns::AvahiPoller AvahiPoller;
+
+    otbr::Timepoint      mTimeout;      ///< Absolute time when this timer timeout.
+    AvahiTimeoutCallback mCallback;     ///< The function to be called when timeout.
+    void                *mContext;      ///< The pointer to application-specific context.
+    bool                 mShouldReport; ///< Whether or not timeout occurred and need to reported (invoking callback).
+    AvahiPoller         &mPoller;       ///< The poller created this timer.
 
     /**
      * The constructor to initialize an AvahiTimeout.
@@ -102,9 +117,10 @@
      * @param[in] aPoller    The AvahiPoller this timeout belongs to.
      *
      */
-    AvahiTimeout(const struct timeval *aTimeout, AvahiTimeoutCallback aCallback, void *aContext, void *aPoller)
+    AvahiTimeout(const struct timeval *aTimeout, AvahiTimeoutCallback aCallback, void *aContext, AvahiPoller &aPoller)
         : mCallback(aCallback)
         , mContext(aContext)
+        , mShouldReport(false)
         , mPoller(aPoller)
     {
         if (aTimeout)
@@ -168,13 +184,13 @@
     void Update(MainloopContext &aMainloop) override;
     void Process(const MainloopContext &aMainloop) override;
 
-    const AvahiPoll *GetAvahiPoll(void) const { return &mAvahiPoller; }
+    const AvahiPoll *GetAvahiPoll(void) const { return &mAvahiPoll; }
 
 private:
     typedef std::vector<AvahiWatch *>   Watches;
     typedef std::vector<AvahiTimeout *> Timers;
 
-    static AvahiWatch     *WatchNew(const struct AvahiPoll *aPoller,
+    static AvahiWatch     *WatchNew(const struct AvahiPoll *aPoll,
                                     int                     aFd,
                                     AvahiWatchEvent         aEvent,
                                     AvahiWatchCallback      aCallback,
@@ -184,7 +200,7 @@
     static AvahiWatchEvent WatchGetEvents(AvahiWatch *aWatch);
     static void            WatchFree(AvahiWatch *aWatch);
     void                   WatchFree(AvahiWatch &aWatch);
-    static AvahiTimeout   *TimeoutNew(const AvahiPoll      *aPoller,
+    static AvahiTimeout   *TimeoutNew(const AvahiPoll      *aPoll,
                                       const struct timeval *aTimeout,
                                       AvahiTimeoutCallback  aCallback,
                                       void                 *aContext);
@@ -195,36 +211,36 @@
 
     Watches   mWatches;
     Timers    mTimers;
-    AvahiPoll mAvahiPoller;
+    AvahiPoll mAvahiPoll;
 };
 
 AvahiPoller::AvahiPoller(void)
 {
-    mAvahiPoller.userdata         = this;
-    mAvahiPoller.watch_new        = WatchNew;
-    mAvahiPoller.watch_update     = WatchUpdate;
-    mAvahiPoller.watch_get_events = WatchGetEvents;
-    mAvahiPoller.watch_free       = WatchFree;
+    mAvahiPoll.userdata         = this;
+    mAvahiPoll.watch_new        = WatchNew;
+    mAvahiPoll.watch_update     = WatchUpdate;
+    mAvahiPoll.watch_get_events = WatchGetEvents;
+    mAvahiPoll.watch_free       = WatchFree;
 
-    mAvahiPoller.timeout_new    = TimeoutNew;
-    mAvahiPoller.timeout_update = TimeoutUpdate;
-    mAvahiPoller.timeout_free   = TimeoutFree;
+    mAvahiPoll.timeout_new    = TimeoutNew;
+    mAvahiPoll.timeout_update = TimeoutUpdate;
+    mAvahiPoll.timeout_free   = TimeoutFree;
 }
 
-AvahiWatch *AvahiPoller::WatchNew(const struct AvahiPoll *aPoller,
+AvahiWatch *AvahiPoller::WatchNew(const struct AvahiPoll *aPoll,
                                   int                     aFd,
                                   AvahiWatchEvent         aEvent,
                                   AvahiWatchCallback      aCallback,
                                   void                   *aContext)
 {
-    return reinterpret_cast<AvahiPoller *>(aPoller->userdata)->WatchNew(aFd, aEvent, aCallback, aContext);
+    return reinterpret_cast<AvahiPoller *>(aPoll->userdata)->WatchNew(aFd, aEvent, aCallback, aContext);
 }
 
 AvahiWatch *AvahiPoller::WatchNew(int aFd, AvahiWatchEvent aEvent, AvahiWatchCallback aCallback, void *aContext)
 {
     assert(aEvent && aCallback && aFd >= 0);
 
-    mWatches.push_back(new AvahiWatch(aFd, aEvent, aCallback, aContext, this));
+    mWatches.push_back(new AvahiWatch(aFd, aEvent, aCallback, aContext, *this));
 
     return mWatches.back();
 }
@@ -241,7 +257,7 @@
 
 void AvahiPoller::WatchFree(AvahiWatch *aWatch)
 {
-    reinterpret_cast<AvahiPoller *>(aWatch->mPoller)->WatchFree(*aWatch);
+    aWatch->mPoller.WatchFree(*aWatch);
 }
 
 void AvahiPoller::WatchFree(AvahiWatch &aWatch)
@@ -257,18 +273,18 @@
     }
 }
 
-AvahiTimeout *AvahiPoller::TimeoutNew(const AvahiPoll      *aPoller,
+AvahiTimeout *AvahiPoller::TimeoutNew(const AvahiPoll      *aPoll,
                                       const struct timeval *aTimeout,
                                       AvahiTimeoutCallback  aCallback,
                                       void                 *aContext)
 {
-    assert(aPoller && aCallback);
-    return static_cast<AvahiPoller *>(aPoller->userdata)->TimeoutNew(aTimeout, aCallback, aContext);
+    assert(aPoll && aCallback);
+    return static_cast<AvahiPoller *>(aPoll->userdata)->TimeoutNew(aTimeout, aCallback, aContext);
 }
 
 AvahiTimeout *AvahiPoller::TimeoutNew(const struct timeval *aTimeout, AvahiTimeoutCallback aCallback, void *aContext)
 {
-    mTimers.push_back(new AvahiTimeout(aTimeout, aCallback, aContext, this));
+    mTimers.push_back(new AvahiTimeout(aTimeout, aCallback, aContext, *this));
     return mTimers.back();
 }
 
@@ -286,7 +302,7 @@
 
 void AvahiPoller::TimeoutFree(AvahiTimeout *aTimer)
 {
-    static_cast<AvahiPoller *>(aTimer->mPoller)->TimeoutFree(*aTimer);
+    aTimer->mPoller.TimeoutFree(*aTimer);
 }
 
 void AvahiPoller::TimeoutFree(AvahiTimeout &aTimer)
@@ -306,10 +322,10 @@
 {
     Timepoint now = Clock::now();
 
-    for (Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it)
+    for (AvahiWatch *watch : mWatches)
     {
-        int             fd     = (*it)->mFd;
-        AvahiWatchEvent events = (*it)->mEvents;
+        int             fd     = watch->mFd;
+        AvahiWatchEvent events = watch->mEvents;
 
         if (AVAHI_WATCH_IN & events)
         {
@@ -333,12 +349,12 @@
 
         aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd);
 
-        (*it)->mHappened = 0;
+        watch->mHappened = 0;
     }
 
-    for (Timers::iterator it = mTimers.begin(); it != mTimers.end(); ++it)
+    for (AvahiTimeout *timer : mTimers)
     {
-        Timepoint timeout = (*it)->mTimeout;
+        Timepoint timeout = timer->mTimeout;
 
         if (timeout == Timepoint::min())
         {
@@ -364,56 +380,92 @@
 
 void AvahiPoller::Process(const MainloopContext &aMainloop)
 {
-    Timepoint                   now = Clock::now();
-    std::vector<AvahiTimeout *> expired;
+    Timepoint now          = Clock::now();
+    bool      shouldReport = false;
 
-    for (Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it)
+    for (AvahiWatch *watch : mWatches)
     {
-        int             fd     = (*it)->mFd;
-        AvahiWatchEvent events = (*it)->mEvents;
+        int             fd     = watch->mFd;
+        AvahiWatchEvent events = watch->mEvents;
 
-        (*it)->mHappened = 0;
+        watch->mHappened = 0;
 
         if ((AVAHI_WATCH_IN & events) && FD_ISSET(fd, &aMainloop.mReadFdSet))
         {
-            (*it)->mHappened |= AVAHI_WATCH_IN;
+            watch->mHappened |= AVAHI_WATCH_IN;
         }
 
         if ((AVAHI_WATCH_OUT & events) && FD_ISSET(fd, &aMainloop.mWriteFdSet))
         {
-            (*it)->mHappened |= AVAHI_WATCH_OUT;
+            watch->mHappened |= AVAHI_WATCH_OUT;
         }
 
         if ((AVAHI_WATCH_ERR & events) && FD_ISSET(fd, &aMainloop.mErrorFdSet))
         {
-            (*it)->mHappened |= AVAHI_WATCH_ERR;
+            watch->mHappened |= AVAHI_WATCH_ERR;
         }
 
-        // TODO hup events
-        if ((*it)->mHappened)
+        if (watch->mHappened != 0)
         {
-            (*it)->mCallback(*it, (*it)->mFd, static_cast<AvahiWatchEvent>((*it)->mHappened), (*it)->mContext);
+            watch->mShouldReport = true;
+            shouldReport         = true;
         }
     }
 
-    for (Timers::iterator it = mTimers.begin(); it != mTimers.end(); ++it)
+    // When we invoke the callback for an `AvahiWatch` or `AvahiTimeout`,
+    // the Avahi module can call any of `mAvahiPoll` APIs we provided to
+    // it. For example, it can update or free any of `AvahiWatch/Timeout`
+    // entries, which in turn, modifies our `mWatches` or `mTimers` list.
+    // So, before invoking the callback, we update the entry's state and
+    // then restart the iteration over the `mWacthes` list to find the
+    // next entry to report, as the list may have changed.
+
+    while (shouldReport)
     {
-        if ((*it)->mTimeout == Timepoint::min())
+        shouldReport = false;
+
+        for (AvahiWatch *watch : mWatches)
+        {
+            if (watch->mShouldReport)
+            {
+                shouldReport         = true;
+                watch->mShouldReport = false;
+                watch->mCallback(watch, watch->mFd, WatchGetEvents(watch), watch->mContext);
+
+                break;
+            }
+        }
+    }
+
+    for (AvahiTimeout *timer : mTimers)
+    {
+        if (timer->mTimeout == Timepoint::min())
         {
             continue;
         }
 
-        if ((*it)->mTimeout <= now)
+        if (timer->mTimeout <= now)
         {
-            expired.push_back(*it);
+            timer->mShouldReport = true;
+            shouldReport         = true;
         }
     }
 
-    for (std::vector<AvahiTimeout *>::iterator it = expired.begin(); it != expired.end(); ++it)
+    while (shouldReport)
     {
-        AvahiTimeout *avahiTimeout = *it;
+        shouldReport = false;
 
-        avahiTimeout->mCallback(avahiTimeout, avahiTimeout->mContext);
+        for (AvahiTimeout *timer : mTimers)
+        {
+            if (timer->mShouldReport)
+            {
+                shouldReport         = true;
+                timer->mShouldReport = false;
+                timer->mCallback(timer, timer->mContext);
+
+                break;
+            }
+        }
     }
 }
 
@@ -639,13 +691,12 @@
                                              const std::string &aType,
                                              const SubTypeList &aSubTypeList,
                                              uint16_t           aPort,
-                                             const TxtList     &aTxtList,
+                                             const TxtData     &aTxtData,
                                              ResultCallback   &&aCallback)
 {
     otbrError         error             = OTBR_ERROR_NONE;
     int               avahiError        = AVAHI_OK;
     SubTypeList       sortedSubTypeList = SortSubTypeList(aSubTypeList);
-    TxtList           sortedTxtList     = SortTxtList(aTxtList);
     const std::string logHostName       = !aHostName.empty() ? aHostName : "localhost";
     std::string       fullHostName;
     std::string       serviceName = aName;
@@ -667,11 +718,11 @@
         serviceName = avahi_client_get_host_name(mClient);
     }
 
-    aCallback = HandleDuplicateServiceRegistration(aHostName, serviceName, aType, sortedSubTypeList, aPort,
-                                                   sortedTxtList, std::move(aCallback));
+    aCallback = HandleDuplicateServiceRegistration(aHostName, serviceName, aType, sortedSubTypeList, aPort, aTxtData,
+                                                   std::move(aCallback));
     VerifyOrExit(!aCallback.IsNull());
 
-    SuccessOrExit(error = TxtListToAvahiStringList(aTxtList, txtBuffer, sizeof(txtBuffer), txtHead));
+    SuccessOrExit(error = TxtDataToAvahiStringList(aTxtData, txtBuffer, sizeof(txtBuffer), txtHead));
     VerifyOrExit((group = CreateGroup(mClient)) != nullptr, error = OTBR_ERROR_MDNS);
     avahiError = avahi_entry_group_add_service_strlst(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags{},
                                                       serviceName.c_str(), aType.c_str(),
@@ -693,7 +744,7 @@
     VerifyOrExit(avahiError == AVAHI_OK);
 
     AddServiceRegistration(std::unique_ptr<AvahiServiceRegistration>(new AvahiServiceRegistration(
-        aHostName, serviceName, aType, sortedSubTypeList, aPort, sortedTxtList, std::move(aCallback), group, this)));
+        aHostName, serviceName, aType, sortedSubTypeList, aPort, aTxtData, std::move(aCallback), group, this)));
 
 exit:
     if (avahiError != AVAHI_OK || error != OTBR_ERROR_NONE)
@@ -724,9 +775,9 @@
     std::move(aCallback)(error);
 }
 
-otbrError PublisherAvahi::PublishHostImpl(const std::string             &aName,
-                                          const std::vector<Ip6Address> &aAddresses,
-                                          ResultCallback               &&aCallback)
+otbrError PublisherAvahi::PublishHostImpl(const std::string &aName,
+                                          const AddressList &aAddresses,
+                                          ResultCallback   &&aCallback)
 {
     otbrError        error      = OTBR_ERROR_NONE;
     int              avahiError = AVAHI_OK;
@@ -790,7 +841,7 @@
     std::move(aCallback)(error);
 }
 
-otbrError PublisherAvahi::TxtListToAvahiStringList(const TxtList    &aTxtList,
+otbrError PublisherAvahi::TxtDataToAvahiStringList(const TxtData    &aTxtData,
                                                    AvahiStringList  *aBuffer,
                                                    size_t            aBufferSize,
                                                    AvahiStringList *&aHead)
@@ -799,32 +850,40 @@
     size_t           used  = 0;
     AvahiStringList *last  = nullptr;
     AvahiStringList *curr  = aBuffer;
+    const uint8_t   *next;
+    const uint8_t   *data    = aTxtData.data();
+    const uint8_t   *dataEnd = aTxtData.data() + aTxtData.size();
 
     aHead = nullptr;
-    for (const auto &txtEntry : aTxtList)
+
+    while (data < dataEnd)
     {
-        const char    *name        = txtEntry.mName.c_str();
-        size_t         nameLength  = txtEntry.mName.length();
-        const uint8_t *value       = txtEntry.mValue.data();
-        size_t         valueLength = txtEntry.mValue.size();
-        // +1 for the size of "=", avahi doesn't need '\0' at the end of the entry
-        size_t needed = sizeof(AvahiStringList) - sizeof(AvahiStringList::text) + nameLength + valueLength + 1;
+        uint8_t entryLength = *data++;
+        size_t  needed      = sizeof(AvahiStringList) - sizeof(AvahiStringList::text) + entryLength;
+
+        if (entryLength == 0)
+        {
+            continue;
+        }
+
+        VerifyOrExit(data + entryLength <= dataEnd, error = OTBR_ERROR_PARSE);
 
         VerifyOrExit(used + needed <= aBufferSize, error = OTBR_ERROR_INVALID_ARGS);
         curr->next = last;
         last       = curr;
-        memcpy(curr->text, name, nameLength);
-        curr->text[nameLength] = '=';
-        memcpy(curr->text + nameLength + 1, value, valueLength);
-        curr->size = nameLength + valueLength + 1;
-        {
-            const uint8_t *next = curr->text + curr->size;
-            curr                = OTBR_ALIGNED(next, AvahiStringList *);
-        }
+
+        memcpy(curr->text, data, entryLength);
+        curr->size = entryLength;
+
+        data += entryLength;
+
+        next = curr->text + curr->size;
+        curr = OTBR_ALIGNED(next, AvahiStringList *);
         used = static_cast<size_t>(reinterpret_cast<uint8_t *>(curr) - reinterpret_cast<uint8_t *>(aBuffer));
     }
-    SuccessOrExit(error);
+
     aHead = last;
+
 exit:
     return error;
 }
@@ -1071,18 +1130,22 @@
                                                   const std::string &aInstanceName,
                                                   const std::string &aType)
 {
-    AvahiServiceResolver *resolver;
+    auto serviceResolver = MakeUnique<ServiceResolver>();
 
     mPublisherAvahi->mServiceInstanceResolutionBeginTime[std::make_pair(aInstanceName, aType)] = Clock::now();
 
     otbrLogInfo("Resolve service %s.%s inf %" PRIu32, aInstanceName.c_str(), aType.c_str(), aInterfaceIndex);
 
-    resolver = avahi_service_resolver_new(
+    serviceResolver->mType            = aType;
+    serviceResolver->mPublisherAvahi  = this->mPublisherAvahi;
+    serviceResolver->mServiceResolver = avahi_service_resolver_new(
         mPublisherAvahi->mClient, aInterfaceIndex, aProtocol, aInstanceName.c_str(), aType.c_str(),
-        /* domain */ nullptr, AVAHI_PROTO_UNSPEC, static_cast<AvahiLookupFlags>(0), HandleResolveResult, this);
-    if (resolver != nullptr)
+        /* domain */ nullptr, AVAHI_PROTO_UNSPEC, static_cast<AvahiLookupFlags>(AVAHI_LOOKUP_NO_ADDRESS),
+        &ServiceResolver::HandleResolveServiceResult, serviceResolver.get());
+
+    if (serviceResolver->mServiceResolver != nullptr)
     {
-        AddServiceResolver(aInstanceName, resolver);
+        AddServiceResolver(aInstanceName, serviceResolver.release());
     }
     else
     {
@@ -1091,106 +1154,179 @@
     }
 }
 
-void PublisherAvahi::ServiceSubscription::HandleResolveResult(AvahiServiceResolver  *aServiceResolver,
-                                                              AvahiIfIndex           aInterfaceIndex,
-                                                              AvahiProtocol          aProtocol,
-                                                              AvahiResolverEvent     aEvent,
-                                                              const char            *aName,
-                                                              const char            *aType,
-                                                              const char            *aDomain,
-                                                              const char            *aHostName,
-                                                              const AvahiAddress    *aAddress,
-                                                              uint16_t               aPort,
-                                                              AvahiStringList       *aTxt,
-                                                              AvahiLookupResultFlags aFlags,
-                                                              void                  *aContext)
+void PublisherAvahi::ServiceResolver::HandleResolveServiceResult(AvahiServiceResolver  *aServiceResolver,
+                                                                 AvahiIfIndex           aInterfaceIndex,
+                                                                 AvahiProtocol          aProtocol,
+                                                                 AvahiResolverEvent     aEvent,
+                                                                 const char            *aName,
+                                                                 const char            *aType,
+                                                                 const char            *aDomain,
+                                                                 const char            *aHostName,
+                                                                 const AvahiAddress    *aAddress,
+                                                                 uint16_t               aPort,
+                                                                 AvahiStringList       *aTxt,
+                                                                 AvahiLookupResultFlags aFlags,
+                                                                 void                  *aContext)
 {
-    static_cast<PublisherAvahi::ServiceSubscription *>(aContext)->HandleResolveResult(
+    static_cast<PublisherAvahi::ServiceResolver *>(aContext)->HandleResolveServiceResult(
         aServiceResolver, aInterfaceIndex, aProtocol, aEvent, aName, aType, aDomain, aHostName, aAddress, aPort, aTxt,
         aFlags);
 }
 
-void PublisherAvahi::ServiceSubscription::HandleResolveResult(AvahiServiceResolver  *aServiceResolver,
-                                                              AvahiIfIndex           aInterfaceIndex,
-                                                              AvahiProtocol          aProtocol,
-                                                              AvahiResolverEvent     aEvent,
-                                                              const char            *aName,
-                                                              const char            *aType,
-                                                              const char            *aDomain,
-                                                              const char            *aHostName,
-                                                              const AvahiAddress    *aAddress,
-                                                              uint16_t               aPort,
-                                                              AvahiStringList       *aTxt,
-                                                              AvahiLookupResultFlags aFlags)
+void PublisherAvahi::ServiceResolver::HandleResolveServiceResult(AvahiServiceResolver  *aServiceResolver,
+                                                                 AvahiIfIndex           aInterfaceIndex,
+                                                                 AvahiProtocol          aProtocol,
+                                                                 AvahiResolverEvent     aEvent,
+                                                                 const char            *aName,
+                                                                 const char            *aType,
+                                                                 const char            *aDomain,
+                                                                 const char            *aHostName,
+                                                                 const AvahiAddress    *aAddress,
+                                                                 uint16_t               aPort,
+                                                                 AvahiStringList       *aTxt,
+                                                                 AvahiLookupResultFlags aFlags)
 {
     OT_UNUSED_VARIABLE(aServiceResolver);
     OT_UNUSED_VARIABLE(aInterfaceIndex);
     OT_UNUSED_VARIABLE(aProtocol);
     OT_UNUSED_VARIABLE(aType);
     OT_UNUSED_VARIABLE(aDomain);
+    OT_UNUSED_VARIABLE(aAddress);
 
-    char                   addrBuf[AVAHI_ADDRESS_STR_MAX] = "";
-    Ip6Address             address;
-    size_t                 totalTxtSize = 0;
-    DiscoveredInstanceInfo instanceInfo;
-    bool                   resolved   = false;
-    int                    avahiError = AVAHI_OK;
+    size_t totalTxtSize = 0;
+    bool   resolved     = false;
+    int    avahiError   = AVAHI_OK;
 
     otbrLog(aEvent == AVAHI_RESOLVER_FOUND ? OTBR_LOG_INFO : OTBR_LOG_WARNING, OTBR_LOG_TAG,
             "Resolve service reply: protocol %d %s.%s.%s = host %s port %" PRIu16 " flags %d event %d", aProtocol,
             aName, aType, aDomain, aHostName, aPort, static_cast<int>(aFlags), static_cast<int>(aEvent));
 
     VerifyOrExit(aEvent == AVAHI_RESOLVER_FOUND, avahiError = avahi_client_errno(mPublisherAvahi->mClient));
-
-    avahi_address_snprint(addrBuf, sizeof(addrBuf), aAddress);
-    otbrLogInfo("Resolve service reply: address %s", addrBuf);
-
     VerifyOrExit(aHostName != nullptr, avahiError = AVAHI_ERR_INVALID_HOST_NAME);
 
-    instanceInfo.mNetifIndex = static_cast<uint32_t>(aInterfaceIndex);
-    instanceInfo.mName       = aName;
-    instanceInfo.mHostName   = std::string(aHostName) + ".";
-    instanceInfo.mPort       = aPort;
-    VerifyOrExit(otbrError::OTBR_ERROR_NONE == Ip6Address::FromString(addrBuf, address),
-                 otbrLogErr("Failed to parse the IP address: %s", addrBuf), avahiError = AVAHI_ERR_INVALID_ADDRESS);
+    mInstanceInfo.mNetifIndex = static_cast<uint32_t>(aInterfaceIndex);
+    mInstanceInfo.mName       = aName;
+    mInstanceInfo.mHostName   = std::string(aHostName) + ".";
+    mInstanceInfo.mPort       = aPort;
 
     otbrLogInfo("Resolve service reply: flags=%u, host=%s", aFlags, aHostName);
 
-    VerifyOrExit(!address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback() && !address.IsUnspecified(),
-                 otbrLogInfo("Ignoring address %s", address.ToString().c_str()),
-                 avahiError = AVAHI_ERR_INVALID_ADDRESS);
-
-    instanceInfo.mAddresses.push_back(address);
-
     // TODO priority
     // TODO weight
     // TODO use a more proper TTL
-    instanceInfo.mTtl = kDefaultTtl;
+    mInstanceInfo.mTtl = kDefaultTtl;
     for (auto p = aTxt; p; p = avahi_string_list_get_next(p))
     {
         totalTxtSize += avahi_string_list_get_size(p) + 1;
     }
-    instanceInfo.mTxtData.resize(totalTxtSize);
-    avahi_string_list_serialize(aTxt, instanceInfo.mTxtData.data(), totalTxtSize);
+    mInstanceInfo.mTxtData.resize(totalTxtSize);
+    avahi_string_list_serialize(aTxt, mInstanceInfo.mTxtData.data(), totalTxtSize);
 
-    otbrLogInfo("Resolve service reply: address=%s, ttl=%" PRIu32, address.ToString().c_str(), instanceInfo.mTtl);
+    // NOTE: Avahi only returns one of the host's addresses in the service resolution callback. However, the address may
+    // be link-local so it may not be preferred from Thread's perspective. We want to go through the complete list of
+    // addresses associated with the host and choose a routable address. Therefore, as below we will resolve the host
+    // and go through all its addresses.
 
     resolved = true;
 
 exit:
     if (resolved)
     {
-        // NOTE: This `ServiceSubscrption` object may be freed in `OnServiceResolved`.
-        mPublisherAvahi->OnServiceResolved(mType, instanceInfo);
+        // In case the callback is triggered when a service instance is updated, there may already be a record browser.
+        // We should free it before switching to the new record browser.
+        if (mRecordBrowser)
+        {
+            avahi_record_browser_free(mRecordBrowser);
+            mRecordBrowser = nullptr;
+        }
+        // NOTE: This `ServiceResolver` object may be freed in `OnServiceResolved`.
+        mRecordBrowser = avahi_record_browser_new(mPublisherAvahi->mClient, aInterfaceIndex, AVAHI_PROTO_UNSPEC,
+                                                  aHostName, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_AAAA,
+                                                  static_cast<AvahiLookupFlags>(0), HandleResolveHostResult, this);
+        if (!mRecordBrowser)
+        {
+            resolved   = false;
+            avahiError = avahi_client_errno(mPublisherAvahi->mClient);
+        }
     }
-    else if (avahiError != AVAHI_OK)
+    if (!resolved && avahiError != AVAHI_OK)
     {
         mPublisherAvahi->OnServiceResolveFailed(aType, aName, avahiError);
     }
 }
 
-void PublisherAvahi::ServiceSubscription::AddServiceResolver(const std::string    &aInstanceName,
-                                                             AvahiServiceResolver *aServiceResolver)
+void PublisherAvahi::ServiceResolver::HandleResolveHostResult(AvahiRecordBrowser    *aRecordBrowser,
+                                                              AvahiIfIndex           aInterfaceIndex,
+                                                              AvahiProtocol          aProtocol,
+                                                              AvahiBrowserEvent      aEvent,
+                                                              const char            *aName,
+                                                              uint16_t               aClazz,
+                                                              uint16_t               aType,
+                                                              const void            *aRdata,
+                                                              size_t                 aSize,
+                                                              AvahiLookupResultFlags aFlags,
+                                                              void                  *aContext)
+{
+    static_cast<PublisherAvahi::ServiceResolver *>(aContext)->HandleResolveHostResult(
+        aRecordBrowser, aInterfaceIndex, aProtocol, aEvent, aName, aClazz, aType, aRdata, aSize, aFlags);
+}
+
+void PublisherAvahi::ServiceResolver::HandleResolveHostResult(AvahiRecordBrowser    *aRecordBrowser,
+                                                              AvahiIfIndex           aInterfaceIndex,
+                                                              AvahiProtocol          aProtocol,
+                                                              AvahiBrowserEvent      aEvent,
+                                                              const char            *aName,
+                                                              uint16_t               aClazz,
+                                                              uint16_t               aType,
+                                                              const void            *aRdata,
+                                                              size_t                 aSize,
+                                                              AvahiLookupResultFlags aFlags)
+{
+    OTBR_UNUSED_VARIABLE(aRecordBrowser);
+    OTBR_UNUSED_VARIABLE(aInterfaceIndex);
+    OTBR_UNUSED_VARIABLE(aProtocol);
+    OTBR_UNUSED_VARIABLE(aEvent);
+    OTBR_UNUSED_VARIABLE(aClazz);
+    OTBR_UNUSED_VARIABLE(aType);
+    OTBR_UNUSED_VARIABLE(aFlags);
+
+    Ip6Address address;
+    bool       resolved   = false;
+    int        avahiError = AVAHI_OK;
+
+    otbrLog(aEvent != AVAHI_BROWSER_FAILURE ? OTBR_LOG_INFO : OTBR_LOG_WARNING, OTBR_LOG_TAG,
+            "Resolve host reply: %s inf %d protocol %d class %" PRIu16 " type %" PRIu16 " size %zu flags %d event %d",
+            aName, aInterfaceIndex, aProtocol, aClazz, aType, aSize, static_cast<int>(aFlags),
+            static_cast<int>(aEvent));
+
+    VerifyOrExit(aEvent == AVAHI_BROWSER_NEW);
+    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"),
+                 avahiError = AVAHI_ERR_INVALID_ADDRESS);
+    address = Ip6Address(*static_cast<const uint8_t(*)[OTBR_IP6_ADDRESS_SIZE]>(aRdata));
+
+    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));
+    resolved = true;
+
+exit:
+    if (resolved)
+    {
+        // NOTE: This `HostSubscrption` object may be freed in `OnHostResolved`.
+        mPublisherAvahi->OnServiceResolved(mType, mInstanceInfo);
+    }
+    else if (avahiError != AVAHI_OK)
+    {
+        mPublisherAvahi->OnServiceResolveFailed(mType, mInstanceInfo.mName, avahiError);
+    }
+}
+
+void PublisherAvahi::ServiceSubscription::AddServiceResolver(const std::string &aInstanceName,
+                                                             ServiceResolver   *aServiceResolver)
 {
     assert(aServiceResolver != nullptr);
     mServiceResolvers[aInstanceName].insert(aServiceResolver);
@@ -1208,7 +1344,7 @@
 
     for (auto resolver : mServiceResolvers[aInstanceName])
     {
-        avahi_service_resolver_free(resolver);
+        delete resolver;
     }
 
     mServiceResolvers.erase(aInstanceName);
diff --git a/src/mdns/mdns_avahi.hpp b/src/mdns/mdns_avahi.hpp
index 1704f76..844e242 100644
--- a/src/mdns/mdns_avahi.hpp
+++ b/src/mdns/mdns_avahi.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_MDNS_AVAHI_HPP_
 #define OTBR_AGENT_MDNS_AVAHI_HPP_
 
+#include "openthread-br/config.h"
+
 #include <memory>
 #include <set>
 #include <vector>
@@ -90,11 +92,11 @@
                                  const std::string &aType,
                                  const SubTypeList &aSubTypeList,
                                  uint16_t           aPort,
-                                 const TxtList     &aTxtList,
+                                 const TxtData     &aTxtData,
                                  ResultCallback   &&aCallback) override;
-    otbrError PublishHostImpl(const std::string             &aName,
-                              const std::vector<Ip6Address> &aAddresses,
-                              ResultCallback               &&aCallback) override;
+    otbrError PublishHostImpl(const std::string &aName,
+                              const AddressList &aAddresses,
+                              ResultCallback   &&aCallback) override;
     void      OnServiceResolveFailedImpl(const std::string &aType,
                                          const std::string &aInstanceName,
                                          int32_t            aErrorCode) override;
@@ -113,7 +115,7 @@
                                  const std::string &aType,
                                  const SubTypeList &aSubTypeList,
                                  uint16_t           aPort,
-                                 const TxtList     &aTxtList,
+                                 const TxtData     &aTxtData,
                                  ResultCallback   &&aCallback,
                                  AvahiEntryGroup   *aEntryGroup,
                                  PublisherAvahi    *aPublisher)
@@ -122,7 +124,7 @@
                                   aType,
                                   aSubTypeList,
                                   aPort,
-                                  aTxtList,
+                                  aTxtData,
                                   std::move(aCallback),
                                   aPublisher)
             , mEntryGroup(aEntryGroup)
@@ -139,11 +141,11 @@
     class AvahiHostRegistration : public HostRegistration
     {
     public:
-        AvahiHostRegistration(const std::string             &aName,
-                              const std::vector<Ip6Address> &aAddresses,
-                              ResultCallback               &&aCallback,
-                              AvahiEntryGroup               *aEntryGroup,
-                              PublisherAvahi                *aPublisher)
+        AvahiHostRegistration(const std::string &aName,
+                              const AddressList &aAddresses,
+                              ResultCallback   &&aCallback,
+                              AvahiEntryGroup   *aEntryGroup,
+                              PublisherAvahi    *aPublisher)
             : HostRegistration(aName, aAddresses, std::move(aCallback), aPublisher)
             , mEntryGroup(aEntryGroup)
         {
@@ -166,81 +168,6 @@
         }
     };
 
-    struct ServiceSubscription : public Subscription
-    {
-        explicit ServiceSubscription(PublisherAvahi &aPublisherAvahi, std::string aType, std::string aInstanceName)
-            : Subscription(aPublisherAvahi)
-            , mType(std::move(aType))
-            , mInstanceName(std::move(aInstanceName))
-            , mServiceBrowser(nullptr)
-        {
-        }
-
-        ~ServiceSubscription() { Release(); }
-
-        void Release(void);
-        void Browse(void);
-        void Resolve(uint32_t           aInterfaceIndex,
-                     AvahiProtocol      aProtocol,
-                     const std::string &aInstanceName,
-                     const std::string &aType);
-        void AddServiceResolver(const std::string &aInstanceName, AvahiServiceResolver *aServiceResolver);
-        void RemoveServiceResolver(const std::string &aInstanceName);
-
-        static void HandleBrowseResult(AvahiServiceBrowser   *aServiceBrowser,
-                                       AvahiIfIndex           aInterfaceIndex,
-                                       AvahiProtocol          aProtocol,
-                                       AvahiBrowserEvent      aEvent,
-                                       const char            *aName,
-                                       const char            *aType,
-                                       const char            *aDomain,
-                                       AvahiLookupResultFlags aFlags,
-                                       void                  *aContext);
-
-        void HandleBrowseResult(AvahiServiceBrowser   *aServiceBrowser,
-                                AvahiIfIndex           aInterfaceIndex,
-                                AvahiProtocol          aProtocol,
-                                AvahiBrowserEvent      aEvent,
-                                const char            *aName,
-                                const char            *aType,
-                                const char            *aDomain,
-                                AvahiLookupResultFlags aFlags);
-
-        static void HandleResolveResult(AvahiServiceResolver  *aServiceResolver,
-                                        AvahiIfIndex           aInterfaceIndex,
-                                        AvahiProtocol          Protocol,
-                                        AvahiResolverEvent     aEvent,
-                                        const char            *aName,
-                                        const char            *aType,
-                                        const char            *aDomain,
-                                        const char            *aHostName,
-                                        const AvahiAddress    *aAddress,
-                                        uint16_t               aPort,
-                                        AvahiStringList       *aTxt,
-                                        AvahiLookupResultFlags aFlags,
-                                        void                  *aContext);
-
-        void HandleResolveResult(AvahiServiceResolver  *aServiceResolver,
-                                 AvahiIfIndex           aInterfaceIndex,
-                                 AvahiProtocol          Protocol,
-                                 AvahiResolverEvent     aEvent,
-                                 const char            *aName,
-                                 const char            *aType,
-                                 const char            *aDomain,
-                                 const char            *aHostName,
-                                 const AvahiAddress    *aAddress,
-                                 uint16_t               aPort,
-                                 AvahiStringList       *aTxt,
-                                 AvahiLookupResultFlags aFlags);
-
-        std::string          mType;
-        std::string          mInstanceName;
-        AvahiServiceBrowser *mServiceBrowser;
-
-        using ServiceResolversMap = std::map<std::string, std::set<AvahiServiceResolver *>>;
-        ServiceResolversMap mServiceResolvers;
-    };
-
     struct HostSubscription : public Subscription
     {
         explicit HostSubscription(PublisherAvahi &aAvahiPublisher, std::string aHostName)
@@ -282,6 +209,124 @@
         AvahiRecordBrowser *mRecordBrowser;
     };
 
+    struct ServiceResolver
+    {
+        ~ServiceResolver()
+        {
+            if (mServiceResolver)
+            {
+                avahi_service_resolver_free(mServiceResolver);
+            }
+            if (mRecordBrowser)
+            {
+                avahi_record_browser_free(mRecordBrowser);
+            }
+        }
+
+        static void HandleResolveServiceResult(AvahiServiceResolver  *aServiceResolver,
+                                               AvahiIfIndex           aInterfaceIndex,
+                                               AvahiProtocol          Protocol,
+                                               AvahiResolverEvent     aEvent,
+                                               const char            *aName,
+                                               const char            *aType,
+                                               const char            *aDomain,
+                                               const char            *aHostName,
+                                               const AvahiAddress    *aAddress,
+                                               uint16_t               aPort,
+                                               AvahiStringList       *aTxt,
+                                               AvahiLookupResultFlags aFlags,
+                                               void                  *aContext);
+
+        void HandleResolveServiceResult(AvahiServiceResolver  *aServiceResolver,
+                                        AvahiIfIndex           aInterfaceIndex,
+                                        AvahiProtocol          Protocol,
+                                        AvahiResolverEvent     aEvent,
+                                        const char            *aName,
+                                        const char            *aType,
+                                        const char            *aDomain,
+                                        const char            *aHostName,
+                                        const AvahiAddress    *aAddress,
+                                        uint16_t               aPort,
+                                        AvahiStringList       *aTxt,
+                                        AvahiLookupResultFlags aFlags);
+
+        static void HandleResolveHostResult(AvahiRecordBrowser    *aRecordBrowser,
+                                            AvahiIfIndex           aInterfaceIndex,
+                                            AvahiProtocol          aProtocol,
+                                            AvahiBrowserEvent      aEvent,
+                                            const char            *aName,
+                                            uint16_t               aClazz,
+                                            uint16_t               aType,
+                                            const void            *aRdata,
+                                            size_t                 aSize,
+                                            AvahiLookupResultFlags aFlags,
+                                            void                  *aContext);
+
+        void HandleResolveHostResult(AvahiRecordBrowser    *aRecordBrowser,
+                                     AvahiIfIndex           aInterfaceIndex,
+                                     AvahiProtocol          aProtocol,
+                                     AvahiBrowserEvent      aEvent,
+                                     const char            *aName,
+                                     uint16_t               aClazz,
+                                     uint16_t               aType,
+                                     const void            *aRdata,
+                                     size_t                 aSize,
+                                     AvahiLookupResultFlags aFlags);
+
+        std::string            mType;
+        PublisherAvahi        *mPublisherAvahi;
+        AvahiServiceResolver  *mServiceResolver = nullptr;
+        AvahiRecordBrowser    *mRecordBrowser   = nullptr;
+        DiscoveredInstanceInfo mInstanceInfo;
+    };
+    struct ServiceSubscription : public Subscription
+    {
+        explicit ServiceSubscription(PublisherAvahi &aPublisherAvahi, std::string aType, std::string aInstanceName)
+            : Subscription(aPublisherAvahi)
+            , mType(std::move(aType))
+            , mInstanceName(std::move(aInstanceName))
+            , mServiceBrowser(nullptr)
+        {
+        }
+
+        ~ServiceSubscription() { Release(); }
+
+        void Release(void);
+        void Browse(void);
+        void Resolve(uint32_t           aInterfaceIndex,
+                     AvahiProtocol      aProtocol,
+                     const std::string &aInstanceName,
+                     const std::string &aType);
+        void AddServiceResolver(const std::string &aInstanceName, ServiceResolver *aServiceResolver);
+        void RemoveServiceResolver(const std::string &aInstanceName);
+
+        static void HandleBrowseResult(AvahiServiceBrowser   *aServiceBrowser,
+                                       AvahiIfIndex           aInterfaceIndex,
+                                       AvahiProtocol          aProtocol,
+                                       AvahiBrowserEvent      aEvent,
+                                       const char            *aName,
+                                       const char            *aType,
+                                       const char            *aDomain,
+                                       AvahiLookupResultFlags aFlags,
+                                       void                  *aContext);
+
+        void HandleBrowseResult(AvahiServiceBrowser   *aServiceBrowser,
+                                AvahiIfIndex           aInterfaceIndex,
+                                AvahiProtocol          aProtocol,
+                                AvahiBrowserEvent      aEvent,
+                                const char            *aName,
+                                const char            *aType,
+                                const char            *aDomain,
+                                AvahiLookupResultFlags aFlags);
+
+        std::string          mType;
+        std::string          mInstanceName;
+        AvahiServiceBrowser *mServiceBrowser;
+
+        using ServiceResolversMap = std::map<std::string, std::set<ServiceResolver *>>;
+        ServiceResolversMap mServiceResolvers;
+    };
+
     typedef std::vector<std::unique_ptr<ServiceSubscription>> ServiceSubscriptionList;
     typedef std::vector<std::unique_ptr<HostSubscription>>    HostSubscriptionList;
 
@@ -295,7 +340,7 @@
     void        HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState);
     void        CallHostOrServiceCallback(AvahiEntryGroup *aGroup, otbrError aError);
 
-    static otbrError TxtListToAvahiStringList(const TxtList    &aTxtList,
+    static otbrError TxtDataToAvahiStringList(const TxtData    &aTxtData,
                                               AvahiStringList  *aBuffer,
                                               size_t            aBufferSize,
                                               AvahiStringList *&aHead);
diff --git a/src/mdns/mdns_mdnssd.cpp b/src/mdns/mdns_mdnssd.cpp
index f95d2f5..5a07559 100644
--- a/src/mdns/mdns_mdnssd.cpp
+++ b/src/mdns/mdns_mdnssd.cpp
@@ -222,7 +222,7 @@
 
 PublisherMDnsSd::~PublisherMDnsSd(void)
 {
-    Stop();
+    Stop(kNormalStop);
 }
 
 otbrError PublisherMDnsSd::Start(void)
@@ -237,25 +237,31 @@
     return mState == State::kReady;
 }
 
-void PublisherMDnsSd::Stop(void)
+void PublisherMDnsSd::Stop(StopMode aStopMode)
 {
-    ServiceRegistrationMap serviceRegistrations;
-    HostRegistrationMap    hostRegistrations;
-
     VerifyOrExit(mState == State::kReady);
 
-    std::swap(mServiceRegistrations, serviceRegistrations);
-    std::swap(mHostRegistrations, hostRegistrations);
+    // If we get a `kDNSServiceErr_ServiceNotRunning` and need to
+    // restart the `Publisher`, we should immediately de-allocate
+    // all `ServiceRef`. Otherwise, we first clear the `Registrations`
+    // list so that `DnssdHostRegisteration` destructor gets the chance
+    // to update registered records if needed.
 
-    if (mHostsRef != nullptr)
+    switch (aStopMode)
     {
-        DNSServiceRefDeallocate(mHostsRef);
-        otbrLogDebug("Deallocated DNSServiceRef for hosts: %p", mHostsRef);
-        mHostsRef = nullptr;
+    case kNormalStop:
+        break;
+
+    case kStopOnServiceNotRunningError:
+        DeallocateHostsRef();
+        break;
     }
 
-    mSubscribedServices.clear();
+    mServiceRegistrations.clear();
+    mHostRegistrations.clear();
+    DeallocateHostsRef();
 
+    mSubscribedServices.clear();
     mSubscribedHosts.clear();
 
     mState = State::kIdle;
@@ -264,21 +270,39 @@
     return;
 }
 
+DNSServiceErrorType PublisherMDnsSd::CreateSharedHostsRef(void)
+{
+    DNSServiceErrorType dnsError = kDNSServiceErr_NoError;
+
+    VerifyOrExit(mHostsRef == nullptr);
+
+    dnsError = DNSServiceCreateConnection(&mHostsRef);
+    otbrLogDebug("Created new shared DNSServiceRef: %p", mHostsRef);
+
+exit:
+    return dnsError;
+}
+
+void PublisherMDnsSd::DeallocateHostsRef(void)
+{
+    VerifyOrExit(mHostsRef != nullptr);
+
+    HandleServiceRefDeallocating(mHostsRef);
+    DNSServiceRefDeallocate(mHostsRef);
+    otbrLogDebug("Deallocated DNSServiceRef for hosts: %p", mHostsRef);
+    mHostsRef = nullptr;
+
+exit:
+    return;
+}
+
 void PublisherMDnsSd::Update(MainloopContext &aMainloop)
 {
     for (auto &kv : mServiceRegistrations)
     {
         auto &serviceReg = static_cast<DnssdServiceRegistration &>(*kv.second);
 
-        assert(serviceReg.GetServiceRef() != nullptr);
-
-        int fd = DNSServiceRefSockFD(serviceReg.GetServiceRef());
-
-        if (fd != -1)
-        {
-            FD_SET(fd, &aMainloop.mReadFdSet);
-            aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd);
-        }
+        serviceReg.Update(aMainloop);
     }
 
     if (mHostsRef != nullptr)
@@ -305,17 +329,13 @@
 
 void PublisherMDnsSd::Process(const MainloopContext &aMainloop)
 {
-    std::vector<DNSServiceRef> readyServices;
+    mServiceRefsToProcess.clear();
 
     for (auto &kv : mServiceRegistrations)
     {
         auto &serviceReg = static_cast<DnssdServiceRegistration &>(*kv.second);
-        int   fd         = DNSServiceRefSockFD(serviceReg.GetServiceRef());
 
-        if (FD_ISSET(fd, &aMainloop.mReadFdSet))
-        {
-            readyServices.push_back(serviceReg.GetServiceRef());
-        }
+        serviceReg.Process(aMainloop, mServiceRefsToProcess);
     }
 
     if (mHostsRef != nullptr)
@@ -324,23 +344,41 @@
 
         if (FD_ISSET(fd, &aMainloop.mReadFdSet))
         {
-            readyServices.push_back(mHostsRef);
+            mServiceRefsToProcess.push_back(mHostsRef);
         }
     }
 
     for (const auto &service : mSubscribedServices)
     {
-        service->ProcessAll(aMainloop, readyServices);
+        service->ProcessAll(aMainloop, mServiceRefsToProcess);
     }
 
     for (const auto &host : mSubscribedHosts)
     {
-        host->Process(aMainloop, readyServices);
+        host->Process(aMainloop, mServiceRefsToProcess);
     }
 
-    for (DNSServiceRef serviceRef : readyServices)
+    for (DNSServiceRef serviceRef : mServiceRefsToProcess)
     {
-        DNSServiceErrorType error = DNSServiceProcessResult(serviceRef);
+        DNSServiceErrorType error;
+
+        // As we go through the list of `mServiceRefsToProcess` the call
+        // to `DNSServiceProcessResult()` can itself invoke callbacks
+        // into `PublisherMDnsSd` and OT, which in turn, may change the
+        // state of `Publisher` and potentially trigger a previously
+        // valid `ServiceRef` in the list to be deallocated. We use
+        // `HandleServiceRefDeallocating()` which is called whenever a
+        // `ServiceRef` is being deallocated and from this we update
+        // the entry in `mServiceRefsToProcess` list to `nullptr` so to
+        // avoid calling `DNSServiceProcessResult()` on an already
+        // freed `ServiceRef`.
+
+        if (serviceRef == nullptr)
+        {
+            continue;
+        }
+
+        error = DNSServiceProcessResult(serviceRef);
 
         if (error != kDNSServiceErr_NoError)
         {
@@ -351,7 +389,7 @@
         if (error == kDNSServiceErr_ServiceNotRunning)
         {
             otbrLogWarning("Need to reconnect to mdnsd");
-            Stop();
+            Stop(kStopOnServiceNotRunningError);
             Start();
             ExitNow();
         }
@@ -360,25 +398,167 @@
     return;
 }
 
-PublisherMDnsSd::DnssdServiceRegistration::~DnssdServiceRegistration(void)
+void PublisherMDnsSd::HandleServiceRefDeallocating(const DNSServiceRef &aServiceRef)
 {
-    if (mServiceRef != nullptr)
+    for (DNSServiceRef &entry : mServiceRefsToProcess)
     {
-        DNSServiceRefDeallocate(mServiceRef);
+        if (entry == aServiceRef)
+        {
+            entry = nullptr;
+        }
     }
 }
 
-PublisherMDnsSd::DnssdHostRegistration::~DnssdHostRegistration(void)
+void PublisherMDnsSd::DnssdServiceRegistration::Update(MainloopContext &aMainloop) const
 {
-    int dnsError;
+    int fd;
 
     VerifyOrExit(mServiceRef != nullptr);
 
-    for (const auto &recordRefAndAddress : GetRecordRefMap())
+    fd = DNSServiceRefSockFD(mServiceRef);
+    VerifyOrExit(fd != -1);
+
+    FD_SET(fd, &aMainloop.mReadFdSet);
+    aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd);
+
+exit:
+    return;
+}
+
+void PublisherMDnsSd::DnssdServiceRegistration::Process(const MainloopContext      &aMainloop,
+                                                        std::vector<DNSServiceRef> &aReadyServices) const
+{
+    int fd;
+
+    VerifyOrExit(mServiceRef != nullptr);
+
+    fd = DNSServiceRefSockFD(mServiceRef);
+    VerifyOrExit(fd != -1);
+
+    VerifyOrExit(FD_ISSET(fd, &aMainloop.mReadFdSet));
+    aReadyServices.push_back(mServiceRef);
+
+exit:
+    return;
+}
+
+otbrError PublisherMDnsSd::DnssdServiceRegistration::Register(void)
+{
+    std::string         fullHostName;
+    std::string         regType            = MakeRegType(mType, mSubTypeList);
+    const char         *hostNameCString    = nullptr;
+    const char         *serviceNameCString = nullptr;
+    DNSServiceErrorType dnsError;
+
+    if (!mHostName.empty())
     {
-        const DNSRecordRef &recordRef = recordRefAndAddress.first;
-        const Ip6Address   &address   = recordRefAndAddress.second;
-        if (IsCompleted())
+        fullHostName    = MakeFullHostName(mHostName);
+        hostNameCString = fullHostName.c_str();
+    }
+
+    if (!mName.empty())
+    {
+        serviceNameCString = mName.c_str();
+    }
+
+    otbrLogInfo("Registering service %s.%s", mName.c_str(), regType.c_str());
+
+    dnsError = DNSServiceRegister(&mServiceRef, kDNSServiceFlagsNoAutoRename, kDNSServiceInterfaceIndexAny,
+                                  serviceNameCString, regType.c_str(),
+                                  /* domain */ nullptr, hostNameCString, htons(mPort), mTxtData.size(), mTxtData.data(),
+                                  HandleRegisterResult, this);
+
+    if (dnsError != kDNSServiceErr_NoError)
+    {
+        HandleRegisterResult(/* aFlags */ 0, dnsError);
+    }
+
+    return GetPublisher().DnsErrorToOtbrError(dnsError);
+}
+
+void PublisherMDnsSd::DnssdServiceRegistration::Unregister(void)
+{
+    if (mServiceRef != nullptr)
+    {
+        GetPublisher().HandleServiceRefDeallocating(mServiceRef);
+        DNSServiceRefDeallocate(mServiceRef);
+        mServiceRef = nullptr;
+    }
+}
+
+void PublisherMDnsSd::DnssdServiceRegistration::HandleRegisterResult(DNSServiceRef       aServiceRef,
+                                                                     DNSServiceFlags     aFlags,
+                                                                     DNSServiceErrorType aError,
+                                                                     const char         *aName,
+                                                                     const char         *aType,
+                                                                     const char         *aDomain,
+                                                                     void               *aContext)
+{
+    OTBR_UNUSED_VARIABLE(aServiceRef);
+    OTBR_UNUSED_VARIABLE(aName);
+    OTBR_UNUSED_VARIABLE(aType);
+    OTBR_UNUSED_VARIABLE(aDomain);
+
+    static_cast<DnssdServiceRegistration *>(aContext)->HandleRegisterResult(aFlags, aError);
+}
+
+void PublisherMDnsSd::DnssdServiceRegistration::HandleRegisterResult(DNSServiceFlags aFlags, DNSServiceErrorType aError)
+{
+    if ((aError == kDNSServiceErr_NoError) && (aFlags & kDNSServiceFlagsAdd))
+    {
+        otbrLogInfo("Successfully registered service %s.%s", mName.c_str(), mType.c_str());
+        Complete(OTBR_ERROR_NONE);
+    }
+    else
+    {
+        otbrLogErr("Failed to register service %s.%s: %s", mName.c_str(), mType.c_str(), DNSErrorToString(aError));
+        GetPublisher().RemoveServiceRegistration(mName, mType, DNSErrorToOtbrError(aError));
+    }
+}
+
+otbrError PublisherMDnsSd::DnssdHostRegistration::Register(void)
+{
+    DNSServiceErrorType dnsError = kDNSServiceErr_NoError;
+
+    otbrLogInfo("Registering new host %s", mName.c_str());
+
+    for (const Ip6Address &address : mAddresses)
+    {
+        DNSRecordRef recordRef = nullptr;
+
+        dnsError = GetPublisher().CreateSharedHostsRef();
+        VerifyOrExit(dnsError == kDNSServiceErr_NoError);
+
+        dnsError = DNSServiceRegisterRecord(GetPublisher().mHostsRef, &recordRef, kDNSServiceFlagsShared,
+                                            kDNSServiceInterfaceIndexAny, MakeFullHostName(mName).c_str(),
+                                            kDNSServiceType_AAAA, kDNSServiceClass_IN, sizeof(address.m8), address.m8,
+                                            /* ttl */ 0, HandleRegisterResult, this);
+        VerifyOrExit(dnsError == kDNSServiceErr_NoError);
+
+        mAddrRecordRefs.push_back(recordRef);
+        mAddrRegistered.push_back(false);
+    }
+
+exit:
+    if ((dnsError != kDNSServiceErr_NoError) || mAddresses.empty())
+    {
+        HandleRegisterResult(/* aRecordRef */ nullptr, dnsError);
+    }
+
+    return GetPublisher().DnsErrorToOtbrError(dnsError);
+}
+
+void PublisherMDnsSd::DnssdHostRegistration::Unregister(void)
+{
+    DNSServiceErrorType dnsError;
+
+    VerifyOrExit(GetPublisher().mHostsRef != nullptr);
+
+    for (size_t index = 0; index < mAddrRecordRefs.size(); index++)
+    {
+        const Ip6Address &address = mAddresses[index];
+
+        if (mAddrRegistered[index])
         {
             // The Bonjour mDNSResponder somehow doesn't send goodbye message for the AAAA record when it is
             // removed by `DNSServiceRemoveRecord`. Per RFC 6762, a goodbye message of a record sets its TTL
@@ -386,103 +566,67 @@
             // we remove the AAAA record after updating its TTL to 1 second. This has the same effect as
             // sending a goodbye message.
             // TODO: resolve the goodbye issue with Bonjour mDNSResponder.
-            dnsError = DNSServiceUpdateRecord(mServiceRef, recordRef, kDNSServiceFlagsUnique, sizeof(address.m8),
-                                              address.m8, /* ttl */ 1);
+            dnsError = DNSServiceUpdateRecord(GetPublisher().mHostsRef, mAddrRecordRefs[index], kDNSServiceFlagsUnique,
+                                              sizeof(address.m8), address.m8, /* ttl */ 1);
             otbrLogResult(DNSErrorToOtbrError(dnsError), "Send goodbye message for host %s address %s: %s",
                           MakeFullHostName(mName).c_str(), address.ToString().c_str(), DNSErrorToString(dnsError));
         }
-        dnsError = DNSServiceRemoveRecord(mServiceRef, recordRef, /* flags */ 0);
+
+        dnsError = DNSServiceRemoveRecord(GetPublisher().mHostsRef, mAddrRecordRefs[index], /* flags */ 0);
+
         otbrLogResult(DNSErrorToOtbrError(dnsError), "Remove record for host %s address %s: %s",
                       MakeFullHostName(mName).c_str(), address.ToString().c_str(), DNSErrorToString(dnsError));
-        // TODO: ?
-        // DNSRecordRefDeallocate(recordRef);
     }
 
 exit:
-    return;
+    mAddrRegistered.clear();
+    mAddrRecordRefs.clear();
 }
 
-Publisher::ServiceRegistration *PublisherMDnsSd::FindServiceRegistration(const DNSServiceRef &aServiceRef)
+void PublisherMDnsSd::DnssdHostRegistration::HandleRegisterResult(DNSServiceRef       aServiceRef,
+                                                                  DNSRecordRef        aRecordRef,
+                                                                  DNSServiceFlags     aFlags,
+                                                                  DNSServiceErrorType aError,
+                                                                  void               *aContext)
 {
-    ServiceRegistration *result = nullptr;
+    OT_UNUSED_VARIABLE(aServiceRef);
+    OT_UNUSED_VARIABLE(aFlags);
 
-    for (auto &kv : mServiceRegistrations)
+    static_cast<DnssdHostRegistration *>(aContext)->HandleRegisterResult(aRecordRef, aError);
+}
+
+void PublisherMDnsSd::DnssdHostRegistration::HandleRegisterResult(DNSRecordRef aRecordRef, DNSServiceErrorType aError)
+{
+    if (aError != kDNSServiceErr_NoError)
     {
-        // We are sure that the service registrations must be instances of `DnssdServiceRegistration`.
-        auto &serviceReg = static_cast<DnssdServiceRegistration &>(*kv.second);
-
-        if (serviceReg.GetServiceRef() == aServiceRef)
-        {
-            result = kv.second.get();
-            break;
-        }
-    }
-
-    return result;
-}
-
-Publisher::HostRegistration *PublisherMDnsSd::FindHostRegistration(const DNSServiceRef &aServiceRef,
-                                                                   const DNSRecordRef  &aRecordRef)
-{
-    HostRegistration *result = nullptr;
-
-    for (auto &kv : mHostRegistrations)
-    {
-        // We are sure that the host registrations must be instances of `DnssdHostRegistration`.
-        auto &hostReg = static_cast<DnssdHostRegistration &>(*kv.second);
-
-        if (hostReg.GetServiceRef() == aServiceRef && hostReg.GetRecordRefMap().count(aRecordRef))
-        {
-            result = kv.second.get();
-            break;
-        }
-    }
-
-    return result;
-}
-
-void PublisherMDnsSd::HandleServiceRegisterResult(DNSServiceRef         aService,
-                                                  const DNSServiceFlags aFlags,
-                                                  DNSServiceErrorType   aError,
-                                                  const char           *aName,
-                                                  const char           *aType,
-                                                  const char           *aDomain,
-                                                  void                 *aContext)
-{
-    static_cast<PublisherMDnsSd *>(aContext)->HandleServiceRegisterResult(aService, aFlags, aError, aName, aType,
-                                                                          aDomain);
-}
-
-void PublisherMDnsSd::HandleServiceRegisterResult(DNSServiceRef         aServiceRef,
-                                                  const DNSServiceFlags aFlags,
-                                                  DNSServiceErrorType   aError,
-                                                  const char           *aName,
-                                                  const char           *aType,
-                                                  const char           *aDomain)
-{
-    OTBR_UNUSED_VARIABLE(aDomain);
-
-    otbrError            error      = DNSErrorToOtbrError(aError);
-    ServiceRegistration *serviceReg = FindServiceRegistration(aServiceRef);
-    serviceReg->mName               = aName;
-
-    otbrLogInfo("Received reply for service %s.%s, serviceRef = %p", aName, aType, aServiceRef);
-
-    VerifyOrExit(serviceReg != nullptr);
-
-    if (aError == kDNSServiceErr_NoError && (aFlags & kDNSServiceFlagsAdd))
-    {
-        otbrLogInfo("Successfully registered service %s.%s", aName, aType);
-        serviceReg->Complete(OTBR_ERROR_NONE);
+        otbrLogErr("Failed to register host %s: %s", mName.c_str(), DNSErrorToString(aError));
+        GetPublisher().RemoveHostRegistration(mName, DNSErrorToOtbrError(aError));
     }
     else
     {
-        otbrLogErr("Failed to register service %s.%s: %s", aName, aType, DNSErrorToString(aError));
-        RemoveServiceRegistration(serviceReg->mName, serviceReg->mType, error);
-    }
+        bool shouldComplete = !IsCompleted();
 
-exit:
-    return;
+        for (size_t index = 0; index < mAddrRecordRefs.size(); index++)
+        {
+            if ((mAddrRecordRefs[index] == aRecordRef) && !mAddrRegistered[index])
+            {
+                mAddrRegistered[index] = true;
+                otbrLogInfo("Successfully registered host %s address %s", mName.c_str(),
+                            mAddresses[index].ToString().c_str());
+            }
+
+            if (!mAddrRegistered[index])
+            {
+                shouldComplete = false;
+            }
+        }
+
+        if (shouldComplete)
+        {
+            otbrLogInfo("Successfully registered all host %s addresses", mName.c_str());
+            Complete(OTBR_ERROR_NONE);
+        }
+    }
 }
 
 otbrError PublisherMDnsSd::PublishServiceImpl(const std::string &aHostName,
@@ -490,62 +634,33 @@
                                               const std::string &aType,
                                               const SubTypeList &aSubTypeList,
                                               uint16_t           aPort,
-                                              const TxtList     &aTxtList,
+                                              const TxtData     &aTxtData,
                                               ResultCallback   &&aCallback)
 {
-    otbrError            ret   = OTBR_ERROR_NONE;
-    int                  error = 0;
-    std::vector<uint8_t> txt;
-    SubTypeList          sortedSubTypeList = SortSubTypeList(aSubTypeList);
-    TxtList              sortedTxtList     = SortTxtList(aTxtList);
-    std::string          regType           = MakeRegType(aType, sortedSubTypeList);
-    DNSServiceRef        serviceRef        = nullptr;
-    std::string          fullHostName;
-    const char          *hostNameCString    = nullptr;
-    const char          *serviceNameCString = nullptr;
+    otbrError                 error             = OTBR_ERROR_NONE;
+    SubTypeList               sortedSubTypeList = SortSubTypeList(aSubTypeList);
+    std::string               regType           = MakeRegType(aType, sortedSubTypeList);
+    DnssdServiceRegistration *serviceReg;
 
-    VerifyOrExit(mState == State::kReady, ret = OTBR_ERROR_INVALID_STATE);
-
-    if (!aHostName.empty())
+    if (mState != State::kReady)
     {
-        fullHostName    = MakeFullHostName(aHostName);
-        hostNameCString = fullHostName.c_str();
-    }
-    if (!aName.empty())
-    {
-        serviceNameCString = aName.c_str();
+        error = OTBR_ERROR_INVALID_STATE;
+        std::move(aCallback)(error);
+        ExitNow();
     }
 
-    aCallback = HandleDuplicateServiceRegistration(aHostName, aName, aType, sortedSubTypeList, aPort, sortedTxtList,
+    aCallback = HandleDuplicateServiceRegistration(aHostName, aName, aType, sortedSubTypeList, aPort, aTxtData,
                                                    std::move(aCallback));
     VerifyOrExit(!aCallback.IsNull());
 
-    SuccessOrExit(ret = EncodeTxtData(aTxtList, txt));
-    otbrLogInfo("Registering new service %s.%s.local, serviceRef = %p", aName.c_str(), regType.c_str(), serviceRef);
-    SuccessOrExit(error = DNSServiceRegister(&serviceRef, kDNSServiceFlagsNoAutoRename, kDNSServiceInterfaceIndexAny,
-                                             serviceNameCString, regType.c_str(),
-                                             /* domain */ nullptr, hostNameCString, htons(aPort), txt.size(),
-                                             txt.data(), HandleServiceRegisterResult, this));
-    AddServiceRegistration(std::unique_ptr<DnssdServiceRegistration>(new DnssdServiceRegistration(
-        aHostName, aName, aType, sortedSubTypeList, aPort, sortedTxtList, std::move(aCallback), serviceRef, this)));
+    serviceReg = new DnssdServiceRegistration(aHostName, aName, aType, sortedSubTypeList, aPort, aTxtData,
+                                              std::move(aCallback), this);
+    AddServiceRegistration(std::unique_ptr<DnssdServiceRegistration>(serviceReg));
+
+    error = serviceReg->Register();
 
 exit:
-    if (error != kDNSServiceErr_NoError || ret != OTBR_ERROR_NONE)
-    {
-        if (error != kDNSServiceErr_NoError)
-        {
-            ret = DNSErrorToOtbrError(error);
-            otbrLogErr("Failed to publish service %s.%s for mdnssd error: %s!", aName.c_str(), aType.c_str(),
-                       DNSErrorToString(error));
-        }
-
-        if (serviceRef != nullptr)
-        {
-            DNSServiceRefDeallocate(serviceRef);
-        }
-        std::move(aCallback)(ret);
-    }
-    return ret;
+    return error;
 }
 
 void PublisherMDnsSd::UnpublishService(const std::string &aName, const std::string &aType, ResultCallback &&aCallback)
@@ -559,58 +674,30 @@
     std::move(aCallback)(error);
 }
 
-otbrError PublisherMDnsSd::PublishHostImpl(const std::string             &aName,
-                                           const std::vector<Ip6Address> &aAddresses,
-                                           ResultCallback               &&aCallback)
+otbrError PublisherMDnsSd::PublishHostImpl(const std::string &aName,
+                                           const AddressList &aAddresses,
+                                           ResultCallback   &&aCallback)
 {
-    otbrError              ret   = OTBR_ERROR_NONE;
-    int                    error = 0;
-    std::string            fullName;
-    DnssdHostRegistration *registration;
+    otbrError              error = OTBR_ERROR_NONE;
+    DnssdHostRegistration *hostReg;
 
-    VerifyOrExit(mState == Publisher::State::kReady, ret = OTBR_ERROR_INVALID_STATE);
-
-    fullName = MakeFullHostName(aName);
+    if (mState != State::kReady)
+    {
+        error = OTBR_ERROR_INVALID_STATE;
+        std::move(aCallback)(error);
+        ExitNow();
+    }
 
     aCallback = HandleDuplicateHostRegistration(aName, aAddresses, std::move(aCallback));
     VerifyOrExit(!aCallback.IsNull());
-    VerifyOrExit(!aAddresses.empty(), std::move(aCallback)(OTBR_ERROR_NONE));
 
-    if (mHostsRef == nullptr)
-    {
-        SuccessOrExit(error = DNSServiceCreateConnection(&mHostsRef));
-        otbrLogDebug("Created new DNSServiceRef for hosts: %p", mHostsRef);
-    }
+    hostReg = new DnssdHostRegistration(aName, aAddresses, std::move(aCallback), this);
+    AddHostRegistration(std::unique_ptr<DnssdHostRegistration>(hostReg));
 
-    registration = new DnssdHostRegistration(aName, aAddresses, std::move(aCallback), mHostsRef, this);
-
-    otbrLogInfo("Registering new host %s", aName.c_str());
-    for (const auto &address : aAddresses)
-    {
-        DNSRecordRef recordRef = nullptr;
-        // Supports only IPv6 for now, may support IPv4 in the future.
-        SuccessOrExit(error = DNSServiceRegisterRecord(mHostsRef, &recordRef, kDNSServiceFlagsShared,
-                                                       kDNSServiceInterfaceIndexAny, fullName.c_str(),
-                                                       kDNSServiceType_AAAA, kDNSServiceClass_IN, sizeof(address.m8),
-                                                       address.m8, /* ttl */ 0, HandleRegisterHostResult, this));
-        registration->GetRecordRefMap()[recordRef] = address;
-    }
-
-    AddHostRegistration(std::unique_ptr<DnssdHostRegistration>(registration));
+    error = hostReg->Register();
 
 exit:
-    if (error != kDNSServiceErr_NoError || ret != OTBR_ERROR_NONE)
-    {
-        if (error != kDNSServiceErr_NoError)
-        {
-            ret = DNSErrorToOtbrError(error);
-            otbrLogErr("Failed to publish/update host %s for mdnssd error: %s!", aName.c_str(),
-                       DNSErrorToString(error));
-        }
-
-        std::move(aCallback)(ret);
-    }
-    return ret;
+    return error;
 }
 
 void PublisherMDnsSd::UnpublishHost(const std::string &aName, ResultCallback &&aCallback)
@@ -627,52 +714,6 @@
     std::move(aCallback)(error);
 }
 
-void PublisherMDnsSd::HandleRegisterHostResult(DNSServiceRef       aServiceRef,
-                                               DNSRecordRef        aRecordRef,
-                                               DNSServiceFlags     aFlags,
-                                               DNSServiceErrorType aError,
-                                               void               *aContext)
-{
-    static_cast<PublisherMDnsSd *>(aContext)->HandleRegisterHostResult(aServiceRef, aRecordRef, aFlags, aError);
-}
-
-void PublisherMDnsSd::HandleRegisterHostResult(DNSServiceRef       aServiceRef,
-                                               DNSRecordRef        aRecordRef,
-                                               DNSServiceFlags     aFlags,
-                                               DNSServiceErrorType aError)
-{
-    OTBR_UNUSED_VARIABLE(aFlags);
-
-    otbrError error   = DNSErrorToOtbrError(aError);
-    auto     *hostReg = static_cast<DnssdHostRegistration *>(FindHostRegistration(aServiceRef, aRecordRef));
-
-    std::string hostName;
-
-    VerifyOrExit(hostReg != nullptr);
-
-    hostName = MakeFullHostName(hostReg->mName);
-
-    otbrLogInfo("Received reply for host %s: %s", hostName.c_str(), DNSErrorToString(aError));
-
-    if (error == OTBR_ERROR_NONE)
-    {
-        --hostReg->mCallbackCount;
-        if (!hostReg->mCallbackCount)
-        {
-            otbrLogInfo("Successfully registered host %s", hostName.c_str());
-            hostReg->Complete(OTBR_ERROR_NONE);
-        }
-    }
-    else
-    {
-        otbrLogWarning("Failed to register host %s for mdnssd error: %s", hostName.c_str(), DNSErrorToString(aError));
-        RemoveHostRegistration(hostReg->mName, error);
-    }
-
-exit:
-    return;
-}
-
 // See `regtype` parameter of the DNSServiceRegister() function for more information.
 std::string PublisherMDnsSd::MakeRegType(const std::string &aType, SubTypeList aSubTypeList)
 {
@@ -797,6 +838,7 @@
 {
     if (mServiceRef != nullptr)
     {
+        mPublisher.HandleServiceRefDeallocating(mServiceRef);
         DNSServiceRefDeallocate(mServiceRef);
         mServiceRef = nullptr;
     }
@@ -878,13 +920,13 @@
     }
     else
     {
-        mMDnsSd->OnServiceRemoved(aInterfaceIndex, mType, aInstanceName);
+        mPublisher.OnServiceRemoved(aInterfaceIndex, mType, aInstanceName);
     }
 
 exit:
     if (aErrorCode != kDNSServiceErr_NoError)
     {
-        mMDnsSd->OnServiceResolveFailed(mType, mInstanceName, aErrorCode);
+        mPublisher.OnServiceResolveFailed(mType, mInstanceName, aErrorCode);
         Release();
     }
 }
@@ -937,12 +979,11 @@
 {
     assert(mServiceRef == nullptr);
 
-    mSubscription->mMDnsSd->mServiceInstanceResolutionBeginTime[std::make_pair(mInstanceName, mTypeEndWithDot)] =
-        Clock::now();
+    mSubscription->mPublisher.mServiceInstanceResolutionBeginTime[std::make_pair(mInstanceName, mType)] = Clock::now();
 
-    otbrLogInfo("DNSServiceResolve %s %s inf %u", mInstanceName.c_str(), mTypeEndWithDot.c_str(), mNetifIndex);
+    otbrLogInfo("DNSServiceResolve %s %s inf %u", mInstanceName.c_str(), mType.c_str(), mNetifIndex);
     DNSServiceResolve(&mServiceRef, /* flags */ kDNSServiceFlagsTimeout, mNetifIndex, mInstanceName.c_str(),
-                      mTypeEndWithDot.c_str(), mDomain.c_str(), HandleResolveResult, this);
+                      mType.c_str(), mDomain.c_str(), HandleResolveResult, this);
 }
 
 void PublisherMDnsSd::ServiceInstanceResolution::HandleResolveResult(DNSServiceRef        aServiceRef,
@@ -1002,7 +1043,7 @@
 
     if (aErrorCode != kDNSServiceErr_NoError || error != OTBR_ERROR_NONE)
     {
-        mSubscription->mMDnsSd->OnServiceResolveFailed(mSubscription->mType, mInstanceName, aErrorCode);
+        mSubscription->mPublisher.OnServiceResolveFailed(mSubscription->mType, mInstanceName, aErrorCode);
         FinishResolution();
     }
 }
@@ -1087,7 +1128,7 @@
     subscription->RemoveInstanceResolution(*this);
 
     // NOTE: The `ServiceSubscription` object may be freed in `OnServiceResolved`.
-    subscription->mMDnsSd->OnServiceResolved(serviceName, instanceInfo);
+    subscription->mPublisher.OnServiceResolved(serviceName, instanceInfo);
 }
 
 void PublisherMDnsSd::HostSubscription::Resolve(void)
@@ -1096,7 +1137,7 @@
 
     assert(mServiceRef == nullptr);
 
-    mMDnsSd->mHostResolutionBeginTime[mHostName] = Clock::now();
+    mPublisher.mHostResolutionBeginTime[mHostName] = Clock::now();
 
     otbrLogInfo("DNSServiceGetAddrInfo %s inf %d", fullHostName.c_str(), kDNSServiceInterfaceIndexAny);
 
@@ -1149,12 +1190,12 @@
     otbrLogInfo("DNSServiceGetAddrInfo reply: address=%s, ttl=%" PRIu32, address.ToString().c_str(), aTtl);
 
     // NOTE: This `HostSubscription` object may be freed in `OnHostResolved`.
-    mMDnsSd->OnHostResolved(mHostName, mHostInfo);
+    mPublisher.OnHostResolved(mHostName, mHostInfo);
 
 exit:
     if (aErrorCode != kDNSServiceErr_NoError)
     {
-        mMDnsSd->OnHostResolveFailed(aHostName, aErrorCode);
+        mPublisher.OnHostResolveFailed(aHostName, aErrorCode);
     }
 }
 
diff --git a/src/mdns/mdns_mdnssd.hpp b/src/mdns/mdns_mdnssd.hpp
index 8321811..fa4bf4d 100644
--- a/src/mdns/mdns_mdnssd.hpp
+++ b/src/mdns/mdns_mdnssd.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_MDNS_MDNSSD_HPP_
 #define OTBR_AGENT_MDNS_MDNSSD_HPP_
 
+#include "openthread-br/config.h"
+
 #include <array>
 #include <map>
 #include <memory>
@@ -74,7 +76,7 @@
     void      UnsubscribeHost(const std::string &aHostName) override;
     otbrError Start(void) override;
     bool      IsStarted(void) const override;
-    void      Stop(void) override;
+    void      Stop(void) override { Stop(kNormalStop); }
 
     // Implementation of MainloopProcessor.
 
@@ -87,11 +89,11 @@
                                  const std::string &aType,
                                  const SubTypeList &aSubTypeList,
                                  uint16_t           aPort,
-                                 const TxtList     &aTxtList,
+                                 const TxtData     &aTxtData,
                                  ResultCallback   &&aCallback) override;
-    otbrError PublishHostImpl(const std::string             &aName,
-                              const std::vector<Ip6Address> &aAddress,
-                              ResultCallback               &&aCallback) override;
+    otbrError PublishHostImpl(const std::string &aName,
+                              const AddressList &aAddress,
+                              ResultCallback   &&aCallback) override;
     void      OnServiceResolveFailedImpl(const std::string &aType,
                                          const std::string &aInstanceName,
                                          int32_t            aErrorCode) override;
@@ -101,71 +103,69 @@
 private:
     static constexpr uint32_t kDefaultTtl = 10;
 
+    enum StopMode : uint8_t
+    {
+        kNormalStop,
+        kStopOnServiceNotRunningError,
+    };
+
     class DnssdServiceRegistration : public ServiceRegistration
     {
     public:
-        DnssdServiceRegistration(const std::string &aHostName,
-                                 const std::string &aName,
-                                 const std::string &aType,
-                                 const SubTypeList &aSubTypeList,
-                                 uint16_t           aPort,
-                                 const TxtList     &aTxtList,
-                                 ResultCallback   &&aCallback,
-                                 DNSServiceRef      aServiceRef,
-                                 PublisherMDnsSd   *aPublisher)
-            : ServiceRegistration(aHostName,
-                                  aName,
-                                  aType,
-                                  aSubTypeList,
-                                  aPort,
-                                  aTxtList,
-                                  std::move(aCallback),
-                                  aPublisher)
-            , mServiceRef(aServiceRef)
-        {
-        }
+        using ServiceRegistration::ServiceRegistration; // Inherit base constructor
 
-        ~DnssdServiceRegistration(void) override;
-        const DNSServiceRef &GetServiceRef() const { return mServiceRef; }
+        ~DnssdServiceRegistration(void) override { Unregister(); }
+
+        void      Update(MainloopContext &aMainloop) const;
+        void      Process(const MainloopContext &aMainloop, std::vector<DNSServiceRef> &aReadyServices) const;
+        otbrError Register(void);
 
     private:
-        DNSServiceRef mServiceRef;
+        void             Unregister(void);
+        PublisherMDnsSd &GetPublisher(void) { return *static_cast<PublisherMDnsSd *>(mPublisher); }
+        void             HandleRegisterResult(DNSServiceFlags aFlags, DNSServiceErrorType aError);
+        static void      HandleRegisterResult(DNSServiceRef       aServiceRef,
+                                              DNSServiceFlags     aFlags,
+                                              DNSServiceErrorType aError,
+                                              const char         *aName,
+                                              const char         *aType,
+                                              const char         *aDomain,
+                                              void               *aContext);
+
+        DNSServiceRef mServiceRef = nullptr;
     };
 
     class DnssdHostRegistration : public HostRegistration
     {
     public:
-        DnssdHostRegistration(const std::string             &aName,
-                              const std::vector<Ip6Address> &aAddresses,
-                              ResultCallback               &&aCallback,
-                              DNSServiceRef                  aServiceRef,
-                              Publisher                     *aPublisher)
-            : HostRegistration(aName, aAddresses, std::move(aCallback), aPublisher)
-            , mServiceRef(aServiceRef)
-            , mRecordRefMap()
-            , mCallbackCount(aAddresses.size())
-        {
-        }
+        using HostRegistration::HostRegistration; // Inherit base class constructor
 
-        ~DnssdHostRegistration(void) override;
-        const DNSServiceRef                      &GetServiceRef() const { return mServiceRef; }
-        const std::map<DNSRecordRef, Ip6Address> &GetRecordRefMap() const { return mRecordRefMap; }
-        std::map<DNSRecordRef, Ip6Address>       &GetRecordRefMap() { return mRecordRefMap; }
+        ~DnssdHostRegistration(void) override { Unregister(); }
+
+        otbrError Register(void);
 
     private:
-        DNSServiceRef mServiceRef;
+        void             Unregister(void);
+        PublisherMDnsSd &GetPublisher(void) { return *static_cast<PublisherMDnsSd *>(mPublisher); }
+        void             HandleRegisterResult(DNSRecordRef aRecordRef, DNSServiceErrorType aError);
+        static void      HandleRegisterResult(DNSServiceRef       aServiceRef,
+                                              DNSRecordRef        aRecordRef,
+                                              DNSServiceFlags     aFlags,
+                                              DNSServiceErrorType aErrorCode,
+                                              void               *aContext);
 
-    public:
-        std::map<DNSRecordRef, Ip6Address> mRecordRefMap;
-        uint32_t                           mCallbackCount;
+        std::vector<DNSRecordRef> mAddrRecordRefs;
+        std::vector<bool>         mAddrRegistered;
     };
 
     struct ServiceRef : private ::NonCopyable
     {
-        DNSServiceRef mServiceRef;
+        DNSServiceRef    mServiceRef;
+        PublisherMDnsSd &mPublisher;
 
-        explicit ServiceRef(void)
+        explicit ServiceRef(PublisherMDnsSd &aPublisher)
             : mServiceRef(nullptr)
+            , mPublisher(aPublisher)
         {
         }
 
@@ -186,10 +186,10 @@
                                            std::string          aType,
                                            std::string          aDomain,
                                            uint32_t             aNetifIndex)
-            : ServiceRef()
+            : ServiceRef(aSubscription.mPublisher)
             , mSubscription(&aSubscription)
             , mInstanceName(std::move(aInstanceName))
-            , mTypeEndWithDot(std::move(aType))
+            , mType(std::move(aType))
             , mDomain(std::move(aDomain))
             , mNetifIndex(aNetifIndex)
         {
@@ -236,7 +236,7 @@
 
         ServiceSubscription   *mSubscription;
         std::string            mInstanceName;
-        std::string            mTypeEndWithDot;
+        std::string            mType;
         std::string            mDomain;
         uint32_t               mNetifIndex;
         DiscoveredInstanceInfo mInstanceInfo;
@@ -244,9 +244,8 @@
 
     struct ServiceSubscription : public ServiceRef
     {
-        explicit ServiceSubscription(PublisherMDnsSd &aMDnsSd, std::string aType, std::string aInstanceName)
-            : ServiceRef()
-            , mMDnsSd(&aMDnsSd)
+        explicit ServiceSubscription(PublisherMDnsSd &aPublisher, std::string aType, std::string aInstanceName)
+            : ServiceRef(aPublisher)
             , mType(std::move(aType))
             , mInstanceName(std::move(aInstanceName))
         {
@@ -277,18 +276,16 @@
                                        const char         *aType,
                                        const char         *aDomain);
 
-        PublisherMDnsSd *mMDnsSd;
-        std::string      mType;
-        std::string      mInstanceName;
+        std::string mType;
+        std::string mInstanceName;
 
         std::vector<std::unique_ptr<ServiceInstanceResolution>> mResolvingInstances;
     };
 
     struct HostSubscription : public ServiceRef
     {
-        explicit HostSubscription(PublisherMDnsSd &aMDnsSd, std::string aHostName)
-            : ServiceRef()
-            , mMDnsSd(&aMDnsSd)
+        explicit HostSubscription(PublisherMDnsSd &aPublisher, std::string aHostName)
+            : ServiceRef(aPublisher)
             , mHostName(std::move(aHostName))
         {
         }
@@ -310,7 +307,6 @@
                                         const struct sockaddr *aAddress,
                                         uint32_t               aTtl);
 
-        PublisherMDnsSd   *mMDnsSd;
         std::string        mHostName;
         DiscoveredHostInfo mHostInfo;
     };
@@ -318,33 +314,12 @@
     using ServiceSubscriptionList = std::vector<std::unique_ptr<ServiceSubscription>>;
     using HostSubscriptionList    = std::vector<std::unique_ptr<HostSubscription>>;
 
-    static void HandleServiceRegisterResult(DNSServiceRef         aService,
-                                            const DNSServiceFlags aFlags,
-                                            DNSServiceErrorType   aError,
-                                            const char           *aName,
-                                            const char           *aType,
-                                            const char           *aDomain,
-                                            void                 *aContext);
-    void        HandleServiceRegisterResult(DNSServiceRef         aService,
-                                            const DNSServiceFlags aFlags,
-                                            DNSServiceErrorType   aError,
-                                            const char           *aName,
-                                            const char           *aType,
-                                            const char           *aDomain);
-    static void HandleRegisterHostResult(DNSServiceRef       aHostsConnection,
-                                         DNSRecordRef        aHostRecord,
-                                         DNSServiceFlags     aFlags,
-                                         DNSServiceErrorType aErrorCode,
-                                         void               *aContext);
-    void        HandleRegisterHostResult(DNSServiceRef       aHostsConnection,
-                                         DNSRecordRef        aHostRecord,
-                                         DNSServiceFlags     aFlags,
-                                         DNSServiceErrorType aErrorCode);
-
     static std::string MakeRegType(const std::string &aType, SubTypeList aSubTypeList);
 
-    ServiceRegistration *FindServiceRegistration(const DNSServiceRef &aServiceRef);
-    HostRegistration    *FindHostRegistration(const DNSServiceRef &aServiceRef, const DNSRecordRef &aRecordRef);
+    void                Stop(StopMode aStopMode);
+    DNSServiceErrorType CreateSharedHostsRef(void);
+    void                DeallocateHostsRef(void);
+    void                HandleServiceRefDeallocating(const DNSServiceRef &aServiceRef);
 
     DNSServiceRef mHostsRef;
     State         mState;
@@ -352,6 +327,8 @@
 
     ServiceSubscriptionList mSubscribedServices;
     HostSubscriptionList    mSubscribedHosts;
+
+    std::vector<DNSServiceRef> mServiceRefsToProcess;
 };
 
 /**
diff --git a/src/ncp/CMakeLists.txt b/src/ncp/CMakeLists.txt
index c2eac7a..1540225 100644
--- a/src/ncp/CMakeLists.txt
+++ b/src/ncp/CMakeLists.txt
@@ -34,10 +34,5 @@
 target_link_libraries(otbr-ncp PRIVATE
     otbr-common
     $<$<BOOL:${OTBR_FEATURE_FLAGS}>:otbr-proto>
+    $<$<BOOL:${OTBR_TELEMETRY_DATA_API}>:otbr-proto>
 )
-
-if(OTBR_FEATURE_FLAGS)
-    target_include_directories(otbr-ncp PRIVATE
-        ${PROJECT_SOURCE_DIR}/build/src
-    )
-endif()
diff --git a/src/ncp/ncp_openthread.cpp b/src/ncp/ncp_openthread.cpp
index 0f4302c..efd3e7f 100644
--- a/src/ncp/ncp_openthread.cpp
+++ b/src/ncp/ncp_openthread.cpp
@@ -35,6 +35,7 @@
 #include <string.h>
 
 #include <openthread/backbone_router_ftd.h>
+#include <openthread/border_routing.h>
 #include <openthread/dataset.h>
 #include <openthread/dnssd_server.h>
 #include <openthread/logging.h>
@@ -59,9 +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 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
 
 ControllerOpenThread::ControllerOpenThread(const char                      *aInterfaceName,
                                            const std::vector<const char *> &aRadioUrls,
@@ -243,6 +245,9 @@
 #if OTBR_ENABLE_DNS_UPSTREAM_QUERY
     otDnssdUpstreamQuerySetEnabled(mInstance, /* aEnabled */ true);
 #endif
+#if OTBR_ENABLE_DHCP6_PD
+    otBorderRoutingDhcp6PdSetEnabled(mInstance, /* aEnabled */ true);
+#endif
 #endif // OTBR_ENABLE_FEATURE_FLAGS
 
     mThreadHelper = std::unique_ptr<otbr::agent::ThreadHelper>(new otbr::agent::ThreadHelper(mInstance, this));
@@ -277,6 +282,9 @@
 #if OTBR_ENABLE_DNS_UPSTREAM_QUERY
     otDnssdUpstreamQuerySetEnabled(mInstance, aFeatureFlagList.enable_dns_upstream_query());
 #endif
+#if OTBR_ENABLE_DHCP6_PD
+    otBorderRoutingDhcp6PdSetEnabled(mInstance, aFeatureFlagList.enable_dhcp6_pd());
+#endif
 
     return error;
 }
@@ -377,6 +385,9 @@
     case kThreadVersion13:
         version = "1.3.0";
         break;
+    case kThreadVersion131:
+        version = "1.3.1";
+        break;
     default:
         otbrLogEmerg("Unexpected thread version %hu", otThreadGetVersion());
         exit(-1);
diff --git a/src/ncp/ncp_openthread.hpp b/src/ncp/ncp_openthread.hpp
index 58779dd..51ba31f 100644
--- a/src/ncp/ncp_openthread.hpp
+++ b/src/ncp/ncp_openthread.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_NCP_OPENTHREAD_HPP_
 #define OTBR_AGENT_NCP_OPENTHREAD_HPP_
 
+#include "openthread-br/config.h"
+
 #include <chrono>
 #include <memory>
 
diff --git a/src/openwrt/otbr-agent.init.in b/src/openwrt/otbr-agent.init.in
index 2c1de2f..46505ff 100755
--- a/src/openwrt/otbr-agent.init.in
+++ b/src/openwrt/otbr-agent.init.in
@@ -34,8 +34,11 @@
 start_service()
 {
     local uci_thread_if_name=$(uci -q get otbr-agent.service.thread_if_name)
+    local uci_infra_if_name=$(uci -q get otbr-agent.service.infra_if_name)
+    local uci_uart_device=$(uci -q get otbr-agent.service.uart_device)
+    local uci_uart_baudrate=$(uci -q get otbr-agent.service.uart_baudrate)
 
     procd_open_instance
-    procd_set_param command @CMAKE_INSTALL_FULL_SBINDIR@/otbr-agent -I $uci_thread_if_name spinel+hdlc+uart:///dev/ttyACM0
+    procd_set_param command @CMAKE_INSTALL_FULL_SBINDIR@/otbr-agent -I $uci_thread_if_name -B $uci_infra_if_name spinel+hdlc+uart://$uci_uart_device?uart-baudrate=$uci_uart_baudrate trel://$uci_infra_if_name
     procd_close_instance
 }
diff --git a/src/openwrt/otbr-agent.uci-config.in b/src/openwrt/otbr-agent.uci-config.in
index 7d8473c..855d708 100644
--- a/src/openwrt/otbr-agent.uci-config.in
+++ b/src/openwrt/otbr-agent.uci-config.in
@@ -1,2 +1,5 @@
 config otbr-agent 'service'
 	option thread_if_name "wpan0"
+	option infra_if_name "eth0"
+	option uart_device "/dev/ttyACM0"
+	option uart_baudrate 115200
diff --git a/src/openwrt/view/admin_thread/thread_view.htm b/src/openwrt/view/admin_thread/thread_view.htm
index bc30bfd..4737210 100644
--- a/src/openwrt/view/admin_thread/thread_view.htm
+++ b/src/openwrt/view/admin_thread/thread_view.htm
@@ -116,7 +116,7 @@
 </div>
 <%+footer%>
 
-<script src='http://d3js.org/d3.v4.min.js'></script>
+<script src='//d3js.org/d3.v4.min.js'></script>
 <script type="text/javascript" src="/luci-static/resources/handle_error.js"></script>
 <script type="text/javascript">//<![CDATA[
 	handle_error(GetURLParameter('error'));
diff --git a/src/proto/CMakeLists.txt b/src/proto/CMakeLists.txt
index 28e83dc..e982d52 100644
--- a/src/proto/CMakeLists.txt
+++ b/src/proto/CMakeLists.txt
@@ -1,3 +1,11 @@
+# Config brew protobuf version for Mac, see .github/workflows/macOS.yml
+if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+    set(Protobuf_PREFIX_PATH
+        "/usr/local/opt/protobuf@21/include"            
+        "/usr/local/opt/protobuf@21/lib"             
+        "/usr/local/opt/protobuf@21/bin")
+    list(APPEND CMAKE_PREFIX_PATH "${Protobuf_PREFIX_PATH}")
+endif()
 find_package(Protobuf REQUIRED)
 
 # Set up the output path.
@@ -50,6 +58,11 @@
 )
 
 find_package(Protobuf REQUIRED)
-target_link_libraries(otbr-proto
+
+target_link_libraries(otbr-proto PUBLIC
     protobuf::libprotobuf-lite
 )
+
+target_include_directories(otbr-proto PUBLIC
+    ${PROJECT_SOURCE_DIR}/build/src
+)
diff --git a/src/proto/capabilities.proto b/src/proto/capabilities.proto
new file mode 100644
index 0000000..f49864a
--- /dev/null
+++ b/src/proto/capabilities.proto
@@ -0,0 +1,14 @@
+syntax = "proto2";
+option optimize_for = LITE_RUNTIME;
+
+package otbr;
+
+// Capabilities message exposes a list of values of the macros of ot-br-posix. This is not an exhaustive
+// list of all macros of ot-br-posix.
+message Capabilities {
+    // Each of the following items matches exactly one macro, i.e. OTBR_ENABLE_NAT64 -> nat64
+    // When some macro is deleted, the corresponding value should be marked as "reserved".
+    // It is suggested to assign a new field number to a macro when its scope has significantly changed.
+    optional bool nat64 = 1; // OTBR_ENABLE_NAT64
+    optional bool dhcp6_pd = 2; // OTBR_ENABLE_DHCP6_PD
+}
diff --git a/src/proto/feature_flag.proto b/src/proto/feature_flag.proto
index 1b1cf01..2b82b89 100644
--- a/src/proto/feature_flag.proto
+++ b/src/proto/feature_flag.proto
@@ -62,4 +62,6 @@
   optional bool enable_trel = 4 [default = false];
   // Whether to enable upstream DNS forwarding.
   optional bool enable_dns_upstream_query = 5 [default = false];
+  // Whether to enable prefix delegation.
+  optional bool enable_dhcp6_pd = 6 [default = false];
 }
diff --git a/src/proto/thread_telemetry.proto b/src/proto/thread_telemetry.proto
new file mode 100644
index 0000000..8ffcadf
--- /dev/null
+++ b/src/proto/thread_telemetry.proto
@@ -0,0 +1,489 @@
+/*
+ *  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.
+ */
+syntax = "proto2";
+option optimize_for = LITE_RUNTIME;
+
+package threadnetwork;
+
+// Thread Telemetry data definition.
+// The field range for your data definition is determined as:
+// ---------------------------------------------------------------------------
+// | Field Range  | Logging From
+// | [1 - 500)    | Primary fields logged from OTBR-agent/OpenThread.
+// | [500-600)    | OTBR vendor fields logged from OTBR-agent/OpenThread.
+// | Other        | Reserved for now.
+// ---------------------------------------------------------------------------
+// Usage:
+// Delete field: do not directly delete field. Deprecate it instead.
+message TelemetryData {
+  message Duration {
+    // Signed seconds of the span of time. Must be from -315,576,000,000
+    // to +315,576,000,000 inclusive. Note: these bounds are computed from:
+    // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
+    optional int64 seconds = 1;
+
+    // Signed fractions of a second at nanosecond resolution of the span
+    // of time. Durations less than one second are represented with a 0
+    // `seconds` field and a positive or negative `nanos` field. For durations
+    // of one second or more, a non-zero value for the `nanos` field must be
+    // of the same sign as the `seconds` field. Must be from -999,999,999
+    // to +999,999,999 inclusive.
+    optional int32 nanos = 2;
+  }
+
+  message WpanStats {
+    optional int32 phy_rx = 1;
+    optional int32 phy_tx = 2;
+    optional int32 mac_unicast_rx = 3;
+    optional int32 mac_unicast_tx = 4;
+    optional int32 mac_broadcast_rx = 5;
+    optional int32 mac_broadcast_tx = 6;
+    optional int32 mac_tx_ack_req = 7;
+    optional int32 mac_tx_no_ack_req = 8;
+    optional int32 mac_tx_acked = 9;
+    optional int32 mac_tx_data = 10;
+    optional int32 mac_tx_data_poll = 11;
+    optional int32 mac_tx_beacon = 12;
+    optional int32 mac_tx_beacon_req = 13;
+    optional int32 mac_tx_other_pkt = 14;
+    optional int32 mac_tx_retry = 15;
+    optional int32 mac_rx_data = 16;
+    optional int32 mac_rx_data_poll = 17;
+    optional int32 mac_rx_beacon = 18;
+    optional int32 mac_rx_beacon_req = 19;
+    optional int32 mac_rx_other_pkt = 20;
+    optional int32 mac_rx_filter_whitelist = 21;
+    optional int32 mac_rx_filter_dest_addr = 22;
+    optional int32 mac_tx_fail_cca = 23;
+    optional int32 mac_rx_fail_decrypt = 24;
+    optional int32 mac_rx_fail_no_frame = 25;
+    optional int32 mac_rx_fail_unknown_neighbor = 26;
+    optional int32 mac_rx_fail_invalid_src_addr = 27;
+    optional int32 mac_rx_fail_fcs = 28;
+    optional int32 mac_rx_fail_other = 29;
+    optional int32 ip_tx_success = 30;
+    optional int32 ip_rx_success = 31;
+    optional int32 ip_tx_failure = 32;
+    optional int32 ip_rx_failure = 33;
+    optional uint32 node_type = 34;
+    optional uint32 channel = 35;
+    optional int32 radio_tx_power = 36;
+    optional float mac_cca_fail_rate = 37;
+  }
+
+  message WpanTopoFull {
+    optional uint32 rloc16 = 1;
+    optional uint32 router_id = 2;
+    optional uint32 leader_router_id = 3;
+    // Deprecate bytes ext_address.
+    reserved 4;
+    optional bytes leader_address = 5;
+    optional uint32 leader_weight = 6;
+    optional uint32 leader_local_weight = 7;
+    optional bytes network_data = 8;
+    optional uint32 network_data_version = 9;
+    optional bytes stable_network_data = 10;
+    optional uint32 stable_network_data_version = 11;
+    optional uint32 preferred_router_id = 12;
+    optional uint32 partition_id = 13;
+    optional uint32 child_table_size = 14;
+    optional uint32 neighbor_table_size = 15;
+    optional int32 instant_rssi = 16;
+    optional uint64 extended_pan_id = 17;
+  }
+
+  message TopoEntry {
+    // deprecate bytes ext_address.
+    reserved 1;
+    optional uint32 rloc16 = 2;
+    // link quality with data range: 0~3.
+    optional uint32 link_quality_in = 3;
+    // the most recent RSSI measurement (8 bit).
+    optional int32 average_rssi = 4;
+    optional Duration age = 5;
+    optional bool rx_on_when_idle = 6;
+    optional bool full_function = 7;
+    optional bool secure_data_request = 8;
+    optional bool full_network_data = 9;
+    optional int32 last_rssi = 10;
+    optional uint32 link_frame_counter = 11;
+    optional uint32 mle_frame_counter = 12;
+    optional bool is_child = 13;
+    optional Duration timeout = 14;
+    optional uint32 network_data_version = 15;
+    optional float mac_frame_error_rate = 16;
+    optional float ip_message_error_rate = 17;
+    optional int32 version = 18;
+  }
+
+  enum NodeType {
+    NODE_TYPE_UNSPECIFIED = 0;
+    NODE_TYPE_ROUTER = 1;
+    NODE_TYPE_END = 2;
+    NODE_TYPE_SLEEPY_END = 3;
+    NODE_TYPE_MINIMAL_END = 4;
+
+    NODE_TYPE_OFFLINE = 5;
+    NODE_TYPE_DISABLED = 6;
+    NODE_TYPE_DETACHED = 7;
+
+    NODE_TYPE_NL_LURKER = 0x10;
+    NODE_TYPE_COMMISSIONER = 0x20;
+    NODE_TYPE_LEADER = 0x40;
+  }
+
+  message PacketsAndBytes {
+    optional int64 packet_count = 1;
+    optional int64 byte_count = 2;
+  }
+
+  message Nat64TrafficCounters {
+    optional int64 ipv4_to_ipv6_packets = 1;
+    optional int64 ipv4_to_ipv6_bytes = 2;
+    optional int64 ipv6_to_ipv4_packets = 3;
+    optional int64 ipv6_to_ipv4_bytes = 4;
+  }
+
+  message Nat64ProtocolCounters {
+    optional Nat64TrafficCounters tcp = 1;
+    optional Nat64TrafficCounters udp = 2;
+    optional Nat64TrafficCounters icmp = 3;
+  }
+
+  message Nat64PacketCounters {
+    optional int64 ipv4_to_ipv6_packets = 1;
+    optional int64 ipv6_to_ipv4_packets = 2;
+  }
+
+  message Nat64ErrorCounters {
+    optional Nat64PacketCounters unknown = 1;
+    optional Nat64PacketCounters illegal_packet = 2;
+    optional Nat64PacketCounters unsupported_protocol = 3;
+    optional Nat64PacketCounters no_mapping = 4;
+  }
+
+  message BorderRoutingCounters {
+    reserved 1 to 8;
+    // The number of Router Advertisement packets received by otbr-agent on the
+    // infra link
+    optional int64 ra_rx = 9;
+
+    // The number of Router Advertisement packets successfully transmitted by
+    // otbr-agent on the infra link.
+    optional int64 ra_tx_success = 10;
+
+    // The number of Router Advertisement packets failed to transmit by
+    // otbr-agent on the infra link.
+    optional int64 ra_tx_failure = 11;
+
+    // The number of Router Solicitation packets received by otbr-agent on the
+    // infra link
+    optional int64 rs_rx = 12;
+
+    // The number of Router Solicitation packets successfully transmitted by
+    // otbr-agent on the infra link.
+    optional int64 rs_tx_success = 13;
+
+    // The number of Router Solicitation packets failed to transmit by
+    // otbr-agent on the infra link.
+    optional int64 rs_tx_failure = 14;
+
+    // The counters for inbound unicast packets
+    optional PacketsAndBytes inbound_unicast = 15;
+
+    // The counters for inbound multicast packets
+    optional PacketsAndBytes inbound_multicast = 16;
+
+    // The counters for outbound unicast packets
+    optional PacketsAndBytes outbound_unicast = 17;
+
+    // The counters for outbound multicast packets
+    optional PacketsAndBytes outbound_multicast = 18;
+
+    // The inbound and outbound NAT64 traffic through the border router
+    optional Nat64ProtocolCounters nat64_protocol_counters = 19;
+
+    // Error counters for NAT64 translator on the border router
+    optional Nat64ErrorCounters nat64_error_counters = 20;
+  }
+
+  message SrpServerRegistrationInfo {
+    // The number of active hosts/services registered on the SRP server.
+    optional uint32 fresh_count = 1;
+
+    // The number of hosts/services in 'Deleted' state on the SRP server.
+    optional uint32 deleted_count = 2;
+
+    // The sum of lease time in milliseconds of all active hosts/services on the
+    // SRP server.
+    optional uint64 lease_time_total_ms = 3;
+
+    // The sum of key lease time in milliseconds of all active hosts/services on
+    // the SRP server.
+    optional uint64 key_lease_time_total_ms = 4;
+
+    // The sum of remaining lease time in milliseconds of all active
+    // hosts/services on the SRP server.
+    optional uint64 remaining_lease_time_total_ms = 5;
+
+    // The sum of remaining key lease time in milliseconds of all active
+    // hosts/services on the SRP server.
+    optional uint64 remaining_key_lease_time_total_ms = 6;
+  }
+
+  message SrpServerResponseCounters {
+    // The number of successful responses
+    optional uint32 success_count = 1;
+
+    // The number of server failure responses
+    optional uint32 server_failure_count = 2;
+
+    // The number of format error responses
+    optional uint32 format_error_count = 3;
+
+    // The number of 'name exists' responses
+    optional uint32 name_exists_count = 4;
+
+    // The number of refused responses
+    optional uint32 refused_count = 5;
+
+    // The number of other responses
+    optional uint32 other_count = 6;
+  }
+
+  enum SrpServerState {
+    SRP_SERVER_STATE_UNSPECIFIED = 0;
+    SRP_SERVER_STATE_DISABLED = 1;
+    SRP_SERVER_STATE_RUNNING = 2;
+    SRP_SERVER_STATE_STOPPED = 3;
+  }
+
+  // The address mode used by the SRP server
+  enum SrpServerAddressMode {
+    SRP_SERVER_ADDRESS_MODE_UNSPECIFIED = 0;
+    SRP_SERVER_ADDRESS_MODE_UNICAST = 1;
+    SRP_SERVER_ADDRESS_MODE_STATE_ANYCAST = 2;
+  }
+
+  message SrpServerInfo {
+    // The state of the SRP server
+    optional SrpServerState state = 1;
+
+    // Listening port number
+    optional uint32 port = 2;
+    // The address mode {unicast, anycast} of the SRP server
+    optional SrpServerAddressMode address_mode = 3;
+
+    // The registration information of hosts on the SRP server
+    optional SrpServerRegistrationInfo hosts = 4;
+
+    // The registration information of services on the SRP server
+    optional SrpServerRegistrationInfo services = 5;
+
+    // The counters of response codes sent by the SRP server
+    optional SrpServerResponseCounters response_counters = 6;
+  }
+
+  message DnsServerResponseCounters {
+    // The number of successful responses
+    optional uint32 success_count = 1;
+
+    // The number of server failure responses
+    optional uint32 server_failure_count = 2;
+
+    // The number of format error responses
+    optional uint32 format_error_count = 3;
+
+    // The number of name error responses
+    optional uint32 name_error_count = 4;
+
+    // The number of 'not implemented' responses
+    optional uint32 not_implemented_count = 5;
+
+    // The number of other responses
+    optional uint32 other_count = 6;
+  }
+
+  message DnsServerInfo {
+    // The counters of response codes sent by the DNS server
+    optional DnsServerResponseCounters response_counters = 1;
+
+    // The number of DNS queries resolved at the local SRP server
+    optional uint32 resolved_by_local_srp_count = 2;
+  }
+
+  message MdnsResponseCounters {
+    // The number of successful responses
+    optional uint32 success_count = 1;
+
+    // The number of 'not found' responses
+    optional uint32 not_found_count = 2;
+
+    // The number of 'invalid arg' responses
+    optional uint32 invalid_args_count = 3;
+
+    // The number of 'duplicated' responses
+    optional uint32 duplicated_count = 4;
+
+    // The number of 'not implemented' responses
+    optional uint32 not_implemented_count = 5;
+
+    // The number of unknown error responses
+    optional uint32 unknown_error_count = 6;
+
+    // The number of aborted responses
+    optional uint32 aborted_count = 7;
+
+    // The number of invalid state responses
+    optional uint32 invalid_state_count = 8;
+  }
+
+  message MdnsInfo {
+    // The response counters of host registrations
+    optional MdnsResponseCounters host_registration_responses = 1;
+
+    // The response counters of service registrations
+    optional MdnsResponseCounters service_registration_responses = 2;
+
+    // The response counters of host resolutions
+    optional MdnsResponseCounters host_resolution_responses = 3;
+
+    // The response counters of service resolutions
+    optional MdnsResponseCounters service_resolution_responses = 4;
+
+    // The EMA (Exponential Moving Average) latencies of mDNS operations
+
+    // The EMA latency of host registrations in milliseconds
+    optional uint32 host_registration_ema_latency_ms = 5;
+
+    // The EMA latency of service registrations in milliseconds
+    optional uint32 service_registration_ema_latency_ms = 6;
+
+    // The EMA latency of host resolutions in milliseconds
+    optional uint32 host_resolution_ema_latency_ms = 7;
+
+    // The EMA latency of service resolutions in milliseconds
+    optional uint32 service_resolution_ema_latency_ms = 8;
+  }
+
+  enum Nat64State {
+    NAT64_STATE_UNSPECIFIED = 0;
+    NAT64_STATE_DISABLED = 1;
+    NAT64_STATE_NOT_RUNNING = 2;
+    NAT64_STATE_IDLE = 3;
+    NAT64_STATE_ACTIVE = 4;
+  }
+
+  message BorderRoutingNat64State {
+    optional Nat64State prefix_manager_state = 1;
+    optional Nat64State translator_state = 2;
+  }
+
+  message Nat64Mapping {
+    optional uint64 mapping_id = 1;
+    optional bytes hashed_ipv6_address = 2;
+    optional Nat64ProtocolCounters counters = 3;
+  }
+
+  message WpanBorderRouter {
+    // Border routing counters
+    optional BorderRoutingCounters border_routing_counters = 1;
+
+    // Information about the SRP server
+    optional SrpServerInfo srp_server = 2;
+
+    // Information about the DNS server
+    optional DnsServerInfo dns_server = 3;
+
+    // Information about the mDNS publisher
+    optional MdnsInfo mdns = 4;
+
+    // TODO(b/285457467): remove this reserved proto field.
+    reserved 5;
+
+    // Information about the state of components of NAT64
+    optional BorderRoutingNat64State nat64_state = 6;
+
+    // Information about the mappings of NAT64 translator
+    repeated Nat64Mapping nat64_mappings = 7;
+  }
+
+  message RcpStabilityStatistics {
+    optional uint32 rcp_timeout_count = 1;
+    optional uint32 rcp_reset_count = 2;
+    optional uint32 rcp_restoration_count = 3;
+    optional uint32 spinel_parse_error_count = 4;
+    optional int32 rcp_firmware_update_count = 5;
+    optional uint32 thread_stack_uptime = 6;
+  }
+
+  message RcpInterfaceStatistics {
+    optional uint32 rcp_interface_type = 1;
+    optional uint64 transferred_frames_count = 2;
+    optional uint64 transferred_valid_frames_count = 3;
+    optional uint64 transferred_garbage_frames_count = 4;
+    optional uint64 rx_frames_count = 5;
+    optional uint64 rx_bytes_count = 6;
+    optional uint64 tx_frames_count = 7;
+    optional uint64 tx_bytes_count = 8;
+  }
+
+  message WpanRcp {
+    optional RcpStabilityStatistics rcp_stability_statistics = 1;
+    optional RcpInterfaceStatistics rcp_interface_statistics = 2;
+  }
+
+  message CoexMetrics {
+    // Use uint32 instead of int64 to save space for payload, and align with the
+    // raw data size.
+    optional uint32 count_tx_request = 1;
+    optional uint32 count_tx_grant_immediate = 2;
+    optional uint32 count_tx_grant_wait = 3;
+    optional uint32 count_tx_grant_wait_activated = 4;
+    optional uint32 count_tx_grant_wait_timeout = 5;
+    optional uint32 count_tx_grant_deactivated_during_request = 6;
+    optional uint32 tx_average_request_to_grant_time_us = 7;
+    optional uint32 count_rx_request = 8;
+    optional uint32 count_rx_grant_immediate = 9;
+    optional uint32 count_rx_grant_wait = 10;
+    optional uint32 count_rx_grant_wait_activated = 11;
+    optional uint32 count_rx_grant_wait_timeout = 12;
+    optional uint32 count_rx_grant_deactivated_during_request = 13;
+    optional uint32 count_rx_grant_none = 14;
+    optional uint32 rx_average_request_to_grant_time_us = 15;
+  }
+
+  optional WpanStats wpan_stats = 1;
+  optional WpanTopoFull wpan_topo_full = 2;
+  repeated TopoEntry topo_entries = 3;
+  optional WpanBorderRouter wpan_border_router = 4;
+  optional WpanRcp wpan_rcp = 5;
+  // Deprecate CoexMetrics with int64 as its fields types to save storage.
+  reserved 6;
+  optional CoexMetrics coex_metrics = 7;
+}
diff --git a/src/rest/connection.hpp b/src/rest/connection.hpp
index 93407c3..728c2b7 100644
--- a/src/rest/connection.hpp
+++ b/src/rest/connection.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_REST_CONNECTION_HPP_
 #define OTBR_REST_CONNECTION_HPP_
 
+#include "openthread-br/config.h"
+
 #include <string.h>
 #include <unistd.h>
 
diff --git a/src/rest/json.cpp b/src/rest/json.cpp
index 8aeb89a..d10db85 100644
--- a/src/rest/json.cpp
+++ b/src/rest/json.cpp
@@ -73,6 +73,22 @@
     return ret;
 }
 
+bool JsonString2String(const std::string &aJsonString, std::string &aString)
+{
+    cJSON *jsonString;
+    bool   ret = true;
+
+    VerifyOrExit((jsonString = cJSON_Parse(aJsonString.c_str())) != nullptr, ret = false);
+    VerifyOrExit(cJSON_IsString(jsonString), ret = false);
+
+    aString = std::string(jsonString->valuestring);
+
+exit:
+    cJSON_Delete(jsonString);
+
+    return ret;
+}
+
 std::string Json2String(const cJSON *aJson)
 {
     std::string ret;
@@ -346,8 +362,8 @@
     cJSON      *node = cJSON_CreateObject();
     std::string ret;
 
-    cJSON_AddItemToObject(node, "BaId", Bytes2HexJson(aNode.mBaId, OT_BORDER_AGENT_ID_LENGTH));
-    cJSON_AddItemToObject(node, "State", cJSON_CreateNumber(aNode.mRole));
+    cJSON_AddItemToObject(node, "BaId", Bytes2HexJson(aNode.mBaId.mId, sizeof(aNode.mBaId)));
+    cJSON_AddItemToObject(node, "State", cJSON_CreateString(aNode.mRole.c_str()));
     cJSON_AddItemToObject(node, "NumOfRouter", cJSON_CreateNumber(aNode.mNumOfRouter));
     cJSON_AddItemToObject(node, "RlocAddress", IpAddr2Json(aNode.mRlocAddress));
     cJSON_AddItemToObject(node, "ExtAddress", Bytes2HexJson(aNode.mExtAddress, OT_EXT_ADDRESS_SIZE));
diff --git a/src/rest/json.hpp b/src/rest/json.hpp
index 29153d5..7df74db 100644
--- a/src/rest/json.hpp
+++ b/src/rest/json.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_REST_JSON_HPP_
 #define OTBR_REST_JSON_HPP_
 
+#include "openthread-br/config.h"
+
 #include "openthread/dataset.h"
 #include "openthread/link.h"
 #include "openthread/thread_ftd.h"
@@ -104,6 +106,16 @@
 std::string String2JsonString(const std::string &aString);
 
 /**
+ * This method parses a Json string and checks its datatype and returns a string if it is a string.
+ *
+ * @param[in]  aJsonString  A Json string.
+ * @param[out] aString      The string.
+ *
+ * @returns A boolean indicating whether the Json string was indeed a string.
+ */
+bool JsonString2String(const std::string &aJsonString, std::string &aString);
+
+/**
  * This method formats a Node object to a Json object and serialize it to a string.
  *
  * @param[in] aNode  A Node object.
diff --git a/src/rest/openapi.yaml b/src/rest/openapi.yaml
index f3a4f44..2ba2a4d 100644
--- a/src/rest/openapi.yaml
+++ b/src/rest/openapi.yaml
@@ -43,6 +43,15 @@
             application/json:
               schema:
                 type: object
+    delete:
+      tags:
+        - node
+      summary: Erase all persistent information, essentially factory reset the Border Router.
+      responses:
+        "200":
+          description: Successful operation
+        "409":
+          description: Thread interface is in wrong state.
   /node/ba-id:
     get:
       tags:
@@ -107,20 +116,38 @@
       summary: Get current Thread state.
       description: |-
         State describing the current Thread role of this Thread node.
-        - 0: disabled
-        - 1: detached
-        - 2: child
-        - 3: router
-        - 4: leader
+        - disabled: The Thread stack is disabled.
+        - detached: Not currently participating in a Thread network/partition.
+        - child: The Thread Child role.
+        - router: The Thread Router role.
+        - leader: The Thread Leader role.
       responses:
         "200":
           description: Successful operation
           content:
             application/json:
               schema:
-                type: number
+                type: string
                 description: Current state
-                example: 4
+                example: "leader"
+    put:
+      tags:
+        - node
+      summary: Set current Thread state.
+      description: |-
+        Enable and disable the Thread protocol operation. If network interface
+        hasn't been started yet, it will get started automatically.
+      responses:
+        "200":
+          description: Successful operation.
+      requestBody:
+        description: New Thread state
+        content:
+          application/json:
+            schema:
+              type: string
+              description: Can be "enable" or "disable".
+              example: "enable"
   /node/network-name:
     get:
       tags:
diff --git a/src/rest/parser.hpp b/src/rest/parser.hpp
index a90ad75..b79d79a 100644
--- a/src/rest/parser.hpp
+++ b/src/rest/parser.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_REST_PARSER_HPP_
 #define OTBR_REST_PARSER_HPP_
 
+#include "openthread-br/config.h"
+
 #include <memory>
 
 #include "rest/types.hpp"
diff --git a/src/rest/request.hpp b/src/rest/request.hpp
index 5b73b06..ceb92a5 100644
--- a/src/rest/request.hpp
+++ b/src/rest/request.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_REST_REQUEST_HPP_
 #define OTBR_REST_REQUEST_HPP_
 
+#include "openthread-br/config.h"
+
 #include <map>
 #include <string>
 
diff --git a/src/rest/resource.cpp b/src/rest/resource.cpp
index 4c8d257..a60e9d9 100644
--- a/src/rest/resource.cpp
+++ b/src/rest/resource.cpp
@@ -30,8 +30,6 @@
 
 #include "rest/resource.hpp"
 
-#include "string.h"
-
 #define OT_PSKC_MAX_LENGTH 16
 #define OT_EXTENDED_PANID_LENGTH 8
 
@@ -222,9 +220,8 @@
     uint8_t         maxRouterId;
     std::string     body;
     std::string     errorCode;
-    uint16_t        idLength = OT_BORDER_AGENT_ID_LENGTH;
 
-    VerifyOrExit(otBorderAgentGetId(mInstance, node.mBaId, &idLength) == OT_ERROR_NONE, error = OTBR_ERROR_REST);
+    VerifyOrExit(otBorderAgentGetId(mInstance, &node.mBaId) == OT_ERROR_NONE, error = OTBR_ERROR_REST);
     (void)otThreadGetLeaderData(mInstance, &node.mLeaderData);
 
     node.mNumOfRouter = 0;
@@ -238,7 +235,7 @@
         ++node.mNumOfRouter;
     }
 
-    node.mRole        = otThreadGetDeviceRole(mInstance);
+    node.mRole        = GetDeviceRoleName(otThreadGetDeviceRole(mInstance));
     node.mExtAddress  = reinterpret_cast<const uint8_t *>(otLinkGetExtendedAddress(mInstance));
     node.mNetworkName = otThreadGetNetworkName(mInstance);
     node.mRloc16      = otThreadGetRloc16(mInstance);
@@ -260,30 +257,59 @@
     }
 }
 
+void Resource::DeleteNodeInfo(Response &aResponse) const
+{
+    otbrError   error = OTBR_ERROR_NONE;
+    std::string errorCode;
+
+    VerifyOrExit(mNcp->GetThreadHelper()->Detach() == OT_ERROR_NONE, error = OTBR_ERROR_INVALID_STATE);
+    VerifyOrExit(otInstanceErasePersistentInfo(mInstance) == OT_ERROR_NONE, error = OTBR_ERROR_REST);
+    mNcp->Reset();
+
+exit:
+    if (error == OTBR_ERROR_NONE)
+    {
+        errorCode = GetHttpStatus(HttpStatusCode::kStatusOk);
+        aResponse.SetResponsCode(errorCode);
+    }
+    else if (error == OTBR_ERROR_INVALID_STATE)
+    {
+        ErrorHandler(aResponse, HttpStatusCode::kStatusConflict);
+    }
+    else if (error != OTBR_ERROR_NONE)
+    {
+        ErrorHandler(aResponse, HttpStatusCode::kStatusInternalServerError);
+    }
+}
+
 void Resource::NodeInfo(const Request &aRequest, Response &aResponse) const
 {
     std::string errorCode;
-    if (aRequest.GetMethod() == HttpMethod::kGet)
+
+    switch (aRequest.GetMethod())
     {
+    case HttpMethod::kGet:
         GetNodeInfo(aResponse);
-    }
-    else
-    {
+        break;
+    case HttpMethod::kDelete:
+        DeleteNodeInfo(aResponse);
+        break;
+    default:
         ErrorHandler(aResponse, HttpStatusCode::kStatusMethodNotAllowed);
+        break;
     }
 }
 
 void Resource::GetDataBaId(Response &aResponse) const
 {
-    otbrError   error = OTBR_ERROR_NONE;
-    uint8_t     id[OT_BORDER_AGENT_ID_LENGTH];
-    uint16_t    idLength = OT_BORDER_AGENT_ID_LENGTH;
-    std::string body;
-    std::string errorCode;
+    otbrError       error = OTBR_ERROR_NONE;
+    otBorderAgentId id;
+    std::string     body;
+    std::string     errorCode;
 
-    VerifyOrExit(otBorderAgentGetId(mInstance, id, &idLength) == OT_ERROR_NONE, error = OTBR_ERROR_REST);
+    VerifyOrExit(otBorderAgentGetId(mInstance, &id) == OT_ERROR_NONE, error = OTBR_ERROR_REST);
 
-    body = Json::Bytes2HexJsonString(id, idLength);
+    body = Json::Bytes2HexJsonString(id.mId, sizeof(id));
     aResponse.SetBody(body);
 
 exit:
@@ -339,33 +365,80 @@
 
 void Resource::GetDataState(Response &aResponse) const
 {
-    std::string state;
-    std::string errorCode;
-    uint8_t     role;
-    // 0 : disabled
-    // 1 : detached
-    // 2 : child
-    // 3 : router
-    // 4 : leader
+    std::string  state;
+    std::string  errorCode;
+    otDeviceRole role;
 
     role  = otThreadGetDeviceRole(mInstance);
-    state = Json::Number2JsonString(role);
+    state = Json::String2JsonString(GetDeviceRoleName(role));
     aResponse.SetBody(state);
     errorCode = GetHttpStatus(HttpStatusCode::kStatusOk);
     aResponse.SetResponsCode(errorCode);
 }
 
+void Resource::SetDataState(const Request &aRequest, Response &aResponse) const
+{
+    otbrError   error = OTBR_ERROR_NONE;
+    std::string errorCode;
+    std::string body;
+
+    VerifyOrExit(Json::JsonString2String(aRequest.GetBody(), body), error = OTBR_ERROR_INVALID_ARGS);
+    if (body == "enable")
+    {
+        if (!otIp6IsEnabled(mInstance))
+        {
+            VerifyOrExit(otIp6SetEnabled(mInstance, true) == OT_ERROR_NONE, error = OTBR_ERROR_INVALID_STATE);
+        }
+        VerifyOrExit(otThreadSetEnabled(mInstance, true) == OT_ERROR_NONE, error = OTBR_ERROR_INVALID_STATE);
+    }
+    else if (body == "disable")
+    {
+        VerifyOrExit(otThreadSetEnabled(mInstance, false) == OT_ERROR_NONE, error = OTBR_ERROR_INVALID_STATE);
+        VerifyOrExit(otIp6SetEnabled(mInstance, false) == OT_ERROR_NONE, error = OTBR_ERROR_INVALID_STATE);
+    }
+    else
+    {
+        ExitNow(error = OTBR_ERROR_INVALID_ARGS);
+    }
+
+    errorCode = GetHttpStatus(HttpStatusCode::kStatusOk);
+    aResponse.SetResponsCode(errorCode);
+
+exit:
+    if (error == OTBR_ERROR_INVALID_STATE)
+    {
+        ErrorHandler(aResponse, HttpStatusCode::kStatusConflict);
+    }
+    if (error == OTBR_ERROR_INVALID_ARGS)
+    {
+        ErrorHandler(aResponse, HttpStatusCode::kStatusBadRequest);
+    }
+    else if (error != OTBR_ERROR_NONE)
+    {
+        ErrorHandler(aResponse, HttpStatusCode::kStatusInternalServerError);
+    }
+}
+
 void Resource::State(const Request &aRequest, Response &aResponse) const
 {
     std::string errorCode;
 
-    if (aRequest.GetMethod() == HttpMethod::kGet)
+    switch (aRequest.GetMethod())
     {
+    case HttpMethod::kGet:
         GetDataState(aResponse);
-    }
-    else
-    {
+        break;
+    case HttpMethod::kPut:
+        SetDataState(aRequest, aResponse);
+        break;
+    case HttpMethod::kOptions:
+        errorCode = GetHttpStatus(HttpStatusCode::kStatusOk);
+        aResponse.SetResponsCode(errorCode);
+        aResponse.SetComplete();
+        break;
+    default:
         ErrorHandler(aResponse, HttpStatusCode::kStatusMethodNotAllowed);
+        break;
     }
 }
 
@@ -617,7 +690,7 @@
     struct NodeInfo          node;
     std::string              body;
     std::string              errorCode = GetHttpStatus(HttpStatusCode::kStatusOk);
-    otOperationalDataset     dataset;
+    otOperationalDataset     dataset   = {};
     otOperationalDatasetTlvs datasetTlvs;
     otOperationalDatasetTlvs datasetUpdateTlvs;
     int                      ret;
diff --git a/src/rest/resource.hpp b/src/rest/resource.hpp
index 7b5e592..d79085d 100644
--- a/src/rest/resource.hpp
+++ b/src/rest/resource.hpp
@@ -34,11 +34,14 @@
 #ifndef OTBR_REST_RESOURCE_HPP_
 #define OTBR_REST_RESOURCE_HPP_
 
+#include "openthread-br/config.h"
+
 #include <unordered_map>
 
 #include <openthread/border_agent.h>
 #include <openthread/border_router.h>
 
+#include "common/api_strings.hpp"
 #include "ncp/ncp_openthread.hpp"
 #include "openthread/dataset.h"
 #include "openthread/dataset_ftd.h"
@@ -134,9 +137,11 @@
     void HandleDiagnosticCallback(const Request &aRequest, Response &aResponse);
 
     void GetNodeInfo(Response &aResponse) const;
+    void DeleteNodeInfo(Response &aResponse) const;
     void GetDataBaId(Response &aResponse) const;
     void GetDataExtendedAddr(Response &aResponse) const;
     void GetDataState(Response &aResponse) const;
+    void SetDataState(const Request &aRequest, Response &aResponse) const;
     void GetDataNetworkName(Response &aResponse) const;
     void GetDataLeaderData(Response &aResponse) const;
     void GetDataNumOfRoute(Response &aResponse) const;
diff --git a/src/rest/response.cpp b/src/rest/response.cpp
index 93cbe0b..3460b90 100644
--- a/src/rest/response.cpp
+++ b/src/rest/response.cpp
@@ -34,7 +34,7 @@
 #define OT_REST_RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS                                                              \
     "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, " \
     "Access-Control-Request-Headers"
-#define OT_REST_RESPONSE_ACCESS_CONTROL_ALLOW_METHOD "GET, OPTIONS, PUT"
+#define OT_REST_RESPONSE_ACCESS_CONTROL_ALLOW_METHOD "DELETE, GET, OPTIONS, PUT"
 #define OT_REST_RESPONSE_CONNECTION "close"
 
 namespace otbr {
diff --git a/src/rest/response.hpp b/src/rest/response.hpp
index a7cb73e..2bab14c 100644
--- a/src/rest/response.hpp
+++ b/src/rest/response.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_REST_RESPONSE_HPP_
 #define OTBR_REST_RESPONSE_HPP_
 
+#include "openthread-br/config.h"
+
 #include <chrono>
 #include <map>
 #include <string>
diff --git a/src/rest/rest_web_server.hpp b/src/rest/rest_web_server.hpp
index 6e7956b..1da2e01 100644
--- a/src/rest/rest_web_server.hpp
+++ b/src/rest/rest_web_server.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_REST_REST_WEB_SERVER_HPP_
 #define OTBR_REST_REST_WEB_SERVER_HPP_
 
+#include "openthread-br/config.h"
+
 #include <netinet/in.h>
 #include <netinet/ip.h>
 #include <sys/socket.h>
diff --git a/src/rest/types.hpp b/src/rest/types.hpp
index 0723661..aaa11d1 100644
--- a/src/rest/types.hpp
+++ b/src/rest/types.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_REST_TYPES_HPP_
 #define OTBR_REST_TYPES_HPP_
 
+#include "openthread-br/config.h"
+
 #include <chrono>
 #include <string>
 #include <vector>
@@ -98,15 +100,15 @@
 };
 struct NodeInfo
 {
-    uint8_t        mBaId[OT_BORDER_AGENT_ID_LENGTH];
-    uint32_t       mRole;
-    uint32_t       mNumOfRouter;
-    uint16_t       mRloc16;
-    const uint8_t *mExtPanId;
-    const uint8_t *mExtAddress;
-    otIp6Address   mRlocAddress;
-    otLeaderData   mLeaderData;
-    std::string    mNetworkName;
+    otBorderAgentId mBaId;
+    std::string     mRole;
+    uint32_t        mNumOfRouter;
+    uint16_t        mRloc16;
+    const uint8_t  *mExtPanId;
+    const uint8_t  *mExtAddress;
+    otIp6Address    mRlocAddress;
+    otLeaderData    mLeaderData;
+    std::string     mNetworkName;
 };
 
 struct DiagInfo
diff --git a/src/sdp_proxy/advertising_proxy.cpp b/src/sdp_proxy/advertising_proxy.cpp
index d254ed0..7eac4d0 100644
--- a/src/sdp_proxy/advertising_proxy.cpp
+++ b/src/sdp_proxy/advertising_proxy.cpp
@@ -247,18 +247,16 @@
         aUpdate->mCallbackCount++;
         aUpdate->mHostName = hostName;
         service            = nullptr;
-        while ((service = otSrpServerHostFindNextService(aHost, service, OT_SRP_SERVER_FLAGS_BASE_TYPE_SERVICE_ONLY,
-                                                         /* aServiceName */ nullptr, /* aInstanceName */ nullptr)))
+        while ((service = otSrpServerHostGetNextService(aHost, service)) != nullptr)
         {
             aUpdate->mCallbackCount++;
         }
     }
 
     service = nullptr;
-    while ((service = otSrpServerHostFindNextService(aHost, service, OT_SRP_SERVER_FLAGS_BASE_TYPE_SERVICE_ONLY,
-                                                     /* aServiceName */ nullptr, /* aInstanceName */ nullptr)))
+    while ((service = otSrpServerHostGetNextService(aHost, service)) != nullptr)
     {
-        std::string fullServiceName = otSrpServerServiceGetFullName(service);
+        std::string fullServiceName = otSrpServerServiceGetInstanceName(service);
         std::string serviceName;
         std::string serviceType;
         std::string serviceDomain;
@@ -267,12 +265,12 @@
 
         if (!hostDeleted && !otSrpServerServiceIsDeleted(service))
         {
-            Mdns::Publisher::TxtList     txtList     = MakeTxtList(service);
+            Mdns::Publisher::TxtData     txtData     = MakeTxtData(service);
             Mdns::Publisher::SubTypeList subTypeList = MakeSubTypeList(service);
 
             otbrLogDebug("Publish SRP service '%s'", fullServiceName.c_str());
             mPublisher.PublishService(
-                hostName, serviceName, serviceType, subTypeList, otSrpServerServiceGetPort(service), txtList,
+                hostName, serviceName, serviceType, subTypeList, otSrpServerServiceGetPort(service), txtData,
                 [this, hasUpdate, updateId, fullServiceName](otbrError aError) {
                     otbrLogResult(aError, "Handle publish SRP service '%s'", fullServiceName.c_str());
                     if (hasUpdate)
@@ -340,49 +338,31 @@
     return error;
 }
 
-Mdns::Publisher::TxtList AdvertisingProxy::MakeTxtList(const otSrpServerService *aSrpService)
+Mdns::Publisher::TxtData AdvertisingProxy::MakeTxtData(const otSrpServerService *aSrpService)
 {
-    const uint8_t           *txtData;
-    uint16_t                 txtDataLength = 0;
-    otDnsTxtEntryIterator    iterator;
-    otDnsTxtEntry            txtEntry;
-    Mdns::Publisher::TxtList txtList;
+    const uint8_t *data;
+    uint16_t       length = 0;
 
-    txtData = otSrpServerServiceGetTxtData(aSrpService, &txtDataLength);
+    data = otSrpServerServiceGetTxtData(aSrpService, &length);
 
-    otDnsInitTxtEntryIterator(&iterator, txtData, txtDataLength);
-
-    while (otDnsGetNextTxtEntry(&iterator, &txtEntry) == OT_ERROR_NONE)
-    {
-        txtList.emplace_back(txtEntry.mKey, txtEntry.mValue, txtEntry.mValueLength);
-    }
-
-    return txtList;
+    return Mdns::Publisher::TxtData(data, data + length);
 }
 
 Mdns::Publisher::SubTypeList AdvertisingProxy::MakeSubTypeList(const otSrpServerService *aSrpService)
 {
-    const otSrpServerHost       *host         = otSrpServerServiceGetHost(aSrpService);
-    const char                  *instanceName = otSrpServerServiceGetInstanceName(aSrpService);
-    const otSrpServerService    *subService   = nullptr;
     Mdns::Publisher::SubTypeList subTypeList;
 
-    while ((subService = otSrpServerHostFindNextService(
-                host, subService, (OT_SRP_SERVER_SERVICE_FLAG_SUB_TYPE | OT_SRP_SERVER_SERVICE_FLAG_ACTIVE),
-                /* aServiceName */ nullptr, instanceName)) != nullptr)
+    for (uint16_t index = 0;; index++)
     {
-        char subLabel[OT_DNS_MAX_LABEL_SIZE];
+        const char *subTypeName = otSrpServerServiceGetSubTypeServiceNameAt(aSrpService, index);
+        char        subLabel[OT_DNS_MAX_LABEL_SIZE];
 
-        if (otSrpServerServiceGetServiceSubTypeLabel(subService, subLabel, sizeof(subLabel)) == OT_ERROR_NONE)
-        {
-            subTypeList.emplace_back(subLabel);
-        }
-        else
-        {
-            otbrLogWarning("Failed to retrieve subtype of SRP service: %s", otSrpServerServiceGetFullName(aSrpService));
-        }
+        VerifyOrExit(subTypeName != nullptr);
+        SuccessOrExit(otSrpServerParseSubTypeServiceName(subTypeName, subLabel, sizeof(subLabel)));
+        subTypeList.emplace_back(subLabel);
     }
 
+exit:
     return subTypeList;
 }
 
diff --git a/src/sdp_proxy/advertising_proxy.hpp b/src/sdp_proxy/advertising_proxy.hpp
index e07a7d2..385dadd 100644
--- a/src/sdp_proxy/advertising_proxy.hpp
+++ b/src/sdp_proxy/advertising_proxy.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_SRP_ADVERTISING_PROXY_HPP_
 #define OTBR_SRP_ADVERTISING_PROXY_HPP_
 
+#include "openthread-br/config.h"
+
 #if OTBR_ENABLE_SRP_ADVERTISING_PROXY
 
 #include <stdint.h>
@@ -98,7 +100,7 @@
                                    void                      *aContext);
     void        AdvertisingHandler(otSrpServerServiceUpdateId aId, const otSrpServerHost *aHost, uint32_t aTimeout);
 
-    static Mdns::Publisher::TxtList     MakeTxtList(const otSrpServerService *aSrpService);
+    static Mdns::Publisher::TxtData     MakeTxtData(const otSrpServerService *aSrpService);
     static Mdns::Publisher::SubTypeList MakeSubTypeList(const otSrpServerService *aSrpService);
     void                                OnMdnsPublishResult(otSrpServerServiceUpdateId aUpdateId, otbrError aError);
 
@@ -129,9 +131,6 @@
 
     // A vector that tracks outstanding updates.
     std::vector<OutstandingUpdate> mOutstandingUpdates;
-
-    // Task runner for running tasks in the context of the main thread.
-    TaskRunner mTaskRunner;
 };
 
 } // namespace otbr
diff --git a/src/sdp_proxy/discovery_proxy.hpp b/src/sdp_proxy/discovery_proxy.hpp
index 5a078ac..5263ab1 100644
--- a/src/sdp_proxy/discovery_proxy.hpp
+++ b/src/sdp_proxy/discovery_proxy.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_DISCOVERY_PROXY_HPP_
 #define OTBR_AGENT_DISCOVERY_PROXY_HPP_
 
+#include "openthread-br/config.h"
+
 #if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
 
 #include <set>
diff --git a/src/trel_dnssd/trel_dnssd.cpp b/src/trel_dnssd/trel_dnssd.cpp
index 11794fc..c285a83 100644
--- a/src/trel_dnssd/trel_dnssd.cpp
+++ b/src/trel_dnssd/trel_dnssd.cpp
@@ -249,7 +249,7 @@
 
     mRegisterInfo.mInstanceName = GetTrelInstanceName();
     mPublisher.PublishService(/* aHostName */ "", mRegisterInfo.mInstanceName, kTrelServiceName,
-                              Mdns::Publisher::SubTypeList{}, mRegisterInfo.mPort, mRegisterInfo.mTxtEntries,
+                              Mdns::Publisher::SubTypeList{}, mRegisterInfo.mPort, mRegisterInfo.mTxtData,
                               [](otbrError aError) { HandlePublishTrelServiceError(aError); });
 }
 
@@ -461,18 +461,11 @@
 
 void TrelDnssd::RegisterInfo::Assign(uint16_t aPort, const uint8_t *aTxtData, uint8_t aTxtLength)
 {
-    otbrError error;
-
-    OTBR_UNUSED_VARIABLE(error);
-
     assert(!IsPublished());
     assert(aPort > 0);
 
     mPort = aPort;
-    mTxtEntries.clear();
-
-    error = Mdns::Publisher::DecodeTxtData(mTxtEntries, aTxtData, aTxtLength);
-    assert(error == OTBR_ERROR_NONE);
+    mTxtData.assign(aTxtData, aTxtData + aTxtLength);
 }
 
 void TrelDnssd::RegisterInfo::Clear(void)
@@ -480,7 +473,7 @@
     assert(!IsPublished());
 
     mPort = 0;
-    mTxtEntries.clear();
+    mTxtData.clear();
 }
 
 const char TrelDnssd::Peer::kTxtRecordExtAddressKey[] = "xa";
@@ -495,7 +488,12 @@
 
     for (const auto &txtEntry : txtEntries)
     {
-        if (StringUtils::EqualCaseInsensitive(txtEntry.mName, kTxtRecordExtAddressKey))
+        if (txtEntry.mIsBooleanAttribute)
+        {
+            continue;
+        }
+
+        if (StringUtils::EqualCaseInsensitive(txtEntry.mKey, kTxtRecordExtAddressKey))
         {
             VerifyOrExit(txtEntry.mValue.size() == sizeof(mExtAddr));
 
diff --git a/src/trel_dnssd/trel_dnssd.hpp b/src/trel_dnssd/trel_dnssd.hpp
index b00eff5..8f44104 100644
--- a/src/trel_dnssd/trel_dnssd.hpp
+++ b/src/trel_dnssd/trel_dnssd.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_AGENT_TREL_DNSSD_HPP_
 #define OTBR_AGENT_TREL_DNSSD_HPP_
 
+#include "openthread-br/config.h"
+
 #if OTBR_ENABLE_TREL
 
 #include <assert.h>
@@ -118,9 +120,9 @@
 
     struct RegisterInfo
     {
-        uint16_t                               mPort = 0;
-        std::vector<Mdns::Publisher::TxtEntry> mTxtEntries;
-        std::string                            mInstanceName;
+        uint16_t                 mPort = 0;
+        Mdns::Publisher::TxtData mTxtData;
+        std::string              mInstanceName;
 
         bool IsValid(void) const { return mPort > 0; }
         bool IsPublished(void) const { return !mInstanceName.empty(); }
diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt
index 5a62484..4f9c420 100644
--- a/src/utils/CMakeLists.txt
+++ b/src/utils/CMakeLists.txt
@@ -32,6 +32,7 @@
     hex.cpp
     infra_link_selector.cpp
     pskc.cpp
+    sha256.cpp
     socket_utils.cpp
     steering_data.cpp
     string_utils.cpp
@@ -39,7 +40,10 @@
     thread_helper.cpp
     thread_helper.hpp
 )
-target_link_libraries(otbr-utils PRIVATE
+
+target_link_libraries(otbr-utils PUBLIC
     otbr-common
+    $<$<BOOL:${OTBR_FEATURE_FLAGS}>:otbr-proto>
+    $<$<BOOL:${OTBR_TELEMETRY_DATA_API}>:otbr-proto>
     mbedtls
 )
diff --git a/src/utils/infra_link_selector.hpp b/src/utils/infra_link_selector.hpp
index 02a231b..f2bcd83 100644
--- a/src/utils/infra_link_selector.hpp
+++ b/src/utils/infra_link_selector.hpp
@@ -34,6 +34,8 @@
 #ifndef INFRA_LINK_SELECTOR_HPP_
 #define INFRA_LINK_SELECTOR_HPP_
 
+#include "openthread-br/config.h"
+
 #if __linux__
 
 #include <assert.h>
diff --git a/src/utils/sha256.cpp b/src/utils/sha256.cpp
new file mode 100644
index 0000000..30be250
--- /dev/null
+++ b/src/utils/sha256.cpp
@@ -0,0 +1,105 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file implements SHA-256.
+ */
+
+#include "sha256.hpp"
+
+namespace otbr {
+
+Sha256::Sha256(void)
+{
+    otError error;
+
+    mContext.mContext     = &mContextStorage;
+    mContext.mContextSize = sizeof(mContextStorage);
+
+    SuccessOrExit(error = otPlatCryptoSha256Init(&mContext));
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogErr("Error otPlatCryptoSha256Init: %s", otThreadErrorToString(error));
+    }
+}
+
+Sha256::~Sha256(void)
+{
+    otError error;
+
+    SuccessOrExit(error = otPlatCryptoSha256Deinit(&mContext));
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogErr("Error otPlatCryptoSha256Deinit: %s", otThreadErrorToString(error));
+    }
+}
+
+void Sha256::Start(void)
+{
+    otError error;
+
+    SuccessOrExit(error = otPlatCryptoSha256Start(&mContext));
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogErr("Error otPlatCryptoSha256Start: %s", otThreadErrorToString(error));
+    }
+}
+
+void Sha256::Update(const void *aBuf, uint16_t aBufLength)
+{
+    otError error;
+
+    SuccessOrExit(error = otPlatCryptoSha256Update(&mContext, aBuf, aBufLength));
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogErr("Error otPlatCryptoSha256Update: %s", otThreadErrorToString(error));
+    }
+}
+
+void Sha256::Finish(Hash &aHash)
+{
+    otError error;
+
+    SuccessOrExit(error = otPlatCryptoSha256Finish(&mContext, aHash.m8, Hash::kSize));
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogErr("Error otPlatCryptoSha256Finish: %s", otThreadErrorToString(error));
+    }
+}
+} // namespace otbr
diff --git a/src/utils/sha256.hpp b/src/utils/sha256.hpp
new file mode 100644
index 0000000..df90a84
--- /dev/null
+++ b/src/utils/sha256.hpp
@@ -0,0 +1,118 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file includes definitions for performing SHA-256 computations.
+ */
+
+#ifndef SHA256_HPP_
+#define SHA256_HPP_
+
+#include <openthread/crypto.h>
+#include <openthread/platform/crypto.h>
+#include "common/code_utils.hpp"
+
+#include <mbedtls/sha256.h>
+
+namespace otbr {
+/**
+ * @addtogroup core-security
+ *
+ * @{
+ *
+ */
+
+/**
+ * This class implements SHA-256 computation.
+ *
+ */
+class Sha256
+{
+public:
+    /**
+     * This type represents a SHA-256 hash.
+     *
+     */
+    class Hash : public otCryptoSha256Hash
+    {
+    public:
+        static const uint8_t kSize = OT_CRYPTO_SHA256_HASH_SIZE; ///< SHA-256 hash size (bytes)
+
+        /**
+         * This method returns a pointer to a byte array containing the hash value.
+         *
+         * @returns A pointer to a byte array containing the hash.
+         *
+         */
+        const uint8_t *GetBytes(void) const { return m8; }
+    };
+
+    /**
+     * Constructor for `Sha256` object.
+     *
+     */
+    Sha256(void);
+
+    /**
+     * Destructor for `Sha256` object.
+     *
+     */
+    ~Sha256(void);
+
+    /**
+     * This method starts the SHA-256 computation.
+     *
+     */
+    void Start(void);
+
+    /**
+     * This method inputs bytes into the SHA-256 computation.
+     *
+     * @param[in]  aBuf        A pointer to the input buffer.
+     * @param[in]  aBufLength  The length of @p aBuf in bytes.
+     *
+     */
+    void Update(const void *aBuf, uint16_t aBufLength);
+
+    /**
+     * This method finalizes the hash computation.
+     *
+     * @param[out]  aHash  A reference to a `Hash` to output the calculated hash.
+     *
+     */
+    void Finish(Hash &aHash);
+
+private:
+    otCryptoContext       mContext;
+    const static uint16_t kSha256ContextSize = sizeof(mbedtls_sha256_context);
+    OT_DEFINE_ALIGNED_VAR(mContextStorage, kSha256ContextSize, uint64_t);
+};
+} // namespace otbr
+
+#endif // SHA256_HPP_
diff --git a/src/utils/system_utils.hpp b/src/utils/system_utils.hpp
index 4ae2dad..2af1ae2 100644
--- a/src/utils/system_utils.hpp
+++ b/src/utils/system_utils.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_UTILS_SYSTEM_UTILS_HPP_
 #define OTBR_UTILS_SYSTEM_UTILS_HPP_
 
+#include "openthread-br/config.h"
+
 namespace otbr {
 namespace SystemUtils {
 
diff --git a/src/utils/thread_helper.cpp b/src/utils/thread_helper.cpp
index 9c69b36..9cd2dd8 100644
--- a/src/utils/thread_helper.cpp
+++ b/src/utils/thread_helper.cpp
@@ -39,8 +39,20 @@
 
 #include <openthread/border_router.h>
 #include <openthread/channel_manager.h>
+#include <openthread/dataset_ftd.h>
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+#include <openthread/dnssd_server.h>
+#endif
 #include <openthread/jam_detection.h>
 #include <openthread/joiner.h>
+#if OTBR_ENABLE_NAT64
+#include <openthread/crypto.h>
+#include <openthread/nat64.h>
+#include "utils/sha256.hpp"
+#endif
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+#include <openthread/srp_server.h>
+#endif
 #include <openthread/thread_ftd.h>
 #include <openthread/platform/radio.h>
 
@@ -69,12 +81,135 @@
 exit:
     return result;
 }
+
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+static uint32_t TelemetryNodeTypeFromRoleAndLinkMode(const otDeviceRole &aRole, const otLinkModeConfig &aLinkModeCfg)
+{
+    uint32_t nodeType;
+
+    switch (aRole)
+    {
+    case OT_DEVICE_ROLE_DISABLED:
+        nodeType = threadnetwork::TelemetryData::NODE_TYPE_DISABLED;
+        break;
+    case OT_DEVICE_ROLE_DETACHED:
+        nodeType = threadnetwork::TelemetryData::NODE_TYPE_DETACHED;
+        break;
+    case OT_DEVICE_ROLE_ROUTER:
+        nodeType = threadnetwork::TelemetryData::NODE_TYPE_ROUTER;
+        break;
+    case OT_DEVICE_ROLE_LEADER:
+        nodeType = threadnetwork::TelemetryData::NODE_TYPE_LEADER;
+        break;
+    case OT_DEVICE_ROLE_CHILD:
+        if (!aLinkModeCfg.mRxOnWhenIdle)
+        {
+            nodeType = threadnetwork::TelemetryData::NODE_TYPE_SLEEPY_END;
+        }
+        else if (!aLinkModeCfg.mDeviceType)
+        {
+            // If it's not an FTD, return as minimal end device.
+            nodeType = threadnetwork::TelemetryData::NODE_TYPE_MINIMAL_END;
+        }
+        else
+        {
+            nodeType = threadnetwork::TelemetryData::NODE_TYPE_END;
+        }
+        break;
+    default:
+        nodeType = threadnetwork::TelemetryData::NODE_TYPE_UNSPECIFIED;
+    }
+
+    return nodeType;
+}
+
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+threadnetwork::TelemetryData_SrpServerState SrpServerStateFromOtSrpServerState(otSrpServerState srpServerState)
+{
+    switch (srpServerState)
+    {
+    case OT_SRP_SERVER_STATE_DISABLED:
+        return threadnetwork::TelemetryData::SRP_SERVER_STATE_DISABLED;
+    case OT_SRP_SERVER_STATE_RUNNING:
+        return threadnetwork::TelemetryData::SRP_SERVER_STATE_RUNNING;
+    case OT_SRP_SERVER_STATE_STOPPED:
+        return threadnetwork::TelemetryData::SRP_SERVER_STATE_STOPPED;
+    default:
+        return threadnetwork::TelemetryData::SRP_SERVER_STATE_UNSPECIFIED;
+    }
+}
+
+threadnetwork::TelemetryData_SrpServerAddressMode SrpServerAddressModeFromOtSrpServerAddressMode(
+    otSrpServerAddressMode srpServerAddressMode)
+{
+    switch (srpServerAddressMode)
+    {
+    case OT_SRP_SERVER_ADDRESS_MODE_ANYCAST:
+        return threadnetwork::TelemetryData::SRP_SERVER_ADDRESS_MODE_STATE_ANYCAST;
+    case OT_SRP_SERVER_ADDRESS_MODE_UNICAST:
+        return threadnetwork::TelemetryData::SRP_SERVER_ADDRESS_MODE_UNICAST;
+    default:
+        return threadnetwork::TelemetryData::SRP_SERVER_ADDRESS_MODE_UNSPECIFIED;
+    }
+}
+#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY
+
+#if OTBR_ENABLE_NAT64
+threadnetwork::TelemetryData_Nat64State Nat64StateFromOtNat64State(otNat64State nat64State)
+{
+    switch (nat64State)
+    {
+    case OT_NAT64_STATE_DISABLED:
+        return threadnetwork::TelemetryData::NAT64_STATE_DISABLED;
+    case OT_NAT64_STATE_NOT_RUNNING:
+        return threadnetwork::TelemetryData::NAT64_STATE_NOT_RUNNING;
+    case OT_NAT64_STATE_IDLE:
+        return threadnetwork::TelemetryData::NAT64_STATE_IDLE;
+    case OT_NAT64_STATE_ACTIVE:
+        return threadnetwork::TelemetryData::NAT64_STATE_ACTIVE;
+    default:
+        return threadnetwork::TelemetryData::NAT64_STATE_UNSPECIFIED;
+    }
+}
+
+void CopyNat64TrafficCounters(const otNat64Counters &from, threadnetwork::TelemetryData_Nat64TrafficCounters *to)
+{
+    to->set_ipv4_to_ipv6_packets(from.m4To6Packets);
+    to->set_ipv4_to_ipv6_bytes(from.m4To6Bytes);
+    to->set_ipv6_to_ipv4_packets(from.m6To4Packets);
+    to->set_ipv6_to_ipv4_bytes(from.m6To4Bytes);
+}
+#endif // OTBR_ENABLE_NAT64
+
+void CopyMdnsResponseCounters(const MdnsResponseCounters &from, threadnetwork::TelemetryData_MdnsResponseCounters *to)
+{
+    to->set_success_count(from.mSuccess);
+    to->set_not_found_count(from.mNotFound);
+    to->set_invalid_args_count(from.mInvalidArgs);
+    to->set_duplicated_count(from.mDuplicated);
+    to->set_not_implemented_count(from.mNotImplemented);
+    to->set_unknown_error_count(from.mUnknownError);
+    to->set_aborted_count(from.mAborted);
+    to->set_invalid_state_count(from.mInvalidState);
+}
+#endif // OTBR_ENABLE_TELEMETRY_DATA_API
 } // namespace
 
 ThreadHelper::ThreadHelper(otInstance *aInstance, otbr::Ncp::ControllerOpenThread *aNcp)
     : mInstance(aInstance)
     , mNcp(aNcp)
 {
+#if OTBR_ENABLE_TELEMETRY_DATA_API && OTBR_ENABLE_NAT64
+    otError error;
+
+    SuccessOrExit(error = otPlatCryptoRandomGet(mNat64Ipv6AddressSalt, sizeof(mNat64Ipv6AddressSalt)));
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogWarning("Error otPlatCryptoRandomGet: %s", otThreadErrorToString(error));
+    }
+#endif
 }
 
 void ThreadHelper::StateChangedCallback(otChangedFlags aFlags)
@@ -321,79 +456,52 @@
                           AttachHandler               aHandler)
 
 {
-    otError         error = OT_ERROR_NONE;
-    otExtendedPanId extPanId;
-    otNetworkKey    networkKey;
-    otPskc          pskc;
-    uint32_t        channelMask;
-    uint8_t         channel;
+    otError              error   = OT_ERROR_NONE;
+    otOperationalDataset dataset = {};
 
     VerifyOrExit(aHandler != nullptr, error = OT_ERROR_INVALID_ARGS);
     VerifyOrExit(mAttachHandler == nullptr && mJoinerHandler == nullptr, error = OT_ERROR_INVALID_STATE);
-    VerifyOrExit(aNetworkKey.empty() || aNetworkKey.size() == sizeof(networkKey.m8), error = OT_ERROR_INVALID_ARGS);
-    VerifyOrExit(aPSKc.empty() || aPSKc.size() == sizeof(pskc.m8), error = OT_ERROR_INVALID_ARGS);
+    VerifyOrExit(aNetworkKey.empty() || aNetworkKey.size() == sizeof(dataset.mNetworkKey.m8),
+                 error = OT_ERROR_INVALID_ARGS);
+    VerifyOrExit(aPSKc.empty() || aPSKc.size() == sizeof(dataset.mPskc.m8), error = OT_ERROR_INVALID_ARGS);
     VerifyOrExit(aChannelMask != 0, error = OT_ERROR_INVALID_ARGS);
 
-    while (aPanId == UINT16_MAX)
-    {
-        RandomFill(&aPanId, sizeof(aPanId));
-    }
+    SuccessOrExit(error = otDatasetCreateNewNetwork(mInstance, &dataset));
 
     if (aExtPanId != UINT64_MAX)
     {
-        extPanId = ToOtExtendedPanId(aExtPanId);
-    }
-    else
-    {
-        *reinterpret_cast<uint64_t *>(&extPanId) = UINT64_MAX;
-
-        while (*reinterpret_cast<uint64_t *>(&extPanId) == UINT64_MAX)
-        {
-            RandomFill(extPanId.m8, sizeof(extPanId.m8));
-        }
+        dataset.mExtendedPanId = ToOtExtendedPanId(aExtPanId);
     }
 
     if (!aNetworkKey.empty())
     {
-        memcpy(networkKey.m8, &aNetworkKey[0], sizeof(networkKey.m8));
+        memcpy(dataset.mNetworkKey.m8, &aNetworkKey[0], sizeof(dataset.mNetworkKey.m8));
     }
-    else
+
+    if (aPanId != UINT16_MAX)
     {
-        RandomFill(networkKey.m8, sizeof(networkKey.m8));
+        dataset.mPanId = aPanId;
     }
 
     if (!aPSKc.empty())
     {
-        memcpy(pskc.m8, &aPSKc[0], sizeof(pskc.m8));
+        memcpy(dataset.mPskc.m8, &aPSKc[0], sizeof(dataset.mPskc.m8));
     }
-    else
-    {
-        RandomFill(pskc.m8, sizeof(pskc.m8));
-    }
+
+    SuccessOrExit(error = otNetworkNameFromString(&dataset.mNetworkName, aNetworkName.c_str()));
+
+    dataset.mChannelMask &= aChannelMask;
+    VerifyOrExit(dataset.mChannelMask != 0, otbrLogWarning("Invalid channel mask"), error = OT_ERROR_INVALID_ARGS);
+
+    dataset.mChannel = RandomChannelFromChannelMask(dataset.mChannelMask);
+
+    SuccessOrExit(error = otDatasetSetActive(mInstance, &dataset));
 
     if (!otIp6IsEnabled(mInstance))
     {
         SuccessOrExit(error = otIp6SetEnabled(mInstance, true));
     }
 
-    SuccessOrExit(error = otThreadSetNetworkName(mInstance, aNetworkName.c_str()));
-    SuccessOrExit(error = otLinkSetPanId(mInstance, aPanId));
-    SuccessOrExit(error = otThreadSetExtendedPanId(mInstance, &extPanId));
-    SuccessOrExit(error = otThreadSetNetworkKey(mInstance, &networkKey));
-
-    channelMask = otPlatRadioGetPreferredChannelMask(mInstance) & aChannelMask;
-
-    if (channelMask == 0)
-    {
-        channelMask = otLinkGetSupportedChannelMask(mInstance) & aChannelMask;
-    }
-    VerifyOrExit(channelMask != 0, otbrLogWarning("Invalid channel mask"), error = OT_ERROR_INVALID_ARGS);
-
-    channel = RandomChannelFromChannelMask(channelMask);
-    SuccessOrExit(otLinkSetChannel(mInstance, channel));
-
-    SuccessOrExit(error = otThreadSetPskc(mInstance, &pskc));
-
     SuccessOrExit(error = otThreadSetEnabled(mInstance, true));
     mAttachDelayMs = 0;
     mAttachHandler = aHandler;
@@ -779,5 +887,505 @@
     }
 }
 
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+otError ThreadHelper::RetrieveTelemetryData(Mdns::Publisher *aPublisher, threadnetwork::TelemetryData &telemetryData)
+{
+    otError error = OT_ERROR_NONE;
+
+    // Begin of WpanStats section.
+    auto wpanStats = telemetryData.mutable_wpan_stats();
+
+    {
+        otDeviceRole     role  = otThreadGetDeviceRole(mInstance);
+        otLinkModeConfig otCfg = otThreadGetLinkMode(mInstance);
+
+        wpanStats->set_node_type(TelemetryNodeTypeFromRoleAndLinkMode(role, otCfg));
+    }
+
+    wpanStats->set_channel(otLinkGetChannel(mInstance));
+
+    {
+        uint16_t ccaFailureRate = otLinkGetCcaFailureRate(mInstance);
+
+        wpanStats->set_mac_cca_fail_rate(static_cast<float>(ccaFailureRate) / 0xffff);
+    }
+
+    {
+        int8_t radioTxPower;
+
+        SuccessOrExit(error = otPlatRadioGetTransmitPower(mInstance, &radioTxPower));
+        wpanStats->set_radio_tx_power(radioTxPower);
+    }
+
+    {
+        const otMacCounters *linkCounters = otLinkGetCounters(mInstance);
+
+        wpanStats->set_phy_rx(linkCounters->mRxTotal);
+        wpanStats->set_phy_tx(linkCounters->mTxTotal);
+        wpanStats->set_mac_unicast_rx(linkCounters->mRxUnicast);
+        wpanStats->set_mac_unicast_tx(linkCounters->mTxUnicast);
+        wpanStats->set_mac_broadcast_rx(linkCounters->mRxBroadcast);
+        wpanStats->set_mac_broadcast_tx(linkCounters->mTxBroadcast);
+        wpanStats->set_mac_tx_ack_req(linkCounters->mTxAckRequested);
+        wpanStats->set_mac_tx_no_ack_req(linkCounters->mTxNoAckRequested);
+        wpanStats->set_mac_tx_acked(linkCounters->mTxAcked);
+        wpanStats->set_mac_tx_data(linkCounters->mTxData);
+        wpanStats->set_mac_tx_data_poll(linkCounters->mTxDataPoll);
+        wpanStats->set_mac_tx_beacon(linkCounters->mTxBeacon);
+        wpanStats->set_mac_tx_beacon_req(linkCounters->mTxBeaconRequest);
+        wpanStats->set_mac_tx_other_pkt(linkCounters->mTxOther);
+        wpanStats->set_mac_tx_retry(linkCounters->mTxRetry);
+        wpanStats->set_mac_rx_data(linkCounters->mRxData);
+        wpanStats->set_mac_rx_data_poll(linkCounters->mRxDataPoll);
+        wpanStats->set_mac_rx_beacon(linkCounters->mRxBeacon);
+        wpanStats->set_mac_rx_beacon_req(linkCounters->mRxBeaconRequest);
+        wpanStats->set_mac_rx_other_pkt(linkCounters->mRxOther);
+        wpanStats->set_mac_rx_filter_whitelist(linkCounters->mRxAddressFiltered);
+        wpanStats->set_mac_rx_filter_dest_addr(linkCounters->mRxDestAddrFiltered);
+        wpanStats->set_mac_tx_fail_cca(linkCounters->mTxErrCca);
+        wpanStats->set_mac_rx_fail_decrypt(linkCounters->mRxErrSec);
+        wpanStats->set_mac_rx_fail_no_frame(linkCounters->mRxErrNoFrame);
+        wpanStats->set_mac_rx_fail_unknown_neighbor(linkCounters->mRxErrUnknownNeighbor);
+        wpanStats->set_mac_rx_fail_invalid_src_addr(linkCounters->mRxErrInvalidSrcAddr);
+        wpanStats->set_mac_rx_fail_fcs(linkCounters->mRxErrFcs);
+        wpanStats->set_mac_rx_fail_other(linkCounters->mRxErrOther);
+    }
+
+    {
+        const otIpCounters *ipCounters = otThreadGetIp6Counters(mInstance);
+
+        wpanStats->set_ip_tx_success(ipCounters->mTxSuccess);
+        wpanStats->set_ip_rx_success(ipCounters->mRxSuccess);
+        wpanStats->set_ip_tx_failure(ipCounters->mTxFailure);
+        wpanStats->set_ip_rx_failure(ipCounters->mRxFailure);
+    }
+    // End of WpanStats section.
+
+    {
+        // Begin of WpanTopoFull section.
+        auto     wpanTopoFull = telemetryData.mutable_wpan_topo_full();
+        uint16_t rloc16       = otThreadGetRloc16(mInstance);
+
+        wpanTopoFull->set_rloc16(rloc16);
+
+        otRouterInfo info;
+
+        VerifyOrExit(otThreadGetRouterInfo(mInstance, rloc16, &info) == OT_ERROR_NONE, error = OT_ERROR_INVALID_STATE);
+        wpanTopoFull->set_router_id(info.mRouterId);
+
+        otNeighborInfoIterator      iter = OT_NEIGHBOR_INFO_ITERATOR_INIT;
+        otNeighborInfo              neighborInfo;
+        std::vector<otNeighborInfo> neighborTable;
+
+        while (otThreadGetNextNeighborInfo(mInstance, &iter, &neighborInfo) == OT_ERROR_NONE)
+        {
+            neighborTable.push_back(neighborInfo);
+        }
+        wpanTopoFull->set_neighbor_table_size(neighborTable.size());
+
+        uint16_t                 childIndex = 0;
+        otChildInfo              childInfo;
+        std::vector<otChildInfo> childTable;
+
+        while (otThreadGetChildInfoByIndex(mInstance, childIndex, &childInfo) == OT_ERROR_NONE)
+        {
+            childTable.push_back(childInfo);
+            childIndex++;
+        }
+        wpanTopoFull->set_child_table_size(childTable.size());
+
+        struct otLeaderData leaderData;
+
+        SuccessOrExit(error = otThreadGetLeaderData(mInstance, &leaderData));
+        wpanTopoFull->set_leader_router_id(leaderData.mLeaderRouterId);
+        wpanTopoFull->set_leader_weight(leaderData.mWeighting);
+        wpanTopoFull->set_network_data_version(leaderData.mDataVersion);
+        wpanTopoFull->set_stable_network_data_version(leaderData.mStableDataVersion);
+
+        uint8_t weight = otThreadGetLocalLeaderWeight(mInstance);
+
+        wpanTopoFull->set_leader_local_weight(weight);
+
+        uint32_t partitionId = otThreadGetPartitionId(mInstance);
+
+        wpanTopoFull->set_partition_id(partitionId);
+
+        static constexpr size_t kNetworkDataMaxSize = 255;
+        {
+            uint8_t              data[kNetworkDataMaxSize];
+            uint8_t              len = sizeof(data);
+            std::vector<uint8_t> networkData;
+
+            SuccessOrExit(error = otNetDataGet(mInstance, /*stable=*/false, data, &len));
+            networkData = std::vector<uint8_t>(&data[0], &data[len]);
+            wpanTopoFull->set_network_data(std::string(networkData.begin(), networkData.end()));
+        }
+
+        {
+            uint8_t              data[kNetworkDataMaxSize];
+            uint8_t              len = sizeof(data);
+            std::vector<uint8_t> networkData;
+
+            SuccessOrExit(error = otNetDataGet(mInstance, /*stable=*/true, data, &len));
+            networkData = std::vector<uint8_t>(&data[0], &data[len]);
+            wpanTopoFull->set_stable_network_data(std::string(networkData.begin(), networkData.end()));
+        }
+
+        int8_t rssi = otPlatRadioGetRssi(mInstance);
+
+        wpanTopoFull->set_instant_rssi(rssi);
+
+        const otExtendedPanId *extPanId = otThreadGetExtendedPanId(mInstance);
+        uint64_t               extPanIdVal;
+
+        extPanIdVal = ConvertOpenThreadUint64(extPanId->m8);
+        wpanTopoFull->set_extended_pan_id(extPanIdVal);
+        // End of WpanTopoFull section.
+
+        // Begin of TopoEntry section.
+        std::map<uint16_t, const otChildInfo *> childMap;
+
+        for (const otChildInfo &childInfo : childTable)
+        {
+            auto pair = childMap.insert({childInfo.mRloc16, &childInfo});
+            if (!pair.second)
+            {
+                // This shouldn't happen, so log an error. It doesn't matter which
+                // duplicate is kept.
+                otbrLogErr("Children with duplicate RLOC16 found: 0x%04x", static_cast<int>(childInfo.mRloc16));
+            }
+        }
+
+        for (const otNeighborInfo &neighborInfo : neighborTable)
+        {
+            auto topoEntry = telemetryData.add_topo_entries();
+            topoEntry->set_rloc16(neighborInfo.mRloc16);
+            topoEntry->mutable_age()->set_seconds(neighborInfo.mAge);
+            topoEntry->set_link_quality_in(neighborInfo.mLinkQualityIn);
+            topoEntry->set_average_rssi(neighborInfo.mAverageRssi);
+            topoEntry->set_last_rssi(neighborInfo.mLastRssi);
+            topoEntry->set_link_frame_counter(neighborInfo.mLinkFrameCounter);
+            topoEntry->set_mle_frame_counter(neighborInfo.mMleFrameCounter);
+            topoEntry->set_rx_on_when_idle(neighborInfo.mRxOnWhenIdle);
+            topoEntry->set_secure_data_request(true);
+            topoEntry->set_full_function(neighborInfo.mFullThreadDevice);
+            topoEntry->set_full_network_data(neighborInfo.mFullNetworkData);
+            topoEntry->set_mac_frame_error_rate(static_cast<float>(neighborInfo.mFrameErrorRate) / 0xffff);
+            topoEntry->set_ip_message_error_rate(static_cast<float>(neighborInfo.mMessageErrorRate) / 0xffff);
+            topoEntry->set_version(neighborInfo.mVersion);
+
+            if (!neighborInfo.mIsChild)
+            {
+                continue;
+            }
+
+            auto it = childMap.find(neighborInfo.mRloc16);
+            if (it == childMap.end())
+            {
+                otbrLogErr("Neighbor 0x%04x not found in child table", static_cast<int>(neighborInfo.mRloc16));
+                continue;
+            }
+            const otChildInfo *childInfo = it->second;
+            topoEntry->set_is_child(true);
+            topoEntry->mutable_timeout()->set_seconds(childInfo->mTimeout);
+            topoEntry->set_network_data_version(childInfo->mNetworkDataVersion);
+        }
+        // End of TopoEntry section.
+    }
+
+    {
+        // Begin of WpanBorderRouter section.
+        auto wpanBorderRouter = telemetryData.mutable_wpan_border_router();
+        // Begin of BorderRoutingCounters section.
+        auto                           borderRoutingCouters    = wpanBorderRouter->mutable_border_routing_counters();
+        const otBorderRoutingCounters *otBorderRoutingCounters = otIp6GetBorderRoutingCounters(mInstance);
+
+        borderRoutingCouters->mutable_inbound_unicast()->set_packet_count(
+            otBorderRoutingCounters->mInboundUnicast.mPackets);
+        borderRoutingCouters->mutable_inbound_unicast()->set_byte_count(
+            otBorderRoutingCounters->mInboundUnicast.mBytes);
+        borderRoutingCouters->mutable_inbound_multicast()->set_packet_count(
+            otBorderRoutingCounters->mInboundMulticast.mPackets);
+        borderRoutingCouters->mutable_inbound_multicast()->set_byte_count(
+            otBorderRoutingCounters->mInboundMulticast.mBytes);
+        borderRoutingCouters->mutable_outbound_unicast()->set_packet_count(
+            otBorderRoutingCounters->mOutboundUnicast.mPackets);
+        borderRoutingCouters->mutable_outbound_unicast()->set_byte_count(
+            otBorderRoutingCounters->mOutboundUnicast.mBytes);
+        borderRoutingCouters->mutable_outbound_multicast()->set_packet_count(
+            otBorderRoutingCounters->mOutboundMulticast.mPackets);
+        borderRoutingCouters->mutable_outbound_multicast()->set_byte_count(
+            otBorderRoutingCounters->mOutboundMulticast.mBytes);
+        borderRoutingCouters->set_ra_rx(otBorderRoutingCounters->mRaRx);
+        borderRoutingCouters->set_ra_tx_success(otBorderRoutingCounters->mRaTxSuccess);
+        borderRoutingCouters->set_ra_tx_failure(otBorderRoutingCounters->mRaTxFailure);
+        borderRoutingCouters->set_rs_rx(otBorderRoutingCounters->mRsRx);
+        borderRoutingCouters->set_rs_tx_success(otBorderRoutingCounters->mRsTxSuccess);
+        borderRoutingCouters->set_rs_tx_failure(otBorderRoutingCounters->mRsTxFailure);
+
+#if OTBR_ENABLE_NAT64
+        {
+            auto nat64IcmpCounters = borderRoutingCouters->mutable_nat64_protocol_counters()->mutable_icmp();
+            auto nat64UdpCounters  = borderRoutingCouters->mutable_nat64_protocol_counters()->mutable_udp();
+            auto nat64TcpCounters  = borderRoutingCouters->mutable_nat64_protocol_counters()->mutable_tcp();
+            otNat64ProtocolCounters otCounters;
+
+            otNat64GetCounters(mInstance, &otCounters);
+            nat64IcmpCounters->set_ipv4_to_ipv6_packets(otCounters.mIcmp.m4To6Packets);
+            nat64IcmpCounters->set_ipv4_to_ipv6_bytes(otCounters.mIcmp.m4To6Bytes);
+            nat64IcmpCounters->set_ipv6_to_ipv4_packets(otCounters.mIcmp.m6To4Packets);
+            nat64IcmpCounters->set_ipv6_to_ipv4_bytes(otCounters.mIcmp.m6To4Bytes);
+            nat64UdpCounters->set_ipv4_to_ipv6_packets(otCounters.mUdp.m4To6Packets);
+            nat64UdpCounters->set_ipv4_to_ipv6_bytes(otCounters.mUdp.m4To6Bytes);
+            nat64UdpCounters->set_ipv6_to_ipv4_packets(otCounters.mUdp.m6To4Packets);
+            nat64UdpCounters->set_ipv6_to_ipv4_bytes(otCounters.mUdp.m6To4Bytes);
+            nat64TcpCounters->set_ipv4_to_ipv6_packets(otCounters.mTcp.m4To6Packets);
+            nat64TcpCounters->set_ipv4_to_ipv6_bytes(otCounters.mTcp.m4To6Bytes);
+            nat64TcpCounters->set_ipv6_to_ipv4_packets(otCounters.mTcp.m6To4Packets);
+            nat64TcpCounters->set_ipv6_to_ipv4_bytes(otCounters.mTcp.m6To4Bytes);
+        }
+
+        {
+            auto                 errorCounters = borderRoutingCouters->mutable_nat64_error_counters();
+            otNat64ErrorCounters otCounters;
+            otNat64GetErrorCounters(mInstance, &otCounters);
+
+            errorCounters->mutable_unknown()->set_ipv4_to_ipv6_packets(
+                otCounters.mCount4To6[OT_NAT64_DROP_REASON_UNKNOWN]);
+            errorCounters->mutable_unknown()->set_ipv6_to_ipv4_packets(
+                otCounters.mCount6To4[OT_NAT64_DROP_REASON_UNKNOWN]);
+            errorCounters->mutable_illegal_packet()->set_ipv4_to_ipv6_packets(
+                otCounters.mCount4To6[OT_NAT64_DROP_REASON_ILLEGAL_PACKET]);
+            errorCounters->mutable_illegal_packet()->set_ipv6_to_ipv4_packets(
+                otCounters.mCount6To4[OT_NAT64_DROP_REASON_ILLEGAL_PACKET]);
+            errorCounters->mutable_unsupported_protocol()->set_ipv4_to_ipv6_packets(
+                otCounters.mCount4To6[OT_NAT64_DROP_REASON_UNSUPPORTED_PROTO]);
+            errorCounters->mutable_unsupported_protocol()->set_ipv6_to_ipv4_packets(
+                otCounters.mCount6To4[OT_NAT64_DROP_REASON_UNSUPPORTED_PROTO]);
+            errorCounters->mutable_no_mapping()->set_ipv4_to_ipv6_packets(
+                otCounters.mCount4To6[OT_NAT64_DROP_REASON_NO_MAPPING]);
+            errorCounters->mutable_no_mapping()->set_ipv6_to_ipv4_packets(
+                otCounters.mCount6To4[OT_NAT64_DROP_REASON_NO_MAPPING]);
+        }
+#endif // OTBR_ENABLE_NAT64
+       // End of BorderRoutingCounters section.
+
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+        // Begin of SrpServerInfo section.
+        {
+            auto                               srpServer = wpanBorderRouter->mutable_srp_server();
+            otSrpServerLeaseInfo               leaseInfo;
+            const otSrpServerHost             *host             = nullptr;
+            const otSrpServerResponseCounters *responseCounters = otSrpServerGetResponseCounters(mInstance);
+
+            srpServer->set_state(SrpServerStateFromOtSrpServerState(otSrpServerGetState(mInstance)));
+            srpServer->set_port(otSrpServerGetPort(mInstance));
+            srpServer->set_address_mode(
+                SrpServerAddressModeFromOtSrpServerAddressMode(otSrpServerGetAddressMode(mInstance)));
+
+            auto srpServerHosts            = srpServer->mutable_hosts();
+            auto srpServerServices         = srpServer->mutable_services();
+            auto srpServerResponseCounters = srpServer->mutable_response_counters();
+
+            while ((host = otSrpServerGetNextHost(mInstance, host)))
+            {
+                const otSrpServerService *service = nullptr;
+
+                if (otSrpServerHostIsDeleted(host))
+                {
+                    srpServerHosts->set_deleted_count(srpServerHosts->deleted_count() + 1);
+                }
+                else
+                {
+                    srpServerHosts->set_fresh_count(srpServerHosts->fresh_count() + 1);
+                    otSrpServerHostGetLeaseInfo(host, &leaseInfo);
+                    srpServerHosts->set_lease_time_total_ms(srpServerHosts->lease_time_total_ms() + leaseInfo.mLease);
+                    srpServerHosts->set_key_lease_time_total_ms(srpServerHosts->key_lease_time_total_ms() +
+                                                                leaseInfo.mKeyLease);
+                    srpServerHosts->set_remaining_lease_time_total_ms(srpServerHosts->remaining_lease_time_total_ms() +
+                                                                      leaseInfo.mRemainingLease);
+                    srpServerHosts->set_remaining_key_lease_time_total_ms(
+                        srpServerHosts->remaining_key_lease_time_total_ms() + leaseInfo.mRemainingKeyLease);
+                }
+
+                while ((service = otSrpServerHostGetNextService(host, service)))
+                {
+                    if (otSrpServerServiceIsDeleted(service))
+                    {
+                        srpServerServices->set_deleted_count(srpServerServices->deleted_count() + 1);
+                    }
+                    else
+                    {
+                        srpServerServices->set_fresh_count(srpServerServices->fresh_count() + 1);
+                        otSrpServerServiceGetLeaseInfo(service, &leaseInfo);
+                        srpServerServices->set_lease_time_total_ms(srpServerServices->lease_time_total_ms() +
+                                                                   leaseInfo.mLease);
+                        srpServerServices->set_key_lease_time_total_ms(srpServerServices->key_lease_time_total_ms() +
+                                                                       leaseInfo.mKeyLease);
+                        srpServerServices->set_remaining_lease_time_total_ms(
+                            srpServerServices->remaining_lease_time_total_ms() + leaseInfo.mRemainingLease);
+                        srpServerServices->set_remaining_key_lease_time_total_ms(
+                            srpServerServices->remaining_key_lease_time_total_ms() + leaseInfo.mRemainingKeyLease);
+                    }
+                }
+            }
+
+            srpServerResponseCounters->set_success_count(responseCounters->mSuccess);
+            srpServerResponseCounters->set_server_failure_count(responseCounters->mServerFailure);
+            srpServerResponseCounters->set_format_error_count(responseCounters->mFormatError);
+            srpServerResponseCounters->set_name_exists_count(responseCounters->mNameExists);
+            srpServerResponseCounters->set_refused_count(responseCounters->mRefused);
+            srpServerResponseCounters->set_other_count(responseCounters->mOther);
+        }
+        // End of SrpServerInfo section.
+#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY
+
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+        // Begin of DnsServerInfo section.
+        {
+            auto            dnsServer                 = wpanBorderRouter->mutable_dns_server();
+            auto            dnsServerResponseCounters = dnsServer->mutable_response_counters();
+            otDnssdCounters otDnssdCounters           = *otDnssdGetCounters(mInstance);
+
+            dnsServerResponseCounters->set_success_count(otDnssdCounters.mSuccessResponse);
+            dnsServerResponseCounters->set_server_failure_count(otDnssdCounters.mServerFailureResponse);
+            dnsServerResponseCounters->set_format_error_count(otDnssdCounters.mFormatErrorResponse);
+            dnsServerResponseCounters->set_name_error_count(otDnssdCounters.mNameErrorResponse);
+            dnsServerResponseCounters->set_not_implemented_count(otDnssdCounters.mNotImplementedResponse);
+            dnsServerResponseCounters->set_other_count(otDnssdCounters.mOtherResponse);
+
+            dnsServer->set_resolved_by_local_srp_count(otDnssdCounters.mResolvedBySrp);
+        }
+        // End of DnsServerInfo section.
+#endif // OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+
+        // Start of MdnsInfo section.
+        if (aPublisher != nullptr)
+        {
+            auto                     mdns     = wpanBorderRouter->mutable_mdns();
+            const MdnsTelemetryInfo &mdnsInfo = aPublisher->GetMdnsTelemetryInfo();
+
+            CopyMdnsResponseCounters(mdnsInfo.mHostRegistrations, mdns->mutable_host_registration_responses());
+            CopyMdnsResponseCounters(mdnsInfo.mServiceRegistrations, mdns->mutable_service_registration_responses());
+            CopyMdnsResponseCounters(mdnsInfo.mHostResolutions, mdns->mutable_host_resolution_responses());
+            CopyMdnsResponseCounters(mdnsInfo.mServiceResolutions, mdns->mutable_service_resolution_responses());
+
+            mdns->set_host_registration_ema_latency_ms(mdnsInfo.mHostRegistrationEmaLatency);
+            mdns->set_service_registration_ema_latency_ms(mdnsInfo.mServiceRegistrationEmaLatency);
+            mdns->set_host_resolution_ema_latency_ms(mdnsInfo.mHostResolutionEmaLatency);
+            mdns->set_service_resolution_ema_latency_ms(mdnsInfo.mServiceResolutionEmaLatency);
+        }
+        // End of MdnsInfo section.
+
+#if OTBR_ENABLE_NAT64
+        // Start of BorderRoutingNat64State section.
+        {
+            auto nat64State = wpanBorderRouter->mutable_nat64_state();
+
+            nat64State->set_prefix_manager_state(Nat64StateFromOtNat64State(otNat64GetPrefixManagerState(mInstance)));
+            nat64State->set_translator_state(Nat64StateFromOtNat64State(otNat64GetTranslatorState(mInstance)));
+        }
+        // End of BorderRoutingNat64State section.
+
+        // Start of Nat64Mapping section.
+        {
+            otNat64AddressMappingIterator iterator;
+            otNat64AddressMapping         otMapping;
+            Sha256::Hash                  hash;
+            Sha256                        sha256;
+
+            otNat64InitAddressMappingIterator(mInstance, &iterator);
+            while (otNat64GetNextAddressMapping(mInstance, &iterator, &otMapping) == OT_ERROR_NONE)
+            {
+                auto nat64Mapping         = wpanBorderRouter->add_nat64_mappings();
+                auto nat64MappingCounters = nat64Mapping->mutable_counters();
+
+                nat64Mapping->set_mapping_id(otMapping.mId);
+                CopyNat64TrafficCounters(otMapping.mCounters.mTcp, nat64MappingCounters->mutable_tcp());
+                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(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
+                }
+            }
+        }
+        // End of Nat64Mapping section.
+#endif // OTBR_ENABLE_NAT64
+
+        // End of WpanBorderRouter section.
+
+        // Start of WpanRcp section.
+        {
+            auto                 wpanRcp                = telemetryData.mutable_wpan_rcp();
+            auto                 rcpStabilityStatistics = wpanRcp->mutable_rcp_stability_statistics();
+            otRadioSpinelMetrics otRadioSpinelMetrics   = *otSysGetRadioSpinelMetrics();
+
+            rcpStabilityStatistics->set_rcp_timeout_count(otRadioSpinelMetrics.mRcpTimeoutCount);
+            rcpStabilityStatistics->set_rcp_reset_count(otRadioSpinelMetrics.mRcpUnexpectedResetCount);
+            rcpStabilityStatistics->set_rcp_restoration_count(otRadioSpinelMetrics.mRcpRestorationCount);
+            rcpStabilityStatistics->set_spinel_parse_error_count(otRadioSpinelMetrics.mSpinelParseErrorCount);
+
+            // TODO: provide rcp_firmware_update_count info.
+            rcpStabilityStatistics->set_thread_stack_uptime(otInstanceGetUptime(mInstance));
+
+            auto                  rcpInterfaceStatistics = wpanRcp->mutable_rcp_interface_statistics();
+            otRcpInterfaceMetrics otRcpInterfaceMetrics  = *otSysGetRcpInterfaceMetrics();
+
+            rcpInterfaceStatistics->set_rcp_interface_type(otRcpInterfaceMetrics.mRcpInterfaceType);
+            rcpInterfaceStatistics->set_transferred_frames_count(otRcpInterfaceMetrics.mTransferredFrameCount);
+            rcpInterfaceStatistics->set_transferred_valid_frames_count(
+                otRcpInterfaceMetrics.mTransferredValidFrameCount);
+            rcpInterfaceStatistics->set_transferred_garbage_frames_count(
+                otRcpInterfaceMetrics.mTransferredGarbageFrameCount);
+            rcpInterfaceStatistics->set_rx_frames_count(otRcpInterfaceMetrics.mRxFrameCount);
+            rcpInterfaceStatistics->set_rx_bytes_count(otRcpInterfaceMetrics.mRxFrameByteCount);
+            rcpInterfaceStatistics->set_tx_frames_count(otRcpInterfaceMetrics.mTxFrameCount);
+            rcpInterfaceStatistics->set_tx_bytes_count(otRcpInterfaceMetrics.mTxFrameByteCount);
+        }
+        // End of WpanRcp section.
+
+        // Start of CoexMetrics section.
+        {
+            auto               coexMetrics = telemetryData.mutable_coex_metrics();
+            otRadioCoexMetrics otRadioCoexMetrics;
+
+            SuccessOrExit(error = otPlatRadioGetCoexMetrics(mInstance, &otRadioCoexMetrics));
+            coexMetrics->set_count_tx_request(otRadioCoexMetrics.mNumTxRequest);
+            coexMetrics->set_count_tx_grant_immediate(otRadioCoexMetrics.mNumTxGrantImmediate);
+            coexMetrics->set_count_tx_grant_wait(otRadioCoexMetrics.mNumTxGrantWait);
+            coexMetrics->set_count_tx_grant_wait_activated(otRadioCoexMetrics.mNumTxGrantWaitActivated);
+            coexMetrics->set_count_tx_grant_wait_timeout(otRadioCoexMetrics.mNumTxGrantWaitTimeout);
+            coexMetrics->set_count_tx_grant_deactivated_during_request(
+                otRadioCoexMetrics.mNumTxGrantDeactivatedDuringRequest);
+            coexMetrics->set_tx_average_request_to_grant_time_us(otRadioCoexMetrics.mAvgTxRequestToGrantTime);
+            coexMetrics->set_count_rx_request(otRadioCoexMetrics.mNumRxRequest);
+            coexMetrics->set_count_rx_grant_immediate(otRadioCoexMetrics.mNumRxGrantImmediate);
+            coexMetrics->set_count_rx_grant_wait(otRadioCoexMetrics.mNumRxGrantWait);
+            coexMetrics->set_count_rx_grant_wait_activated(otRadioCoexMetrics.mNumRxGrantWaitActivated);
+            coexMetrics->set_count_rx_grant_wait_timeout(otRadioCoexMetrics.mNumRxGrantWaitTimeout);
+            coexMetrics->set_count_rx_grant_deactivated_during_request(
+                otRadioCoexMetrics.mNumRxGrantDeactivatedDuringRequest);
+            coexMetrics->set_count_rx_grant_none(otRadioCoexMetrics.mNumRxGrantNone);
+            coexMetrics->set_rx_average_request_to_grant_time_us(otRadioCoexMetrics.mAvgRxRequestToGrantTime);
+        }
+        // End of CoexMetrics section.
+    }
+
+exit:
+    return error;
+}
+#endif // OTBR_ENABLE_TELEMETRY_DATA_API
 } // namespace agent
 } // namespace otbr
diff --git a/src/utils/thread_helper.hpp b/src/utils/thread_helper.hpp
index af8ef1a..10d3877 100644
--- a/src/utils/thread_helper.hpp
+++ b/src/utils/thread_helper.hpp
@@ -34,6 +34,8 @@
 #ifndef OTBR_THREAD_HELPER_HPP_
 #define OTBR_THREAD_HELPER_HPP_
 
+#include "openthread-br/config.h"
+
 #include <chrono>
 #include <functional>
 #include <map>
@@ -47,6 +49,10 @@
 #include <openthread/joiner.h>
 #include <openthread/netdata.h>
 #include <openthread/thread.h>
+#include "mdns/mdns.hpp"
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+#include "proto/thread_telemetry.pb.h"
+#endif
 
 namespace otbr {
 namespace Ncp {
@@ -250,6 +256,18 @@
 
     void DetachGracefully(ResultHandler aHandler);
 
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+    /**
+     * This method populates the telemetry data and returns the error code if error happens.
+     *
+     * @param[in] aPublisher     The Mdns::Publisher to provide MDNS telemetry if it is not `nullptr`.
+     * @param[in] telemetryData  The telemetry data to be populated.
+     *
+     * @returns The error code if error happens during the population of the telemetry data.
+     */
+    otError RetrieveTelemetryData(Mdns::Publisher *aPublisher, threadnetwork::TelemetryData &telemetryData);
+#endif // OTBR_ENABLE_TELEMETRY_DATA_API
+
     /**
      * This method logs OpenThread action result.
      *
@@ -309,6 +327,11 @@
 #if OTBR_ENABLE_DBUS_SERVER
     UpdateMeshCopTxtHandler mUpdateMeshCopTxtHandler;
 #endif
+
+#if OTBR_ENABLE_TELEMETRY_DATA_API & OTBR_ENABLE_NAT64
+    static const uint8_t kNat64SourceAddressHashSaltLength = 16;
+    uint8_t              mNat64Ipv6AddressSalt[kNat64SourceAddressHashSaltLength];
+#endif
 };
 
 } // namespace agent
diff --git a/tests/dbus/CMakeLists.txt b/tests/dbus/CMakeLists.txt
index 12f81e1..5aa3f47 100644
--- a/tests/dbus/CMakeLists.txt
+++ b/tests/dbus/CMakeLists.txt
@@ -29,8 +29,10 @@
 add_executable(otbr-test-dbus-client
     test_dbus_client.cpp
 )
+
 target_link_libraries(otbr-test-dbus-client PRIVATE
     otbr-dbus-client
+    otbr-proto
 )
 
 add_executable(otbr-test-dbus-server
diff --git a/tests/dbus/test-client b/tests/dbus/test-client
index 05a8c42..558a7c5 100755
--- a/tests/dbus/test-client
+++ b/tests/dbus/test-client
@@ -221,11 +221,15 @@
     # The ot-cli-ftd node is used to test Thread attach.
     expect <<EOF &
 spawn ot-cli-ftd 3
-send "panid 0x7890\r\n"
+send "dataset init new\r\n"
 expect "Done"
-send "networkname Test1\r\n"
+send "dataset panid 0x7890\r\n"
 expect "Done"
-send "channel 15\r\n"
+send "dataset networkname Test1\r\n"
+expect "Done"
+send "dataset channel 15\r\n"
+expect "Done"
+send "dataset commit active\r\n"
 expect "Done"
 send "ifconfig up\r\n"
 expect "Done"
@@ -249,13 +253,17 @@
     # The ot-cli-mtd node is used to test the child and neighbor table.
     expect <<EOF &
 spawn ot-cli-mtd 2
-send "panid 0x3456\r\n"
+send "dataset clear"
 expect "Done"
-send "networkkey 00112233445566778899aabbccddeeff\r\n"
+send "dataset panid 0x3456\r\n"
 expect "Done"
-send "networkname Test\r\n"
+send "dataset networkkey 00112233445566778899aabbccddeeff\r\n"
 expect "Done"
-send "channel 11\r\n"
+send "dataset networkname Test\r\n"
+expect "Done"
+send "dataset channel 11\r\n"
+expect "Done"
+send "dataset commit active\r\n"
 expect "Done"
 send "ifconfig up\r\n"
 expect "Done"
diff --git a/tests/dbus/test_dbus_client.cpp b/tests/dbus/test_dbus_client.cpp
index ab3aa8f..0e0e613 100644
--- a/tests/dbus/test_dbus_client.cpp
+++ b/tests/dbus/test_dbus_client.cpp
@@ -39,6 +39,10 @@
 #include "common/code_utils.hpp"
 #include "dbus/client/thread_api_dbus.hpp"
 #include "dbus/common/constants.hpp"
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+#include "proto/thread_telemetry.pb.h"
+#endif
+#include "proto/capabilities.pb.h"
 
 using otbr::DBus::ActiveScanResult;
 using otbr::DBus::ClientError;
@@ -239,6 +243,73 @@
 #endif
 }
 
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+void CheckTelemetryData(ThreadApiDBus *aApi)
+{
+    std::vector<uint8_t>         responseTelemetryDataBytes;
+    threadnetwork::TelemetryData telemetryData;
+
+    TEST_ASSERT(aApi->GetTelemetryData(responseTelemetryDataBytes) == OTBR_ERROR_NONE);
+    // Print TelemetryData proto in hex format.
+    printf("TelemetryData bytes in hex: ");
+    for (uint8_t byte : responseTelemetryDataBytes)
+    {
+        printf("%02x ", byte);
+    }
+    printf("\n");
+
+    TEST_ASSERT(telemetryData.ParseFromString(
+        std::string(responseTelemetryDataBytes.begin(), responseTelemetryDataBytes.end())));
+    TEST_ASSERT(telemetryData.wpan_stats().node_type() == threadnetwork::TelemetryData::NODE_TYPE_LEADER);
+    TEST_ASSERT(telemetryData.wpan_stats().channel() == 11);
+    TEST_ASSERT(telemetryData.wpan_stats().radio_tx_power() == 0);
+    TEST_ASSERT(telemetryData.wpan_stats().mac_cca_fail_rate() < 1e-6);
+    TEST_ASSERT(telemetryData.wpan_stats().phy_tx() > 0);
+    TEST_ASSERT(telemetryData.wpan_stats().phy_rx() > 0);
+    TEST_ASSERT(telemetryData.wpan_stats().ip_tx_success() > 0);
+    TEST_ASSERT(telemetryData.wpan_topo_full().rloc16() > 0);
+    TEST_ASSERT(telemetryData.wpan_topo_full().network_data().size() > 0);
+    TEST_ASSERT(telemetryData.wpan_topo_full().partition_id() > 0);
+    TEST_ASSERT(telemetryData.wpan_topo_full().extended_pan_id() > 0);
+    TEST_ASSERT(telemetryData.topo_entries_size() == 1);
+    TEST_ASSERT(telemetryData.topo_entries(0).rloc16() > 0);
+    TEST_ASSERT(telemetryData.wpan_border_router().border_routing_counters().rs_tx_failure() == 0);
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    TEST_ASSERT(telemetryData.wpan_border_router().srp_server().state() ==
+                threadnetwork::TelemetryData::SRP_SERVER_STATE_RUNNING);
+#endif
+#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
+    TEST_ASSERT(telemetryData.wpan_border_router().dns_server().response_counters().server_failure_count() == 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
+    TEST_ASSERT(telemetryData.wpan_rcp().rcp_interface_statistics().transferred_frames_count() > 0);
+    TEST_ASSERT(telemetryData.coex_metrics().count_tx_request() > 0);
+}
+#endif
+
+void CheckCapabilities(ThreadApiDBus *aApi)
+{
+    std::vector<uint8_t> responseCapabilitiesBytes;
+    otbr::Capabilities   capabilities;
+
+    TEST_ASSERT(aApi->GetCapabilities(responseCapabilitiesBytes) == OTBR_ERROR_NONE);
+    // Print TelemetryData proto in hex format.
+    printf("TelemetryData bytes in hex: ");
+    for (uint8_t byte : responseCapabilitiesBytes)
+    {
+        printf("%02x ", byte);
+    }
+    printf("\n");
+
+    TEST_ASSERT(
+        capabilities.ParseFromString(std::string(responseCapabilitiesBytes.begin(), responseCapabilitiesBytes.end())));
+    TEST_ASSERT(capabilities.nat64() == OTBR_ENABLE_NAT64);
+}
+
 int main()
 {
     DBusError                      error;
@@ -267,7 +338,6 @@
     TEST_ASSERT(region == "US");
 
     TEST_ASSERT(api->GetPreferredChannelMask(preferredChannelMask) == ClientError::ERROR_NONE);
-    TEST_ASSERT(preferredChannelMask == 0x7fff800);
 
     api->EnergyScan(scanDuration, [&stepDone](const std::vector<EnergyScanResult> &aResult) {
         TEST_ASSERT(!aResult.empty());
@@ -350,6 +420,10 @@
                             CheckMdnsInfo(api.get());
                             CheckDnssdCounters(api.get());
                             CheckNat64(api.get());
+#if OTBR_ENABLE_TELEMETRY_DATA_API
+                            CheckTelemetryData(api.get());
+#endif
+                            CheckCapabilities(api.get());
                             api->FactoryReset(nullptr);
                             TEST_ASSERT(api->GetNetworkName(name) == OTBR_ERROR_NONE);
                             TEST_ASSERT(rloc16 != 0xffff);
diff --git a/tests/mdns/main.cpp b/tests/mdns/main.cpp
index 758ed1b..66f6366 100644
--- a/tests/mdns/main.cpp
+++ b/tests/mdns/main.cpp
@@ -96,14 +96,17 @@
     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(hostName, {Ip6Address(hostAddr)},
                                          [](otbrError aError) { SuccessOrDie(aError, "cannot publish the host"); });
 
         sContext.mPublisher->PublishService(
-            hostName, "SingleService", "_meshcop._udp.", Mdns::Publisher::SubTypeList{}, 12345, txtList,
+            hostName, "SingleService", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
             [](otbrError aError) { SuccessOrDie(aError, "cannot publish the service"); });
     }
 }
@@ -123,18 +126,21 @@
     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, txtList,
+            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, txtList,
+            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) {
@@ -142,11 +148,11 @@
         });
 
         sContext.mPublisher->PublishService(
-            hostName2, "MultipleService21", "_meshcop._udp.", Mdns::Publisher::SubTypeList{}, 12345, txtList,
+            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, txtList,
+            hostName2, "MultipleService22", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
             [](otbrError aError) { SuccessOrDie(aError, "cannot publish the second service"); });
     }
 }
@@ -157,15 +163,18 @@
 
     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, txtList,
-            [](otbrError aError) { SuccessOrDie(aError, "SingleService._meshcop._udp."); });
+            "", "SingleService", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
+            [](otbrError aError) { SuccessOrDie(aError, "SingleService._meshcop._udp"); });
     }
 }
 
@@ -175,14 +184,17 @@
 
     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, txtList,
-                                            [](otbrError aError) { SuccessOrDie(aError, "(empty)._meshcop._udp."); });
+        sContext.mPublisher->PublishService("", "", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
+                                            [](otbrError aError) { SuccessOrDie(aError, "(empty)._meshcop._udp"); });
     }
 }
 
@@ -196,22 +208,28 @@
     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, txtList,
-            [](otbrError aError) { SuccessOrDie(aError, "MultipleService1._meshcop._udp."); });
+            "", "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, txtList,
-            [](otbrError aError) { SuccessOrDie(aError, "MultipleService2._meshcop._udp."); });
+            "", "MultipleService2", "_meshcop._udp", Mdns::Publisher::SubTypeList{}, 12345, txtData,
+            [](otbrError aError) { SuccessOrDie(aError, "MultipleService2._meshcop._udp"); });
     }
 }
 
@@ -226,28 +244,38 @@
     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, txtList,
+            "", "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, txtList,
+            "", "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);
 
     assert(aContext == &sContext);
@@ -257,8 +285,8 @@
     subTypeList.back() = "_SUBTYPE3";
 
     sContext.mPublisher->PublishService(
-        "", "ServiceWithSubTypes", "_meshcop._udp.", subTypeList, 12345, Mdns::Publisher::TxtList{},
-        [](otbrError aError) { SuccessOrDie(aError, "ServiceWithSubTypes._meshcop._udp."); });
+        "", "ServiceWithSubTypes", "_meshcop._udp", subTypeList, 12345, txtData,
+        [](otbrError aError) { SuccessOrDie(aError, "ServiceWithSubTypes._meshcop._udp"); });
 }
 
 otbrError TestSingleServiceWithCustomHost(void)
@@ -410,10 +438,65 @@
     return ret;
 }
 
+otbrError CheckTxtDataEncoderDecoder(void)
+{
+    otbrError                error = OTBR_ERROR_NONE;
+    Mdns::Publisher::TxtList txtList;
+    Mdns::Publisher::TxtList parsedTxtList;
+    std::vector<uint8_t>     txtData;
+
+    // Encode empty `TxtList`
+
+    SuccessOrExit(error = Mdns::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()));
+    VerifyOrExit(parsedTxtList.size() == 0, error = OTBR_ERROR_PARSE);
+
+    // TxtList with one bool attribute
+
+    txtList.clear();
+    txtList.emplace_back("b1");
+
+    SuccessOrExit(error = Mdns::Publisher::EncodeTxtData(txtList, txtData));
+    SuccessOrExit(error = Mdns::Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
+    VerifyOrExit(parsedTxtList == txtList, error = OTBR_ERROR_PARSE);
+
+    // TxtList with one one key/value
+
+    txtList.clear();
+    txtList.emplace_back("k1", "v1");
+
+    SuccessOrExit(error = Mdns::Publisher::EncodeTxtData(txtList, txtData));
+    SuccessOrExit(error = Mdns::Publisher::DecodeTxtData(parsedTxtList, txtData.data(), txtData.size()));
+    VerifyOrExit(parsedTxtList == txtList, error = OTBR_ERROR_PARSE);
+
+    // TxtList with multiple entries
+
+    txtList.clear();
+    txtList.emplace_back("k1", "v1");
+    txtList.emplace_back("b1");
+    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()));
+    VerifyOrExit(parsedTxtList == txtList, error = OTBR_ERROR_PARSE);
+
+exit:
+    return error;
+}
+
 int main(int argc, char *argv[])
 {
     int ret = 0;
 
+    if (CheckTxtDataEncoderDecoder() != OTBR_ERROR_NONE)
+    {
+        return 1;
+    }
+
     if (argc < 2)
     {
         return 1;
diff --git a/tests/rest/test-rest-server b/tests/rest/test-rest-server
index 3914bd8..cc63de0 100755
--- a/tests/rest/test-rest-server
+++ b/tests/rest/test-rest-server
@@ -50,6 +50,10 @@
     sleep 1
     sudo expect <<EOF &
 spawn ${CMAKE_BINARY_DIR}/third_party/openthread/repo/src/posix/ot-ctl
+send "dataset init new\r\n"
+expect "Done"
+send "dataset commit active\r\n"
+expect "Done"
 send "ifconfig up\r\n"
 expect "Done"
 send "thread start\r\n"
diff --git a/tests/rest/test_rest.py b/tests/rest/test_rest.py
index 6a84648..b419c2d 100644
--- a/tests/rest/test_rest.py
+++ b/tests/rest/test_rest.py
@@ -194,7 +194,7 @@
         "Rloc16", "LeaderData", "ExtPanId"
     ]
     expected_value_type = [
-        int, int, str, str, str, int, dict, str
+        str, int, str, str, str, int, dict, str
     ]
     expected_check_dict = dict(zip(expected_keys, expected_value_type))
 
@@ -250,7 +250,7 @@
 def node_state_check(data):
     assert data is not None
 
-    assert (type(data) == int)
+    assert (type(data) == str)
 
     return True
 
diff --git a/tests/scripts/bootstrap.sh b/tests/scripts/bootstrap.sh
index a89e9cc..f3bdf51 100755
--- a/tests/scripts/bootstrap.sh
+++ b/tests/scripts/bootstrap.sh
@@ -32,6 +32,8 @@
 TOOLS_HOME="$HOME"/.cache/tools
 [[ -d $TOOLS_HOME ]] || mkdir -p "$TOOLS_HOME"
 
+MDNSRESPONDER_PATCH_PATH=$(realpath "$(dirname "$0")"/../../third_party/mDNSResponder)
+
 disable_install_recommends()
 {
     OTBR_APT_CONF_FILE=/etc/apt/apt.conf
@@ -126,15 +128,18 @@
         fi
 
         if [ "${OTBR_MDNS-}" == 'mDNSResponder' ]; then
-            SOURCE_NAME=mDNSResponder-1310.80.1
+            SOURCE_NAME=mDNSResponder-1790.80.10
             wget https://github.com/apple-oss-distributions/mDNSResponder/archive/refs/tags/$SOURCE_NAME.tar.gz \
                 && mkdir -p $SOURCE_NAME \
                 && tar xvf $SOURCE_NAME.tar.gz -C $SOURCE_NAME --strip-components=1 \
-                && cd $SOURCE_NAME/Clients \
-                && sed -i '/#include <ctype.h>/a #include <stdarg.h>' dns-sd.c \
-                && sed -i '/#include <ctype.h>/a #include <sys/param.h>' dns-sd.c \
-                && cd ../mDNSPosix \
-                && make os=linux && sudo make install os=linux
+                && cd "$SOURCE_NAME" \
+                && (
+                    for patch in "$MDNSRESPONDER_PATCH_PATH"/*.patch; do
+                        patch -p1 <"$patch"
+                    done
+                ) \
+                && cd mDNSPosix \
+                && make os=linux tls=no && sudo make install os=linux tls=no
         fi
 
         # Enable IPv6
diff --git a/tests/scripts/check-scan-build b/tests/scripts/check-scan-build
index 77d2058..42faf1c 100755
--- a/tests/scripts/check-scan-build
+++ b/tests/scripts/check-scan-build
@@ -40,6 +40,7 @@
             -DCMAKE_C_COMPILER=clang \
             -DOTBR_DBUS=ON \
             -DOTBR_FEATURE_FLAGS=ON \
+            -DOTBR_TELEMETRY_DATA_API=ON \
             -DOTBR_WEB=ON \
             -DOTBR_REST=ON \
             .. \
diff --git a/tests/scripts/openwrt b/tests/scripts/openwrt
index a960cb9..355e869 100755
--- a/tests/scripts/openwrt
+++ b/tests/scripts/openwrt
@@ -43,7 +43,7 @@
 
 do_prepare()
 {
-    docker create --name "${CONTAINER_NAME}" --rm -v openwrt-bin:/home/build/openwrt/bin -it openwrt/sdk
+    docker create --name "${CONTAINER_NAME}" --rm -v openwrt-bin:/home/build/openwrt/bin -it openwrt/sdk:22.03.3
     docker cp . "${CONTAINER_NAME}":/home/build/ot-br-posix
     echo 'src-link openthread /home/build/ot-br-posix/etc/openwrt' >feeds.conf
 
diff --git a/third_party/mDNSResponder/0001-Fix-Linux-build.patch b/third_party/mDNSResponder/0001-Fix-Linux-build.patch
new file mode 100644
index 0000000..1dc01f3
--- /dev/null
+++ b/third_party/mDNSResponder/0001-Fix-Linux-build.patch
@@ -0,0 +1,32 @@
+From e136dcdcdd93ef32ada981e89c195905eb809eea Mon Sep 17 00:00:00 2001
+Message-ID: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Thu, 23 Mar 2023 00:15:52 -0500
+Subject: [PATCH] Fix Linux build
+
+The __block qualifier is not used in Linux builds.
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+---
+ mDNSShared/uds_daemon.c | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/mDNSShared/uds_daemon.c b/mDNSShared/uds_daemon.c
+index 9ae5f78..5a00bb5 100644
+--- a/mDNSShared/uds_daemon.c
++++ b/mDNSShared/uds_daemon.c
+@@ -2912,7 +2912,11 @@ exit:
+ mDNSlocal mStatus add_domain_to_browser(request_state *info, const domainname *d)
+ {
+     browser_t *b, *p;
++#if defined(TARGET_OS_MAC) && TARGET_OS_MAC
+     __block mStatus err;
++#else
++    mStatus err;
++#endif
+ 
+     for (p = info->u.browser.browsers; p; p = p->next)
+     {
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0002-Create-subroutine-for-cleaning-recent-interfaces.patch b/third_party/mDNSResponder/0002-Create-subroutine-for-cleaning-recent-interfaces.patch
new file mode 100644
index 0000000..98da74c
--- /dev/null
+++ b/third_party/mDNSResponder/0002-Create-subroutine-for-cleaning-recent-interfaces.patch
@@ -0,0 +1,64 @@
+From 4f7970ac1615aba7a39ae94c1ca14135265574e9 Mon Sep 17 00:00:00 2001
+Message-ID: <4f7970ac1615aba7a39ae94c1ca14135265574e9.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Wed, 28 Jun 2017 17:30:00 -0500
+Subject: [PATCH] Create subroutine for cleaning recent interfaces
+
+Moves functionality for cleaning the list of recent
+interfaces into its own subroutine.
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+---
+ mDNSPosix/mDNSPosix.c | 24 ++++++++++++++----------
+ 1 file changed, 14 insertions(+), 10 deletions(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index 0a7c3df..fe7242d 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1322,6 +1322,19 @@ mDNSlocal int SetupSocket(struct sockaddr *intfAddr, mDNSIPPort port, int interf
+     return err;
+ }
+ 
++// Clean up any interfaces that have been hanging around on the RecentInterfaces list for more than a minute
++mDNSlocal void CleanRecentInterfaces(void)
++{
++    PosixNetworkInterface **ri = &gRecentInterfaces;
++    const mDNSs32 utc = mDNSPlatformUTC();
++    while (*ri)
++    {
++        PosixNetworkInterface *pi = *ri;
++        if (utc - pi->LastSeen < 60) ri = (PosixNetworkInterface **)&pi->coreIntf.next;
++        else { *ri = (PosixNetworkInterface *)pi->coreIntf.next; mdns_free(pi); }
++    }
++}
++
+ // Creates a PosixNetworkInterface for the interface whose IP address is
+ // intfAddr and whose name is intfName and registers it with mDNS core.
+ mDNSlocal int SetupOneInterface(mDNS *const m, struct sockaddr *intfAddr, struct sockaddr *intfMask,
+@@ -1559,16 +1572,7 @@ mDNSlocal int SetupInterfaceList(mDNS *const m)
+ 
+     // Clean up.
+     if (intfList != NULL) freeifaddrs(intfList);
+-
+-    // Clean up any interfaces that have been hanging around on the RecentInterfaces list for more than a minute
+-    PosixNetworkInterface **ri = &gRecentInterfaces;
+-    const mDNSs32 utc = mDNSPlatformUTC();
+-    while (*ri)
+-    {
+-        PosixNetworkInterface *pi = *ri;
+-        if (utc - pi->LastSeen < 60) ri = (PosixNetworkInterface **)&pi->coreIntf.next;
+-        else { *ri = (PosixNetworkInterface *)pi->coreIntf.next; mdns_free(pi); }
+-    }
++    CleanRecentInterfaces();
+ 
+     return err;
+ }
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0003-Create-subroutine-for-tearing-down-an-interface.patch b/third_party/mDNSResponder/0003-Create-subroutine-for-tearing-down-an-interface.patch
new file mode 100644
index 0000000..812bd20
--- /dev/null
+++ b/third_party/mDNSResponder/0003-Create-subroutine-for-tearing-down-an-interface.patch
@@ -0,0 +1,62 @@
+From f7ab91f739b936305ca56743adfb4673e3f2f4ba Mon Sep 17 00:00:00 2001
+Message-ID: <f7ab91f739b936305ca56743adfb4673e3f2f4ba.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Wed, 28 Jun 2017 17:30:00 -0500
+Subject: [PATCH] Create subroutine for tearing down an interface
+
+Creates a subroutine for tearing down an interface.
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+---
+ mDNSPosix/mDNSPosix.c | 22 ++++++++++++++++------
+ 1 file changed, 16 insertions(+), 6 deletions(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index fe7242d..a32a880 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1043,6 +1043,19 @@ mDNSlocal void FreePosixNetworkInterface(PosixNetworkInterface *intf)
+     gRecentInterfaces = intf;
+ }
+ 
++mDNSlocal void TearDownInterface(mDNS *const m, PosixNetworkInterface *intf)
++{
++    mDNS_DeregisterInterface(m, &intf->coreIntf, NormalActivation);
++    if (gMDNSPlatformPosixVerboseLevel > 0) fprintf(stderr, "Deregistered interface %s\n", intf->intfName);
++    FreePosixNetworkInterface(intf);
++
++    num_registered_interfaces--;
++    if (num_registered_interfaces == 0) {
++        num_pkts_accepted = 0;
++        num_pkts_rejected = 0;
++    }
++}
++
+ // Grab the first interface, deregister it, free it, and repeat until done.
+ mDNSlocal void ClearInterfaceList(mDNS *const m)
+ {
+@@ -1051,13 +1064,10 @@ mDNSlocal void ClearInterfaceList(mDNS *const m)
+     while (m->HostInterfaces)
+     {
+         PosixNetworkInterface *intf = (PosixNetworkInterface*)(m->HostInterfaces);
+-        mDNS_DeregisterInterface(m, &intf->coreIntf, NormalActivation);
+-        if (gMDNSPlatformPosixVerboseLevel > 0) fprintf(stderr, "Deregistered interface %s\n", intf->intfName);
+-        FreePosixNetworkInterface(intf);
++        TearDownInterface(m, intf);
+     }
+-    num_registered_interfaces = 0;
+-    num_pkts_accepted = 0;
+-    num_pkts_rejected = 0;
++
++    assert(num_registered_interfaces == 0);
+ }
+ 
+ mDNSlocal int SetupIPv6Socket(int fd)
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0004-Track-interface-socket-family.patch b/third_party/mDNSResponder/0004-Track-interface-socket-family.patch
new file mode 100644
index 0000000..48fbc74
--- /dev/null
+++ b/third_party/mDNSResponder/0004-Track-interface-socket-family.patch
@@ -0,0 +1,54 @@
+From 542c1b2ce1dcc069cf848d11978c8b6ae5982b6e Mon Sep 17 00:00:00 2001
+Message-ID: <542c1b2ce1dcc069cf848d11978c8b6ae5982b6e.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Wed, 28 Jun 2017 17:30:00 -0500
+Subject: [PATCH] Track interface socket family
+
+Tracks the socket family associated with the interface.
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+---
+ mDNSPosix/mDNSPosix.c | 1 +
+ mDNSPosix/mDNSPosix.h | 2 ++
+ 2 files changed, 3 insertions(+)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index a32a880..9a5b4d7 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1415,6 +1415,7 @@ mDNSlocal int SetupOneInterface(mDNS *const m, struct sockaddr *intfAddr, struct
+         // Set up the extra fields in PosixNetworkInterface.
+         assert(intf->intfName != NULL);         // intf->intfName already set up above
+         intf->index                = intfIndex;
++        intf->sa_family            = intfAddr->sa_family;
+         intf->multicastSocket4     = -1;
+ #if HAVE_IPV6
+         intf->multicastSocket6     = -1;
+diff --git a/mDNSPosix/mDNSPosix.h b/mDNSPosix/mDNSPosix.h
+index 9675591..dd7864c 100644
+--- a/mDNSPosix/mDNSPosix.h
++++ b/mDNSPosix/mDNSPosix.h
+@@ -19,6 +19,7 @@
+ #define __mDNSPlatformPosix_h
+ 
+ #include <signal.h>
++#include <sys/socket.h>
+ #include <sys/time.h>
+ 
+ #ifdef  __cplusplus
+@@ -40,6 +41,7 @@ struct PosixNetworkInterface
+     char *                  intfName;
+     PosixNetworkInterface * aliasIntf;
+     int index;
++    sa_family_t sa_family;
+     int multicastSocket4;
+ #if HAVE_IPV6
+     int multicastSocket6;
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0005-Indicate-loopback-interface-to-mDNS-core.patch b/third_party/mDNSResponder/0005-Indicate-loopback-interface-to-mDNS-core.patch
new file mode 100644
index 0000000..f7aa461
--- /dev/null
+++ b/third_party/mDNSResponder/0005-Indicate-loopback-interface-to-mDNS-core.patch
@@ -0,0 +1,61 @@
+From 44385771ef63f081ed7e80eae6f24591046b4c7c Mon Sep 17 00:00:00 2001
+Message-ID: <44385771ef63f081ed7e80eae6f24591046b4c7c.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Tue, 1 Aug 2017 17:06:01 -0500
+Subject: [PATCH] Indicate loopback interface to mDNS core
+
+Tells the mDNS core if an interface is a loopback interface,
+similar to AddInterfaceToList() in the MacOS implementation.
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+---
+ mDNSPosix/mDNSPosix.c | 7 ++++---
+ 1 file changed, 4 insertions(+), 3 deletions(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index 9a5b4d7..02a19b4 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1348,7 +1348,7 @@ mDNSlocal void CleanRecentInterfaces(void)
+ // Creates a PosixNetworkInterface for the interface whose IP address is
+ // intfAddr and whose name is intfName and registers it with mDNS core.
+ mDNSlocal int SetupOneInterface(mDNS *const m, struct sockaddr *intfAddr, struct sockaddr *intfMask,
+-    const mDNSu8 *intfHaddr, mDNSu16 intfHlen, const char *intfName, int intfIndex)
++    const mDNSu8 *intfHaddr, mDNSu16 intfHlen, const char *intfName, int intfIndex, int intfFlags)
+ {
+     int err = 0;
+     PosixNetworkInterface *intf;
+@@ -1411,6 +1411,7 @@ mDNSlocal int SetupOneInterface(mDNS *const m, struct sockaddr *intfAddr, struct
+ 
+         intf->coreIntf.Advertise = m->AdvertiseLocalAddresses;
+         intf->coreIntf.McastTxRx = mDNStrue;
++        intf->coreIntf.Loopback = ((intfFlags & IFF_LOOPBACK) != 0) ? mDNStrue : mDNSfalse;
+ 
+         // Set up the extra fields in PosixNetworkInterface.
+         assert(intf->intfName != NULL);         // intf->intfName already set up above
+@@ -1561,7 +1562,7 @@ mDNSlocal int SetupInterfaceList(mDNS *const m)
+                     }
+ #endif
+                     if (SetupOneInterface(m, i->ifa_addr, i->ifa_netmask,
+-                                          hwaddr, hwaddr_len, i->ifa_name, ifIndex) == 0)
++                                          hwaddr, hwaddr_len, i->ifa_name, ifIndex, i->ifa_flags) == 0)
+                     {
+                         if (i->ifa_addr->sa_family == AF_INET)
+                             foundav4 = mDNStrue;
+@@ -1578,7 +1579,7 @@ mDNSlocal int SetupInterfaceList(mDNS *const m)
+         // if ((m->HostInterfaces == NULL) && (firstLoopback != NULL))
+         if (!foundav4 && firstLoopback)
+             (void) SetupOneInterface(m, firstLoopback->ifa_addr, firstLoopback->ifa_netmask,
+-                NULL, 0, firstLoopback->ifa_name, firstLoopbackIndex);
++                NULL, 0, firstLoopback->ifa_name, firstLoopbackIndex, firstLoopback->ifa_flags);
+     }
+ 
+     // Clean up.
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0006-Use-list-for-changed-interfaces.patch b/third_party/mDNSResponder/0006-Use-list-for-changed-interfaces.patch
new file mode 100644
index 0000000..87ac190
--- /dev/null
+++ b/third_party/mDNSResponder/0006-Use-list-for-changed-interfaces.patch
@@ -0,0 +1,178 @@
+From 2a0f873184068f21e1d0d2a3e0d8c26bc705bf88 Mon Sep 17 00:00:00 2001
+Message-ID: <2a0f873184068f21e1d0d2a3e0d8c26bc705bf88.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Thu, 13 Jul 2017 09:00:00 -0500
+Subject: [PATCH] Use list for changed interfaces
+
+Uses a linked list to store the index of changed network interfaces
+instead of a bitfield. This allows for network interfaces with an
+index greater than 31 (an index of 36 was seen on Android).
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+Change-Id: Ibeab0ec68ca0d21da8384d4362e59afd2951f138
+---
+ mDNSPosix/mDNSPosix.c | 60 +++++++++++++++++++++++++++++++------------
+ 1 file changed, 44 insertions(+), 16 deletions(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index 02a19b4..9867881 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -74,6 +74,14 @@ struct IfChangeRec
+ };
+ typedef struct IfChangeRec IfChangeRec;
+ 
++// Used to build a list of network interface indices
++struct NetworkInterfaceIndex
++{
++    int if_index;
++    struct NetworkInterfaceIndex *Next;
++};
++typedef struct NetworkInterfaceIndex NetworkInterfaceIndex;
++
+ // Note that static data is initialized to zero in (modern) C.
+ static PosixEventSource *gEventSources;             // linked list of PosixEventSource's
+ static sigset_t gEventSignalSet;                // Signals which event loop listens for
+@@ -1621,6 +1629,23 @@ mDNSlocal mStatus OpenIfNotifySocket(int *pFD)
+     return err;
+ }
+ 
++mDNSlocal void AddInterfaceIndexToList(GenLinkedList *list, int if_index)
++{
++    NetworkInterfaceIndex *item;
++
++    for (item = (NetworkInterfaceIndex*)list->Head; item != NULL; item = item->Next)
++    {
++        if (if_index == item->if_index) return;
++    }
++
++    item = mdns_malloc(sizeof *item);
++    if (item == NULL) return;
++
++    item->if_index = if_index;
++    item->Next = NULL;
++    AddToTail(list, item);
++}
++
+ #if MDNS_DEBUGMSGS
+ mDNSlocal void      PrintNetLinkMsg(const struct nlmsghdr *pNLMsg)
+ {
+@@ -1648,14 +1673,13 @@ mDNSlocal void      PrintNetLinkMsg(const struct nlmsghdr *pNLMsg)
+ }
+ #endif
+ 
+-mDNSlocal mDNSu32       ProcessRoutingNotification(int sd)
++mDNSlocal void          ProcessRoutingNotification(int sd, GenLinkedList *changedInterfaces)
+ // Read through the messages on sd and if any indicate that any interface records should
+ // be torn down and rebuilt, return affected indices as a bitmask. Otherwise return 0.
+ {
+     ssize_t readCount;
+     char buff[4096];
+     struct nlmsghdr         *pNLMsg = (struct nlmsghdr*) buff;
+-    mDNSu32 result = 0;
+ 
+     // The structure here is more complex than it really ought to be because,
+     // unfortunately, there's no good way to size a buffer in advance large
+@@ -1691,9 +1715,9 @@ mDNSlocal mDNSu32       ProcessRoutingNotification(int sd)
+ 
+         // Process the NetLink message
+         if (pNLMsg->nlmsg_type == RTM_GETLINK || pNLMsg->nlmsg_type == RTM_NEWLINK)
+-            result |= 1 << ((struct ifinfomsg*) NLMSG_DATA(pNLMsg))->ifi_index;
++            AddInterfaceIndexToList(changedInterfaces, ((struct ifinfomsg*) NLMSG_DATA(pNLMsg))->ifi_index);
+         else if (pNLMsg->nlmsg_type == RTM_DELADDR || pNLMsg->nlmsg_type == RTM_NEWADDR)
+-            result |= 1 << ((struct ifaddrmsg*) NLMSG_DATA(pNLMsg))->ifa_index;
++            AddInterfaceIndexToList(changedInterfaces, ((struct ifaddrmsg*) NLMSG_DATA(pNLMsg))->ifa_index);
+ 
+         // Advance pNLMsg to the next message in the buffer
+         if ((pNLMsg->nlmsg_flags & NLM_F_MULTI) != 0 && pNLMsg->nlmsg_type != NLMSG_DONE)
+@@ -1704,8 +1728,6 @@ mDNSlocal mDNSu32       ProcessRoutingNotification(int sd)
+         else
+             break;  // all done!
+     }
+-
+-    return result;
+ }
+ 
+ #else // USES_NETLINK
+@@ -1737,18 +1759,17 @@ mDNSlocal void      PrintRoutingSocketMsg(const struct ifa_msghdr *pRSMsg)
+ }
+ #endif
+ 
+-mDNSlocal mDNSu32       ProcessRoutingNotification(int sd)
++mDNSlocal void          ProcessRoutingNotification(int sd, GenLinkedList *changedInterfaces)
+ // Read through the messages on sd and if any indicate that any interface records should
+ // be torn down and rebuilt, return affected indices as a bitmask. Otherwise return 0.
+ {
+     ssize_t readCount;
+     char buff[4096];
+     struct ifa_msghdr       *pRSMsg = (struct ifa_msghdr*) buff;
+-    mDNSu32 result = 0;
+ 
+     readCount = read(sd, buff, sizeof buff);
+     if (readCount < (ssize_t) sizeof(struct ifa_msghdr))
+-        return mStatus_UnsupportedErr;      // cannot decipher message
++        return;      // cannot decipher message
+ 
+ #if MDNS_DEBUGMSGS
+     PrintRoutingSocketMsg(pRSMsg);
+@@ -1759,12 +1780,10 @@ mDNSlocal mDNSu32       ProcessRoutingNotification(int sd)
+         pRSMsg->ifam_type == RTM_IFINFO)
+     {
+         if (pRSMsg->ifam_type == RTM_IFINFO)
+-            result |= 1 << ((struct if_msghdr*) pRSMsg)->ifm_index;
++            AddInterfaceIndexToList(changedInterfaces, ((struct if_msghdr*) pRSMsg)->ifm_index);
+         else
+-            result |= 1 << pRSMsg->ifam_index;
++            AddInterfaceIndexToList(changedInterfaces, pRSMsg->ifam_index);
+     }
+-
+-    return result;
+ }
+ 
+ #endif // USES_NETLINK
+@@ -1774,7 +1793,8 @@ mDNSlocal void InterfaceChangeCallback(int fd, void *context)
+ {
+     IfChangeRec     *pChgRec = (IfChangeRec*) context;
+     fd_set readFDs;
+-    mDNSu32 changedInterfaces = 0;
++    GenLinkedList changedInterfaces;
++    NetworkInterfaceIndex *changedInterface;
+     struct timeval zeroTimeout = { 0, 0 };
+ 
+     (void)fd; // Unused
+@@ -1782,17 +1802,25 @@ mDNSlocal void InterfaceChangeCallback(int fd, void *context)
+     FD_ZERO(&readFDs);
+     FD_SET(pChgRec->NotifySD, &readFDs);
+ 
++    InitLinkedList(&changedInterfaces, offsetof(NetworkInterfaceIndex, Next));
++
+     do
+     {
+-        changedInterfaces |= ProcessRoutingNotification(pChgRec->NotifySD);
++        ProcessRoutingNotification(pChgRec->NotifySD, &changedInterfaces);
+     }
+     while (0 < select(pChgRec->NotifySD + 1, &readFDs, (fd_set*) NULL, (fd_set*) NULL, &zeroTimeout));
+ 
+     // Currently we rebuild the entire interface list whenever any interface change is
+     // detected. If this ever proves to be a performance issue in a multi-homed
+     // configuration, more care should be paid to changedInterfaces.
+-    if (changedInterfaces)
++    if (changedInterfaces.Head != NULL)
+         mDNSPlatformPosixRefreshInterfaceList(pChgRec->mDNS);
++
++    while ((changedInterface = (NetworkInterfaceIndex*)changedInterfaces.Head) != NULL)
++    {
++        RemoveFromList(&changedInterfaces, changedInterface);
++        mdns_free(changedInterface);
++    }
+ }
+ 
+ // Register with either a Routing Socket or RtNetLink to listen for interface changes.
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0007-Handle-noisy-netlink-sockets.patch b/third_party/mDNSResponder/0007-Handle-noisy-netlink-sockets.patch
new file mode 100644
index 0000000..08cce01
--- /dev/null
+++ b/third_party/mDNSResponder/0007-Handle-noisy-netlink-sockets.patch
@@ -0,0 +1,255 @@
+From 00289e89cccb9567d6ea6bd2a394fd14b61e5ad1 Mon Sep 17 00:00:00 2001
+Message-ID: <00289e89cccb9567d6ea6bd2a394fd14b61e5ad1.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Mon, 24 Jul 2017 09:38:55 -0500
+Subject: [PATCH] Handle noisy netlink sockets
+
+The POSIX implementation currently clears all network interfaces
+when netlink indicates that there has been a change. This causes
+the following problems:
+
+  1) Applications are informed that all of the services they are
+     tracking have been removed.
+  2) Increases network load because the client must re-query for
+     all records it is interested in.
+
+This changes netlink notification handling by:
+
+  1) Always comparing with the latest interface list returned
+     by the OS.
+  2) Confirming that the interface has been changed in a way
+     that we care about.
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+---
+ mDNSPosix/mDNSPosix.c | 182 +++++++++++++++++++++++++++++++++++++++---
+ 1 file changed, 172 insertions(+), 10 deletions(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index 9867881..ad7000d 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1788,14 +1788,43 @@ mDNSlocal void          ProcessRoutingNotification(int sd, GenLinkedList *change
+ 
+ #endif // USES_NETLINK
+ 
++// Test whether the given PosixNetworkInterface matches the given struct ifaddrs
++mDNSlocal mDNSBool InterfacesMatch(PosixNetworkInterface *intf, struct ifaddrs *ifi)
++{
++    mDNSBool match = mDNSfalse;
++    mDNSAddr ip, mask;
++    int if_index;
++
++    if_index = if_nametoindex(ifi->ifa_name);
++    if (if_index == 0)
++        return mDNSfalse;
++
++    if((intf->index == if_index) &&
++       (intf->sa_family == ifi->ifa_addr->sa_family) &&
++       (strcmp(intf->coreIntf.ifname, ifi->ifa_name) == 0))
++        {
++        SockAddrTomDNSAddr(ifi->ifa_addr,    &ip,   NULL);
++        SockAddrTomDNSAddr(ifi->ifa_netmask, &mask, NULL);
++
++        match = mDNSSameAddress(&intf->coreIntf.ip, &ip) &&
++                mDNSSameAddress(&intf->coreIntf.mask, &mask);
++        }
++
++    return match;
++}
++
+ // Called when data appears on interface change notification socket
+ mDNSlocal void InterfaceChangeCallback(int fd, void *context)
+ {
+     IfChangeRec     *pChgRec = (IfChangeRec*) context;
++    mDNS            *m = pChgRec->mDNS;
+     fd_set readFDs;
+     GenLinkedList changedInterfaces;
+     NetworkInterfaceIndex *changedInterface;
+     struct timeval zeroTimeout = { 0, 0 };
++    struct ifaddrs *ifa_list, **ifi, *ifa_loop4 = NULL;
++    PosixNetworkInterface *intf, *intfNext;
++    mDNSBool found, foundav4;
+ 
+     (void)fd; // Unused
+ 
+@@ -1810,12 +1839,149 @@ mDNSlocal void InterfaceChangeCallback(int fd, void *context)
+     }
+     while (0 < select(pChgRec->NotifySD + 1, &readFDs, (fd_set*) NULL, (fd_set*) NULL, &zeroTimeout));
+ 
+-    // Currently we rebuild the entire interface list whenever any interface change is
+-    // detected. If this ever proves to be a performance issue in a multi-homed
+-    // configuration, more care should be paid to changedInterfaces.
+-    if (changedInterfaces.Head != NULL)
+-        mDNSPlatformPosixRefreshInterfaceList(pChgRec->mDNS);
++    CleanRecentInterfaces();
++
++    if (changedInterfaces.Head == NULL) goto cleanup;
++
++    if (getifaddrs(&ifa_list) < 0) goto cleanup;
++
++    for (intf = (PosixNetworkInterface*)(m->HostInterfaces); intf != NULL; intf = intfNext)
++    {
++        intfNext = (PosixNetworkInterface*)(intf->coreIntf.next);
++
++        // Loopback interface(s) are handled later
++        if (intf->coreIntf.Loopback) continue;
++
++        found = mDNSfalse;
++        for (ifi = &ifa_list; *ifi != NULL; ifi = &(*ifi)->ifa_next)
++        {
++            if (InterfacesMatch(intf, *ifi))
++            {
++                found = mDNStrue;
++                break;
++            }
++        }
++
++        // Removes changed and old interfaces from m->HostInterfaces
++        if (!found) TearDownInterface(m, intf);
++    }
++
++    // Add new and changed interfaces in ifa_list
++    // Save off loopback interface in case it is needed later
++    for (ifi = &ifa_list; *ifi != NULL; ifi = &(*ifi)->ifa_next)
++    {
++        found = mDNSfalse;
++        for (intf = (PosixNetworkInterface*)(m->HostInterfaces); intf != NULL; intf = intfNext)
++        {
++            intfNext = (PosixNetworkInterface*)(intf->coreIntf.next);
++
++            // Loopback interface(s) are handled later
++            if (intf->coreIntf.Loopback) continue;
++
++            if (InterfacesMatch(intf, *ifi))
++            {
++                found = mDNStrue;
++                break;
++            }
++
++            // Removes changed and old interfaces from m->HostInterfaces
++        }
++        if (found)
++	    continue;
++
++        if ((ifa_loop4 == NULL) &&
++            ((*ifi)->ifa_addr->sa_family == AF_INET) &&
++            ((*ifi)->ifa_flags & IFF_UP) &&
++            ((*ifi)->ifa_flags & IFF_LOOPBACK))
++        {
++            ifa_loop4 = *ifi;
++            continue;
++        }
++
++        if (     (((*ifi)->ifa_addr->sa_family == AF_INET)
++#if HAVE_IPV6
++                  || ((*ifi)->ifa_addr->sa_family == AF_INET6)
++#endif
++                  ) && ((*ifi)->ifa_flags & IFF_UP)
++                    && !((*ifi)->ifa_flags & IFF_POINTOPOINT)
++                    && !((*ifi)->ifa_flags & IFF_LOOPBACK))
++        {
++            struct ifaddrs *i = *ifi;
++
++#define ethernet_addr_len 6
++            uint8_t hwaddr[ethernet_addr_len];
++            int hwaddr_len = 0;
++
++#if defined(TARGET_OS_LINUX) && TARGET_OS_LINUX
++            struct ifreq ifr;
++            int sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
++            if (sockfd >= 0)
++            {
++                /* Add hardware address */
++                memcpy(ifr.ifr_name, i->ifa_name, IFNAMSIZ);
++                if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) != -1)
++                {
++                    if (ifr.ifr_hwaddr.sa_family == ARPHRD_ETHER)
++                    {
++                        memcpy(hwaddr, ifr.ifr_hwaddr.sa_data, ethernet_addr_len);
++                        hwaddr_len = ethernet_addr_len;
++                    }
++                }
++                close(sockfd);
++            }
++            else
++            {
++                memset(hwaddr, 0, sizeof(hwaddr));
++            }
++#endif // TARGET_OS_LINUX
++            SetupOneInterface(m, i->ifa_addr, i->ifa_netmask,
++                              hwaddr, hwaddr_len, i->ifa_name, if_nametoindex(i->ifa_name), i->ifa_flags);
++        }
++    }
++
++    // Determine if there is at least one non-loopback IPv4 interface. This is to work around issues
++    // with multicast loopback on IPv6 interfaces -- see corresponding logic in SetupInterfaceList().
++    foundav4 = mDNSfalse;
++    for (intf = (PosixNetworkInterface*)(m->HostInterfaces); intf != NULL; intf = (PosixNetworkInterface*)(intf->coreIntf.next))
++    {
++        if (intf->sa_family == AF_INET && !intf->coreIntf.Loopback)
++        {
++            foundav4 = mDNStrue;
++            break;
++        }
++    }
++
++    if (foundav4)
++    {
++        for (intf = (PosixNetworkInterface*)(m->HostInterfaces); intf != NULL; intf = intfNext)
++        {
++            intfNext = (PosixNetworkInterface*)(intf->coreIntf.next);
++            if (intf->coreIntf.Loopback) TearDownInterface(m, intf);
++        }
++    }
++    else
++    {
++        found = mDNSfalse;
++
++        for (intf = (PosixNetworkInterface*)(m->HostInterfaces); intf != NULL; intf = (PosixNetworkInterface*)(intf->coreIntf.next))
++        {
++            if (intf->coreIntf.Loopback)
++            {
++                found = mDNStrue;
++                break;
++            }
++        }
++
++        if (!found && (ifa_loop4 != NULL))
++        {
++            SetupOneInterface(m, ifa_loop4->ifa_addr, ifa_loop4->ifa_netmask,
++                              NULL, 0, ifa_loop4->ifa_name, if_nametoindex(ifa_loop4->ifa_name), ifa_loop4->ifa_flags);
++        }
++    }
++
++    if (ifa_list != NULL) freeifaddrs(ifa_list);
+ 
++cleanup:
+     while ((changedInterface = (NetworkInterfaceIndex*)changedInterfaces.Head) != NULL)
+     {
+         RemoveFromList(&changedInterfaces, changedInterface);
+@@ -1947,15 +2113,11 @@ mDNSexport void mDNSPlatformClose(mDNS *const m)
+ #endif
+ }
+ 
+-// This is used internally by InterfaceChangeCallback.
+-// It's also exported so that the Standalone Responder (mDNSResponderPosix)
++// This is exported so that the Standalone Responder (mDNSResponderPosix)
+ // can call it in response to a SIGHUP (mainly for debugging purposes).
+ mDNSexport mStatus mDNSPlatformPosixRefreshInterfaceList(mDNS *const m)
+ {
+     int err;
+-    // This is a pretty heavyweight way to process interface changes --
+-    // destroying the entire interface list and then making fresh one from scratch.
+-    // We should make it like the OS X version, which leaves unchanged interfaces alone.
+     ClearInterfaceList(m);
+     err = SetupInterfaceList(m);
+     return PosixErrorToStatus(err);
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0008-Mark-deleted-interfaces-as-being-changed.patch b/third_party/mDNSResponder/0008-Mark-deleted-interfaces-as-being-changed.patch
new file mode 100644
index 0000000..216fde7
--- /dev/null
+++ b/third_party/mDNSResponder/0008-Mark-deleted-interfaces-as-being-changed.patch
@@ -0,0 +1,43 @@
+From 8ebfeaf55ab364a1e51a3438dfa9a742a01b8d36 Mon Sep 17 00:00:00 2001
+Message-ID: <8ebfeaf55ab364a1e51a3438dfa9a742a01b8d36.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Wed, 9 Aug 2017 09:16:58 -0500
+Subject: [PATCH] Mark deleted interfaces as being changed
+
+Netlink notification handling ignores messages for deleted links,
+RTM_DELLINK. It does handle RTM_GETLINK. According to libnl docu-
+mentation (http://www.infradead.org/~tgr/libnl/doc/route.html)
+RTM_DELLINK can be sent by the kernel, but RTM_GETLINK cannot.
+There was likely a mixup in the original implementation, so this
+change replaces handling for RTM_GETLINK with RTM_DELLINK.
+
+Testing and Verification Instructions:
+  1. Use ip-link to add and remove a VLAN interface and verify
+     that mDNSResponder handles the deleted link.
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+---
+ mDNSPosix/mDNSPosix.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index ad7000d..010f266 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1714,7 +1714,7 @@ mDNSlocal void          ProcessRoutingNotification(int sd, GenLinkedList *change
+ #endif
+ 
+         // Process the NetLink message
+-        if (pNLMsg->nlmsg_type == RTM_GETLINK || pNLMsg->nlmsg_type == RTM_NEWLINK)
++        if (pNLMsg->nlmsg_type == RTM_DELLINK || pNLMsg->nlmsg_type == RTM_NEWLINK)
+             AddInterfaceIndexToList(changedInterfaces, ((struct ifinfomsg*) NLMSG_DATA(pNLMsg))->ifi_index);
+         else if (pNLMsg->nlmsg_type == RTM_DELADDR || pNLMsg->nlmsg_type == RTM_NEWADDR)
+             AddInterfaceIndexToList(changedInterfaces, ((struct ifaddrmsg*) NLMSG_DATA(pNLMsg))->ifa_index);
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0009-Handle-errors-from-socket-calls.patch b/third_party/mDNSResponder/0009-Handle-errors-from-socket-calls.patch
new file mode 100644
index 0000000..2057e2c
--- /dev/null
+++ b/third_party/mDNSResponder/0009-Handle-errors-from-socket-calls.patch
@@ -0,0 +1,66 @@
+From dae89c4e97faf408394961c0f4b1577a7d5976cc Mon Sep 17 00:00:00 2001
+Message-ID: <dae89c4e97faf408394961c0f4b1577a7d5976cc.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Nate Karstens <nate.karstens@garmin.com>
+Date: Thu, 10 Aug 2017 08:27:32 -0500
+Subject: [PATCH] Handle errors from socket calls
+
+Adds handling for socket() or read() returning a
+negative value (indicating an error has occurred).
+
+Upstream-Status: Submitted [dts@apple.com]
+
+Signed-off-by: Nate Karstens <nate.karstens@garmin.com>
+Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
+---
+ mDNSPosix/mDNSPosix.c | 12 +++++++++---
+ 1 file changed, 9 insertions(+), 3 deletions(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index 010f266..89e108f 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1677,7 +1677,7 @@ mDNSlocal void          ProcessRoutingNotification(int sd, GenLinkedList *change
+ // Read through the messages on sd and if any indicate that any interface records should
+ // be torn down and rebuilt, return affected indices as a bitmask. Otherwise return 0.
+ {
+-    ssize_t readCount;
++    ssize_t readVal, readCount;
+     char buff[4096];
+     struct nlmsghdr         *pNLMsg = (struct nlmsghdr*) buff;
+ 
+@@ -1686,7 +1686,10 @@ mDNSlocal void          ProcessRoutingNotification(int sd, GenLinkedList *change
+     // enough to hold all pending data and so avoid message fragmentation.
+     // (Note that FIONREAD is not supported on AF_NETLINK.)
+ 
+-    readCount = read(sd, buff, sizeof buff);
++    readVal = read(sd, buff, sizeof buff);
++    if (readVal < 0) return;
++    readCount = readVal;
++
+     while (1)
+     {
+         // Make sure we've got an entire nlmsghdr in the buffer, and payload, too.
+@@ -1702,7 +1705,9 @@ mDNSlocal void          ProcessRoutingNotification(int sd, GenLinkedList *change
+                 pNLMsg = (struct nlmsghdr*) buff;
+ 
+                 // read more data
+-                readCount += read(sd, buff + readCount, sizeof buff - readCount);
++                readVal = read(sd, buff + readCount, sizeof buff - readCount);
++                if (readVal < 0) return;
++                readCount += readVal;
+                 continue;                   // spin around and revalidate with new readCount
+             }
+             else
+@@ -2017,6 +2022,7 @@ mDNSlocal mDNSBool mDNSPlatformInit_CanReceiveUnicast(void)
+     int err;
+     int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+     struct sockaddr_in s5353;
++    if (s < 0) return mDNSfalse;
+     s5353.sin_family      = AF_INET;
+     s5353.sin_port        = MulticastDNSPort.NotAnInteger;
+     s5353.sin_addr.s_addr = 0;
+-- 
+2.41.0
+
diff --git a/third_party/mDNSResponder/0010-Handle-interface-without-ifa_addr.patch b/third_party/mDNSResponder/0010-Handle-interface-without-ifa_addr.patch
new file mode 100644
index 0000000..602b205
--- /dev/null
+++ b/third_party/mDNSResponder/0010-Handle-interface-without-ifa_addr.patch
@@ -0,0 +1,41 @@
+From e501d58e9ec6cb6e19a682d425fa638069585fbc Mon Sep 17 00:00:00 2001
+Message-ID: <e501d58e9ec6cb6e19a682d425fa638069585fbc.1687508149.git.stefan@agner.ch>
+In-Reply-To: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+References: <e136dcdcdd93ef32ada981e89c195905eb809eea.1687508149.git.stefan@agner.ch>
+From: Stefan Agner <stefan@agner.ch>
+Date: Fri, 23 Jun 2023 10:10:00 +0200
+Subject: [PATCH] Handle interface without `ifa_addr`
+
+It seems that certain interface types may have `ifa_addr` set to null.
+Handle this case gracefully.
+
+Signed-off-by: Stefan Agner <stefan@agner.ch>
+---
+ mDNSPosix/mDNSPosix.c | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/mDNSPosix/mDNSPosix.c b/mDNSPosix/mDNSPosix.c
+index 89e108f..2056871 100644
+--- a/mDNSPosix/mDNSPosix.c
++++ b/mDNSPosix/mDNSPosix.c
+@@ -1895,6 +1895,7 @@ mDNSlocal void InterfaceChangeCallback(int fd, void *context)
+ 	    continue;
+ 
+         if ((ifa_loop4 == NULL) &&
++            ((*ifi)->ifa_addr != NULL) &&
+             ((*ifi)->ifa_addr->sa_family == AF_INET) &&
+             ((*ifi)->ifa_flags & IFF_UP) &&
+             ((*ifi)->ifa_flags & IFF_LOOPBACK))
+@@ -1903,7 +1904,8 @@ mDNSlocal void InterfaceChangeCallback(int fd, void *context)
+             continue;
+         }
+ 
+-        if (     (((*ifi)->ifa_addr->sa_family == AF_INET)
++        if (     ((*ifi)->ifa_addr != NULL) &&
++                 (((*ifi)->ifa_addr->sa_family == AF_INET)
+ #if HAVE_IPV6
+                   || ((*ifi)->ifa_addr->sa_family == AF_INET6)
+ #endif
+-- 
+2.41.0
+
diff --git a/third_party/openthread/CMakeLists.txt b/third_party/openthread/CMakeLists.txt
index af8ddf4..03ad863 100644
--- a/third_party/openthread/CMakeLists.txt
+++ b/third_party/openthread/CMakeLists.txt
@@ -35,6 +35,7 @@
 set(OT_BORDER_ROUTER ON CACHE STRING "enable border router feature" FORCE)
 set(OT_BORDER_ROUTING ${OTBR_BORDER_ROUTING} CACHE STRING "enable border routing feature" FORCE)
 set(OT_BORDER_ROUTING_COUNTERS ${OTBR_BORDER_ROUTING_COUNTERS} CACHE STRING "enable border routing counters feature" FORCE)
+set(OT_BORDER_ROUTING_DHCP6_PD ${OTBR_DHCP6_PD} CACHE STRING "enable dhcpv6 pd support in border routing" FORCE)
 set(OT_BUILD_EXECUTABLES OFF CACHE STRING "disable building executables" FORCE)
 set(OT_BUILTIN_MBEDTLS_MANAGEMENT OFF CACHE STRING "diable mbedTLS management" FORCE)
 set(OT_CHILD_SUPERVISION ON CACHE STRING "enable child supervision" FORCE)