[trel-dnssd] Integrate TREL DNS-SD with avahi and mdnssd (#1109)

This commit implements TREL DNS-SD module:
- Register local TREL service to mDNS publishers
- Browse remote TREL services from mDNS Publishers and maintain TREL
  peers

Some hints on implementation:
- TREL DNS-SD uses Extended Address as the TREL service instance name
- TREL DNS-SD pauses until all three conditions are met:
  1. TREL DNS-SD was enabled by TREL core
  2. TREL network interface (e.g. WiFi interface) is ready
  3. mDNS Publisher is ready
diff --git a/Android.mk b/Android.mk
index fadc412..5d526fb 100644
--- a/Android.mk
+++ b/Android.mk
@@ -135,6 +135,7 @@
     src/sdp_proxy/discovery_proxy.cpp \
     src/utils/dns_utils.cpp \
     src/utils/hex.cpp \
+    src/utils/string_utils.cpp \
     src/utils/thread_helper.cpp \
 
 LOCAL_STATIC_LIBRARIES += \
diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake
index 5a03d62..a840b82 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -79,4 +79,10 @@
     target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_UNSECURE_JOIN=1)
 endif()
 
+option(OTBR_TREL "Enable TREL link support." OFF)
+if(OTBR_TREL)
+    target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_TREL=1)
+endif()
+
+
 option(OTBR_WEB "Enable Web GUI" OFF)
diff --git a/etc/docker/docker_entrypoint.sh b/etc/docker/docker_entrypoint.sh
index 3cc1d02..60cfb88 100755
--- a/etc/docker/docker_entrypoint.sh
+++ b/etc/docker/docker_entrypoint.sh
@@ -38,6 +38,11 @@
                 shift
                 shift
                 ;;
+            --trel-url)
+                TREL_URL="$2"
+                shift
+                shift
+                ;;
             --interface | -I)
                 TUN_INTERFACE_NAME=$2
                 shift
@@ -71,6 +76,7 @@
 parse_args "$@"
 
 [ -n "$RADIO_URL" ] || RADIO_URL="spinel+hdlc+uart:///dev/ttyUSB0"
+[ -n "$TREL_URL" ] || TREL_URL=""
 [ -n "$TUN_INTERFACE_NAME" ] || TUN_INTERFACE_NAME="wpan0"
 [ -n "$BACKBONE_INTERFACE" ] || BACKBONE_INTERFACE="eth0"
 [ -n "$AUTO_PREFIX_ROUTE" ] || AUTO_PREFIX_ROUTE=true
@@ -78,6 +84,7 @@
 [ -n "$NAT64_PREFIX" ] || NAT64_PREFIX="64:ff9b::/96"
 
 echo "RADIO_URL:" $RADIO_URL
+echo "TREL_URL:" $TREL_URL
 echo "TUN_INTERFACE_NAME:" $TUN_INTERFACE_NAME
 echo "BACKBONE_INTERFACE: $BACKBONE_INTERFACE"
 echo "NAT64_PREFIX:" $NAT64_PREFIX
@@ -90,7 +97,7 @@
 sed -i "s/dns64.*$/dns64 $NAT64_PREFIX {};/" /etc/bind/named.conf.options
 sed -i "s/$INFRA_IF_NAME/$BACKBONE_INTERFACE/" /etc/sysctl.d/60-otbr-accept-ra.conf
 
-echo "OTBR_AGENT_OPTS=\"-I $TUN_INTERFACE_NAME -B $BACKBONE_INTERFACE -d7 $RADIO_URL\"" >/etc/default/otbr-agent
+echo "OTBR_AGENT_OPTS=\"-I $TUN_INTERFACE_NAME -B $BACKBONE_INTERFACE -d7 $RADIO_URL $TREL_URL\"" >/etc/default/otbr-agent
 echo "OTBR_WEB_OPTS=\"-I $TUN_INTERFACE_NAME -d7 -p 80\"" >/etc/default/otbr-web
 
 /app/script/server
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b9027e1..26f3b17 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -33,6 +33,7 @@
 add_subdirectory(common)
 add_subdirectory(ncp)
 add_subdirectory(sdp_proxy)
+add_subdirectory(trel_dnssd)
 
 if(OTBR_DBUS)
     add_subdirectory(dbus)
diff --git a/src/border_agent/CMakeLists.txt b/src/border_agent/CMakeLists.txt
index 9b60a6e..c58acdb 100644
--- a/src/border_agent/CMakeLists.txt
+++ b/src/border_agent/CMakeLists.txt
@@ -34,5 +34,6 @@
 target_link_libraries(otbr-border-agent PRIVATE
     $<$<BOOL:${OTBR_MDNS}>:otbr-mdns>
     $<$<BOOL:${OTBR_BACKBONE_ROUTER}>:otbr-backbone-router>
+    otbr-trel-dnssd
     otbr-common
 )
diff --git a/src/border_agent/border_agent.cpp b/src/border_agent/border_agent.cpp
index a4adaba..87490ca 100644
--- a/src/border_agent/border_agent.cpp
+++ b/src/border_agent/border_agent.cpp
@@ -101,6 +101,9 @@
 #if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
     , mDiscoveryProxy(aNcp, *mPublisher)
 #endif
+#if OTBR_ENABLE_TREL
+    , mTrelDnssd(aNcp, *mPublisher)
+#endif
 {
 }
 
@@ -177,6 +180,9 @@
 #if OTBR_ENABLE_SRP_ADVERTISING_PROXY
         mAdvertisingProxy.PublishAllHostsAndServices();
 #endif
+#if OTBR_ENABLE_TREL
+        mTrelDnssd.OnMdnsPublisherReady();
+#endif
         break;
     default:
         otbrLogWarning("mDNS publisher not available!");
diff --git a/src/border_agent/border_agent.hpp b/src/border_agent/border_agent.hpp
index d946e57..ece48c6 100644
--- a/src/border_agent/border_agent.hpp
+++ b/src/border_agent/border_agent.hpp
@@ -50,6 +50,7 @@
 #include "ncp/ncp_openthread.hpp"
 #include "sdp_proxy/advertising_proxy.hpp"
 #include "sdp_proxy/discovery_proxy.hpp"
+#include "trel_dnssd/trel_dnssd.hpp"
 
 #ifndef OTBR_VENDOR_NAME
 #define OTBR_VENDOR_NAME "OpenThread"
@@ -176,6 +177,9 @@
 #if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY
     Dnssd::DiscoveryProxy mDiscoveryProxy;
 #endif
+#if OTBR_ENABLE_TREL
+    TrelDnssd::TrelDnssd mTrelDnssd;
+#endif
 };
 
 /**
diff --git a/src/common/logging.cpp b/src/common/logging.cpp
index 7e08172..01c8811 100644
--- a/src/common/logging.cpp
+++ b/src/common/logging.cpp
@@ -127,7 +127,7 @@
 }
 
 /** Hex dump data to the log */
-void otbrDump(otbrLogLevel aLevel, const char *aPrefix, const void *aMemory, size_t aSize)
+void otbrDump(otbrLogLevel aLevel, const char *aLogTag, const char *aPrefix, const void *aMemory, size_t aSize)
 {
     static const char kHexChars[] = "0123456789abcdef";
     assert(aPrefix && (aMemory || aSize == 0));
@@ -173,7 +173,7 @@
         }
         *ch = 0;
 
-        syslog(static_cast<int>(aLevel), "%s: %04x: %s", aPrefix, addr, hex);
+        otbrLog(aLevel, aLogTag, "%s: %04x: %s", aPrefix, addr, hex);
     }
 }
 
diff --git a/src/common/logging.hpp b/src/common/logging.hpp
index 9d6f61a..e90ba26 100644
--- a/src/common/logging.hpp
+++ b/src/common/logging.hpp
@@ -117,12 +117,13 @@
  * This function dump memory as hex string at level @p aLevel.
  *
  * @param[in] aLevel   Log level of the logger.
+ * @param[in] aLogTag  Log tag.
  * @param[in] aPrefix  String before dumping memory.
  * @param[in] aMemory  The pointer to the memory to be dumped.
  * @param[in] aSize    The size of memory in bytes to be dumped.
  *
  */
-void otbrDump(otbrLogLevel aLevel, const char *aPrefix, const void *aMemory, size_t aSize);
+void otbrDump(otbrLogLevel aLevel, const char *aLogTag, const char *aPrefix, const void *aMemory, size_t aSize);
 
 /**
  * This function converts error code to string.
diff --git a/src/mdns/mdns.cpp b/src/mdns/mdns.cpp
index e06263c..490b7b6 100644
--- a/src/mdns/mdns.cpp
+++ b/src/mdns/mdns.cpp
@@ -79,6 +79,40 @@
     return error;
 }
 
+otbrError Publisher::DecodeTxtData(Publisher::TxtList &aTxtList, const uint8_t *aTxtData, uint16_t aTxtLength)
+{
+    otbrError error = OTBR_ERROR_NONE;
+
+    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;
+
+        while (keyEnd < entryEnd && aTxtData[keyEnd] != '=')
+        {
+            keyEnd++;
+        }
+
+        valStart = keyEnd;
+        if (valStart < entryEnd && aTxtData[valStart] == '=')
+        {
+            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:
+    return error;
+}
+
 void Publisher::RemoveSubscriptionCallbacks(uint64_t aSubscriberId)
 {
     size_t erased;
diff --git a/src/mdns/mdns.hpp b/src/mdns/mdns.hpp
index a9b18bf..ab8fc5b 100644
--- a/src/mdns/mdns.hpp
+++ b/src/mdns/mdns.hpp
@@ -416,9 +416,29 @@
      * @retval OTBR_ERROR_NONE          Successfully write the TXT entry list.
      * @retval OTBR_ERROR_INVALID_ARGS  The @p aTxtList includes invalid TXT entry.
      *
+     * @sa DecodeTxtData
+     *
      */
     static otbrError EncodeTxtData(const TxtList &aTxtList, std::vector<uint8_t> &aTxtData);
 
+    /**
+     * This function decodes a TXT entry list from a TXT data buffer.
+     *
+     * The input data should be in standard DNS-SD TXT data format.
+     * See RFC 6763 for details: https://tools.ietf.org/html/rfc6763#section-6.
+     *
+     * @param[out]  aTxtList    A TXT entry list.
+     * @param[in]   aTxtData    A pointer to TXT data.
+     * @param[in]   aTxtLength  The TXT data length.
+     *
+     * @retval OTBR_ERROR_NONE          Successfully decoded the TXT data.
+     * @retval OTBR_ERROR_INVALID_ARGS  The @p aTxtdata has invalid TXT format.
+     *
+     * @sa EncodeTxtData
+     *
+     */
+    static otbrError DecodeTxtData(TxtList &aTxtList, const uint8_t *aTxtData, uint16_t aTxtLength);
+
 protected:
     enum : uint8_t
     {
diff --git a/src/trel_dnssd/CMakeLists.txt b/src/trel_dnssd/CMakeLists.txt
new file mode 100644
index 0000000..b0c4a93
--- /dev/null
+++ b/src/trel_dnssd/CMakeLists.txt
@@ -0,0 +1,37 @@
+#
+#  Copyright (c) 2021, 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.
+#
+
+add_library(otbr-trel-dnssd
+    trel_dnssd.cpp
+    trel_dnssd.hpp
+)
+
+target_link_libraries(otbr-trel-dnssd PRIVATE
+    $<$<BOOL:${OTBR_MDNS}>:otbr-mdns>
+    otbr-common
+)
diff --git a/src/trel_dnssd/trel_dnssd.cpp b/src/trel_dnssd/trel_dnssd.cpp
new file mode 100644
index 0000000..e2f9726
--- /dev/null
+++ b/src/trel_dnssd/trel_dnssd.cpp
@@ -0,0 +1,502 @@
+/*
+ *    Copyright (c) 2021, The OpenThread Authors.
+ *    All rights reserved.
+ *
+ *    Redistribution and use in source and binary forms, with or without
+ *    modification, are permitted provided that the following conditions are met:
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *    3. Neither the name of the copyright holder nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ *    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *    POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file includes implementation of TREL DNS-SD over mDNS.
+ */
+
+#if OTBR_ENABLE_TREL
+
+#define OTBR_LOG_TAG "TrelDns"
+
+#include "trel_dnssd/trel_dnssd.hpp"
+
+#include <net/if.h>
+
+#include <openthread/instance.h>
+#include <openthread/link.h>
+#include <openthread/platform/trel.h>
+
+#include "common/code_utils.hpp"
+#include "utils/hex.hpp"
+#include "utils/string_utils.hpp"
+
+static const char kTrelServiceName[] = "_trel._udp";
+
+static otbr::TrelDnssd::TrelDnssd *sTrelDnssd = nullptr;
+
+void trelDnssdInitialize(const char *aTrelNetif)
+{
+    sTrelDnssd->Initialize(aTrelNetif);
+}
+
+void trelDnssdStartBrowse(void)
+{
+    sTrelDnssd->StartBrowse();
+}
+
+void trelDnssdStopBrowse(void)
+{
+    sTrelDnssd->StopBrowse();
+}
+
+void trelDnssdRegisterService(uint16_t aPort, const uint8_t *aTxtData, uint8_t aTxtLength)
+{
+    sTrelDnssd->RegisterService(aPort, aTxtData, aTxtLength);
+}
+
+void trelDnssdRemoveService(void)
+{
+    sTrelDnssd->UnregisterService();
+}
+
+namespace otbr {
+
+namespace TrelDnssd {
+
+TrelDnssd::TrelDnssd(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher)
+    : mPublisher(aPublisher)
+    , mNcp(aNcp)
+{
+    sTrelDnssd = this;
+}
+
+void TrelDnssd::Initialize(std::string aTrelNetif)
+{
+    mTrelNetif = std::move(aTrelNetif);
+
+    if (IsInitialized())
+    {
+        otbrLogDebug("Initialized on netif \"%s\"", mTrelNetif.c_str());
+        CheckTrelNetifReady();
+    }
+    else
+    {
+        otbrLogDebug("Not initialized");
+    }
+}
+
+void TrelDnssd::StartBrowse(void)
+{
+    VerifyOrExit(IsInitialized());
+
+    otbrLogDebug("Start browsing %s services ...", kTrelServiceName);
+
+    assert(mSubscriberId == 0);
+    mSubscriberId = mPublisher.AddSubscriptionCallbacks(
+        [this](const std::string &aType, const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo) {
+            OnTrelServiceInstanceResolved(aType, aInstanceInfo);
+        },
+        /* aHostCallback */ nullptr);
+
+    if (IsReady())
+    {
+        mPublisher.SubscribeService(kTrelServiceName, /* aInstanceName */ "");
+    }
+
+exit:
+    return;
+}
+
+void TrelDnssd::StopBrowse(void)
+{
+    VerifyOrExit(IsInitialized());
+
+    otbrLogDebug("Stop browsing %s service.", kTrelServiceName);
+    assert(mSubscriberId > 0);
+
+    mPublisher.RemoveSubscriptionCallbacks(mSubscriberId);
+    mSubscriberId = 0;
+
+    if (IsReady())
+    {
+        mPublisher.UnsubscribeService(kTrelServiceName, "");
+    }
+
+exit:
+    return;
+}
+
+void TrelDnssd::RegisterService(uint16_t aPort, const uint8_t *aTxtData, uint8_t aTxtLength)
+{
+    assert(aPort > 0);
+    assert(aTxtData != nullptr);
+
+    VerifyOrExit(IsInitialized());
+
+    otbrLogDebug("Register %s service: port=%u, TXT=%d bytes", kTrelServiceName, aPort, aTxtLength);
+    otbrDump(OTBR_LOG_DEBUG, OTBR_LOG_TAG, "TXT", aTxtData, aTxtLength);
+
+    if (mRegisterInfo.IsValid() && IsReady())
+    {
+        UnpublishTrelService();
+    }
+
+    mRegisterInfo.Assign(aPort, aTxtData, aTxtLength);
+
+    if (IsReady())
+    {
+        PublishTrelService();
+    }
+
+exit:
+    return;
+}
+
+void TrelDnssd::UnregisterService(void)
+{
+    VerifyOrExit(IsInitialized());
+
+    otbrLogDebug("Remove %s service", kTrelServiceName);
+    assert(mRegisterInfo.IsValid());
+
+    if (IsReady())
+    {
+        UnpublishTrelService();
+    }
+
+    mRegisterInfo.Clear();
+
+exit:
+    return;
+}
+
+void TrelDnssd::OnMdnsPublisherReady(void)
+{
+    VerifyOrExit(IsInitialized());
+
+    otbrLogDebug("mDNS Publisher is Ready");
+    mMdnsPublisherReady = true;
+    RemoveAllPeers();
+
+    if (mRegisterInfo.IsPublished())
+    {
+        mRegisterInfo.mInstanceName = "";
+    }
+
+    OnBecomeReady();
+
+exit:
+    return;
+}
+
+void TrelDnssd::OnTrelServiceInstanceResolved(const std::string &                            aType,
+                                              const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo)
+{
+    VerifyOrExit(StringUtils::EqualCaseInsensitive(aType, kTrelServiceName));
+    VerifyOrExit(aInstanceInfo.mNetifIndex == mTrelNetifIndex);
+
+    if (aInstanceInfo.mRemoved)
+    {
+        OnTrelServiceInstanceRemoved(aInstanceInfo.mName);
+    }
+    else
+    {
+        OnTrelServiceInstanceAdded(aInstanceInfo);
+    }
+
+exit:
+    return;
+}
+
+std::string TrelDnssd::GetTrelInstanceName(void)
+{
+    const otExtAddress *extaddr = otLinkGetExtendedAddress(mNcp.GetInstance());
+    std::string         name;
+    char                nameBuf[sizeof(extaddr) * 2 + 1];
+
+    Utils::Bytes2Hex(extaddr->m8, sizeof(extaddr), nameBuf);
+    name = StringUtils::ToLowercase(nameBuf);
+
+    assert(name.length() == sizeof(extaddr) * 2);
+
+    otbrLogDebug("Using instance name %s", name.c_str());
+    return name;
+}
+
+void TrelDnssd::PublishTrelService(void)
+{
+    assert(mRegisterInfo.IsValid());
+    assert(!mRegisterInfo.IsPublished());
+    assert(mTrelNetifIndex > 0);
+
+    mRegisterInfo.mInstanceName = GetTrelInstanceName();
+    mPublisher.PublishService(/* aHostName */ "", mRegisterInfo.mPort, mRegisterInfo.mInstanceName, kTrelServiceName,
+                              Mdns::Publisher::SubTypeList{}, mRegisterInfo.mTxtEntries);
+}
+
+void TrelDnssd::UnpublishTrelService(void)
+{
+    assert(mRegisterInfo.IsValid());
+    assert(mRegisterInfo.IsPublished());
+
+    mPublisher.UnpublishService(mRegisterInfo.mInstanceName, kTrelServiceName);
+    mRegisterInfo.mInstanceName = "";
+}
+
+void TrelDnssd::OnTrelServiceInstanceAdded(const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo)
+{
+    std::string        instanceName = StringUtils::ToLowercase(aInstanceInfo.mName);
+    otPlatTrelPeerInfo peerInfo;
+
+    // Remove any existing TREL service instance before adding
+    OnTrelServiceInstanceRemoved(instanceName);
+
+    otbrLogDebug("Peer discovered: %s hostname %s addresses %zu port %d priority %d "
+                 "weight %d",
+                 aInstanceInfo.mName.c_str(), aInstanceInfo.mHostName.c_str(), aInstanceInfo.mAddresses.size(),
+                 aInstanceInfo.mPort, aInstanceInfo.mPriority, aInstanceInfo.mWeight);
+
+    for (const auto &addr : aInstanceInfo.mAddresses)
+    {
+        otbrLogDebug("Peer address: %s", addr.ToString().c_str());
+    }
+
+    if (aInstanceInfo.mAddresses.empty())
+    {
+        otbrLogWarning("Peer %s does not have any IPv6 address, ignored", aInstanceInfo.mName.c_str());
+        ExitNow();
+    }
+
+    peerInfo.mRemoved = false;
+    memcpy(&peerInfo.mSockAddr.mAddress, &aInstanceInfo.mAddresses[0], sizeof(peerInfo.mSockAddr.mAddress));
+    peerInfo.mSockAddr.mPort = aInstanceInfo.mPort;
+    peerInfo.mTxtData        = aInstanceInfo.mTxtData.data();
+    peerInfo.mTxtLength      = aInstanceInfo.mTxtData.size();
+
+    {
+        Peer peer(aInstanceInfo.mTxtData, peerInfo.mSockAddr);
+
+        VerifyOrExit(peer.mValid, otbrLogWarning("Peer %s is invalid", aInstanceInfo.mName.c_str()));
+
+        otPlatTrelHandleDiscoveredPeerInfo(mNcp.GetInstance(), &peerInfo);
+
+        mPeers.emplace(instanceName, peer);
+        CheckPeersNumLimit();
+    }
+
+exit:
+    return;
+}
+
+void TrelDnssd::OnTrelServiceInstanceRemoved(const std::string &aInstanceName)
+{
+    std::string instanceName = StringUtils::ToLowercase(aInstanceName);
+    auto        it           = mPeers.find(instanceName);
+
+    VerifyOrExit(it != mPeers.end());
+
+    otbrLogDebug("Peer removed: %s", instanceName.c_str());
+
+    // Remove the peer only when all instances are removed because one peer can have multiple instances if expired
+    // instances were not properly removed by mDNS.
+    if (CountDuplicatePeers(it->second) == 0)
+    {
+        NotifyRemovePeer(it->second);
+    }
+
+    mPeers.erase(it);
+
+exit:
+    return;
+}
+
+void TrelDnssd::CheckPeersNumLimit(void)
+{
+    const PeerMap::value_type *oldestPeer = nullptr;
+
+    VerifyOrExit(mPeers.size() >= kPeerCacheSize);
+
+    for (const auto &entry : mPeers)
+    {
+        if (oldestPeer == nullptr || entry.second.mDiscoverTime < oldestPeer->second.mDiscoverTime)
+        {
+            oldestPeer = &entry;
+        }
+    }
+
+    OnTrelServiceInstanceRemoved(oldestPeer->first);
+
+exit:
+    return;
+}
+
+void TrelDnssd::NotifyRemovePeer(const Peer &aPeer)
+{
+    otPlatTrelPeerInfo peerInfo;
+
+    peerInfo.mRemoved   = true;
+    peerInfo.mTxtData   = aPeer.mTxtData.data();
+    peerInfo.mTxtLength = aPeer.mTxtData.size();
+    peerInfo.mSockAddr  = aPeer.mSockAddr;
+
+    otPlatTrelHandleDiscoveredPeerInfo(mNcp.GetInstance(), &peerInfo);
+}
+
+void TrelDnssd::RemoveAllPeers(void)
+{
+    for (const auto &entry : mPeers)
+    {
+        NotifyRemovePeer(entry.second);
+    }
+
+    mPeers.clear();
+}
+
+void TrelDnssd::CheckTrelNetifReady(void)
+{
+    assert(IsInitialized());
+
+    if (mTrelNetifIndex == 0)
+    {
+        mTrelNetifIndex = if_nametoindex(mTrelNetif.c_str());
+
+        if (mTrelNetifIndex != 0)
+        {
+            otbrLogDebug("Netif %s is ready: index = %d", mTrelNetif.c_str(), mTrelNetifIndex);
+            OnBecomeReady();
+        }
+        else
+        {
+            uint16_t delay = kCheckNetifReadyIntervalMs;
+
+            otbrLogWarning("Netif %s is not ready (%s), will retry after %d seconds", mTrelNetif.c_str(),
+                           strerror(errno), delay / 1000);
+            mTaskRunner.Post(Milliseconds(delay), [this]() { CheckTrelNetifReady(); });
+        }
+    }
+}
+
+bool TrelDnssd::IsReady(void) const
+{
+    assert(IsInitialized());
+
+    return mTrelNetifIndex > 0 && mMdnsPublisherReady;
+}
+
+void TrelDnssd::OnBecomeReady(void)
+{
+    if (IsReady())
+    {
+        otbrLogInfo("TREL DNS-SD Is Now Ready: Netif=%s(%u), SubscriberId=%u, Register=%s!", mTrelNetif.c_str(),
+                    mTrelNetifIndex, mSubscriberId, mRegisterInfo.mInstanceName.c_str());
+
+        if (mSubscriberId > 0)
+        {
+            mPublisher.SubscribeService(kTrelServiceName, /* aInstanceName */ "");
+        }
+
+        if (mRegisterInfo.IsValid())
+        {
+            PublishTrelService();
+        }
+    }
+}
+
+uint16_t TrelDnssd::CountDuplicatePeers(const TrelDnssd::Peer &aPeer)
+{
+    uint16_t count = 0;
+
+    for (const auto &entry : mPeers)
+    {
+        if (&entry.second == &aPeer)
+        {
+            continue;
+        }
+
+        if (!memcmp(&entry.second.mSockAddr, &aPeer.mSockAddr, sizeof(otSockAddr)) &&
+            !memcmp(&entry.second.mExtAddr, &aPeer.mExtAddr, sizeof(otExtAddress)))
+        {
+            count++;
+        }
+    }
+
+    return count;
+}
+
+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);
+}
+
+void TrelDnssd::RegisterInfo::Clear(void)
+{
+    assert(!IsPublished());
+
+    mPort = 0;
+    mTxtEntries.clear();
+}
+
+const char TrelDnssd::Peer::kTxtRecordExtAddressKey[] = "xa";
+
+void TrelDnssd::Peer::ReadExtAddrFromTxtData(void)
+{
+    std::vector<Mdns::Publisher::TxtEntry> txtEntries;
+
+    memset(&mExtAddr, 0, sizeof(mExtAddr));
+
+    SuccessOrExit(Mdns::Publisher::DecodeTxtData(txtEntries, mTxtData.data(), mTxtData.size()));
+
+    for (const auto &txtEntry : txtEntries)
+    {
+        if (StringUtils::EqualCaseInsensitive(txtEntry.mName, kTxtRecordExtAddressKey))
+        {
+            char extAddrHexBuf[sizeof(mExtAddr) * 2 + 1];
+
+            VerifyOrExit(txtEntry.mValue.size() == sizeof(mExtAddr) * 2);
+
+            memcpy(extAddrHexBuf, txtEntry.mValue.data(), sizeof(mExtAddr) * 2);
+            extAddrHexBuf[sizeof(mExtAddr) * 2] = '\0';
+
+            VerifyOrExit(Utils::Hex2Bytes(extAddrHexBuf, mExtAddr.m8, sizeof(mExtAddr)) == sizeof(mExtAddr));
+            mValid = true;
+            break;
+        }
+    }
+
+exit:
+    return;
+}
+
+} // namespace TrelDnssd
+
+} // namespace otbr
+
+#endif // OTBR_ENABLE_TREL
diff --git a/src/trel_dnssd/trel_dnssd.hpp b/src/trel_dnssd/trel_dnssd.hpp
new file mode 100644
index 0000000..46c8f10
--- /dev/null
+++ b/src/trel_dnssd/trel_dnssd.hpp
@@ -0,0 +1,194 @@
+/*
+ *    Copyright (c) 2021, 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 TREL DNS-SD over mDNS.
+ */
+
+#ifndef OTBR_AGENT_TREL_DNSSD_HPP_
+#define OTBR_AGENT_TREL_DNSSD_HPP_
+
+#if OTBR_ENABLE_TREL
+
+#include <assert.h>
+#include <utility>
+
+#include <openthread/instance.h>
+
+#include "common/types.hpp"
+#include "mdns/mdns.hpp"
+#include "ncp/ncp_openthread.hpp"
+
+namespace otbr {
+
+namespace TrelDnssd {
+
+/**
+ * @addtogroup border-router-trel-dnssd
+ *
+ * @brief
+ *   This module includes definition for TREL DNS-SD over mDNS.
+ *
+ * @{
+ */
+
+class TrelDnssd
+{
+public:
+    /**
+     * This constructor initializes the TrelDnssd instance.
+     *
+     * @param[in] aNcp        A reference to the OpenThread Controller instance.
+     * @param[in] aPublisher  A reference to the mDNS Publisher.
+     *
+     */
+    explicit TrelDnssd(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher);
+
+    /**
+     * This method initializes the TrelDnssd instance.
+     *
+     * @param[in] aTrelNetif  The network interface for discovering TREL peers.
+     *
+     */
+    void Initialize(std::string aTrelNetif);
+
+    /**
+     * This method starts browsing for TREL peers.
+     *
+     */
+    void StartBrowse(void);
+
+    /**
+     * This method stops browsing for TREL peers.
+     *
+     */
+    void StopBrowse(void);
+
+    /**
+     * This method registers the TREL service to DNS-SD.
+     *
+     * @param[in] aPort         The UDP port of TREL service.
+     * @param[in] aTxtData      The TXT data of TREL service.
+     * @param[in] aTxtLength    The TXT length of TREL service.
+     *
+     */
+    void RegisterService(uint16_t aPort, const uint8_t *aTxtData, uint8_t aTxtLength);
+
+    /**
+     * This method removes the TREL service from DNS-SD.
+     *
+     */
+    void UnregisterService(void);
+
+    /**
+     * This method notifies that mDNS Publisher is ready.
+     *
+     */
+    void OnMdnsPublisherReady(void);
+
+private:
+    static constexpr size_t   kPeerCacheSize             = 256;
+    static constexpr uint16_t kCheckNetifReadyIntervalMs = 5000;
+
+    struct RegisterInfo
+    {
+        uint16_t                               mPort = 0;
+        std::vector<Mdns::Publisher::TxtEntry> mTxtEntries;
+        std::string                            mInstanceName;
+
+        bool IsValid(void) const { return mPort > 0; }
+        bool IsPublished(void) const { return !mInstanceName.empty(); }
+        void Assign(uint16_t aPort, const uint8_t *aTxtData, uint8_t aTxtLength);
+        void Clear(void);
+    };
+
+    using Clock = std::chrono::system_clock;
+
+    struct Peer
+    {
+        static const char kTxtRecordExtAddressKey[];
+
+        explicit Peer(std::vector<uint8_t> aTxtData, const otSockAddr &aSockAddr)
+            : mDiscoverTime(Clock::now())
+            , mTxtData(std::move(aTxtData))
+            , mSockAddr(aSockAddr)
+        {
+            ReadExtAddrFromTxtData();
+        }
+
+        void ReadExtAddrFromTxtData(void);
+
+        Clock::time_point    mDiscoverTime;
+        std::vector<uint8_t> mTxtData;
+        otSockAddr           mSockAddr;
+        otExtAddress         mExtAddr;
+        bool                 mValid = false;
+    };
+
+    using PeerMap = std::map<std::string, Peer>;
+
+    bool        IsInitialized(void) const { return !mTrelNetif.empty(); }
+    bool        IsReady(void) const;
+    void        OnBecomeReady(void);
+    void        CheckTrelNetifReady(void);
+    std::string GetTrelInstanceName(void);
+    void        PublishTrelService(void);
+    void        UnpublishTrelService(void);
+    void        OnTrelServiceInstanceResolved(const std::string &                            aType,
+                                              const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo);
+    void        OnTrelServiceInstanceAdded(const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo);
+    void        OnTrelServiceInstanceRemoved(const std::string &aInstanceName);
+
+    void     NotifyRemovePeer(const Peer &aPeer);
+    void     CheckPeersNumLimit(void);
+    void     RemoveAllPeers(void);
+    uint16_t CountDuplicatePeers(const Peer &aPeer);
+
+    Mdns::Publisher &          mPublisher;
+    Ncp::ControllerOpenThread &mNcp;
+    TaskRunner                 mTaskRunner;
+    std::string                mTrelNetif;
+    uint32_t                   mTrelNetifIndex = 0;
+    uint64_t                   mSubscriberId   = 0;
+    RegisterInfo               mRegisterInfo;
+    PeerMap                    mPeers;
+    bool                       mMdnsPublisherReady = false;
+};
+
+/**
+ * @}
+ */
+
+} // namespace TrelDnssd
+
+} // namespace otbr
+
+#endif // OTBR_ENABLE_TREL
+
+#endif // OTBR_AGENT_TREL_DNSSD_HPP_
diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp
index dccfc50..b10475b 100644
--- a/tests/unit/test_logging.cpp
+++ b/tests/unit/test_logging.cpp
@@ -92,7 +92,7 @@
     sprintf(ident, "otbr-test-%ld", clock());
     otbrLogInit(ident, OTBR_LOG_DEBUG, true);
     const char s[] = "one super long string with lots of text";
-    otbrDump(OTBR_LOG_INFO, "foobar", s, sizeof(s));
+    otbrDump(OTBR_LOG_INFO, "Test", "foobar", s, sizeof(s));
     otbrLogDeinit();
     sleep(0);
 
diff --git a/third_party/openthread/CMakeLists.txt b/third_party/openthread/CMakeLists.txt
index 5f86336..82c444f 100644
--- a/third_party/openthread/CMakeLists.txt
+++ b/third_party/openthread/CMakeLists.txt
@@ -40,7 +40,7 @@
     set(OT_LINK_METRICS_SUBJECT ON CACHE BOOL "enable link metrics subject" FORCE)
 endif()
 set(OT_SLAAC ON CACHE BOOL "enable SLAAC" FORCE)
-set(OT_TREL ON CACHE BOOL "enable TREL")
+set(OT_TREL ${OTBR_TREL} CACHE BOOL "enable TREL" FORCE)
 
 if (NOT OT_LOG_LEVEL)
     if (CMAKE_BUILD_TYPE STREQUAL "Debug")