From ff4ea4e00123b38db74da8a162b02a11beca7d23 Mon Sep 17 00:00:00 2001
From: Li Cao <irvingcl@google.com>
Date: Thu, 5 Dec 2024 14:59:03 +0800
Subject: [PATCH] [ncp] integrate SRP Server and Advertising Proxy for NCP
 (#2624)

This commit integrates the mDNS Publisher with NcpHost to make
advertising proxy work under NCP mode.

The commit adds an expect test to verify the advertising proxy
function under NCP mode. To do the test, `avahi-browser` is used. So
it's added into the bootstrap of the CI for ncp_mode.

As an abstraction:
1. The commit adds `NcpSpinel::SrpServerSetEnabled` and
   `NcpSpinel::SrpServerSetAutoEnableMode` to control the SRP server
   on the NCP.
2. The commit adds the handling of `SPINEL_PROP_DNSSD_HOST`,
   `SPINEL_PROP_DNSSD_SERVICE` and `SPINEL_PROP_DNSSD_KEY_RECORD` to
   help NCP to publish the dnssd entries (by using the mDNSPublisher
   on the host).
---
 .github/workflows/ncp_mode.yml                |   4 +-
 src/agent/application.cpp                     |  10 +-
 src/agent/application.hpp                     |   1 +
 src/ncp/ncp_host.cpp                          |  23 +-
 src/ncp/ncp_host.hpp                          |  16 +-
 src/ncp/ncp_spinel.cpp                        | 265 +++++++++++++++++-
 src/ncp/ncp_spinel.hpp                        |  47 +++-
 tests/scripts/bootstrap.sh                    |   4 +
 .../scripts/expect/ncp_schedule_migration.exp |   0
 tests/scripts/expect/ncp_srp_server.exp       |  77 +++++
 tests/scripts/ncp_mode                        |   2 +
 11 files changed, 441 insertions(+), 8 deletions(-)
 mode change 100644 => 100755 tests/scripts/expect/ncp_schedule_migration.exp
 create mode 100755 tests/scripts/expect/ncp_srp_server.exp

diff --git a/.github/workflows/ncp_mode.yml b/.github/workflows/ncp_mode.yml
index 1584aaab63d..1c6926d40d8 100644
--- a/.github/workflows/ncp_mode.yml
+++ b/.github/workflows/ncp_mode.yml
@@ -53,7 +53,7 @@ jobs:
         OTBR_MDNS: ${{ matrix.mdns }}
         OTBR_COVERAGE: 1
         OTBR_VERBOSE: 1
-        OTBR_OPTIONS: "-DCMAKE_BUILD_TYPE=Debug -DOT_THREAD_VERSION=1.4 -DOTBR_COVERAGE=ON -DOTBR_DBUS=ON -DOTBR_FEATURE_FLAGS=ON -DOTBR_TELEMETRY_DATA_API=ON -DOTBR_UNSECURE_JOIN=ON -DOTBR_TREL=ON -DBUILD_TESTING=OFF"
+        OTBR_OPTIONS: "-DCMAKE_BUILD_TYPE=Debug -DOT_THREAD_VERSION=1.4 -DOTBR_COVERAGE=ON -DOTBR_DBUS=ON -DOTBR_FEATURE_FLAGS=ON -DOTBR_TELEMETRY_DATA_API=ON -DOTBR_UNSECURE_JOIN=ON -DOTBR_TREL=ON -DOTBR_SRP_ADVERTISING_PROXY=ON -DBUILD_TESTING=OFF"
     steps:
     - uses: actions/checkout@v4
       with:
@@ -76,6 +76,6 @@ jobs:
             --build-arg OTBR_OPTIONS="${OTBR_OPTIONS}"
     - name: Run
       run: |
-       top_builddir="./build/temp" tests/scripts/ncp_mode build_ot_sim expect
+        top_builddir="./build/temp" tests/scripts/ncp_mode build_ot_sim expect
     - name: Codecov
       uses: codecov/codecov-action@v5
diff --git a/src/agent/application.cpp b/src/agent/application.cpp
index fb6cee2cb46..690fa37cbdb 100644
--- a/src/agent/application.cpp
+++ b/src/agent/application.cpp
@@ -288,6 +288,12 @@ void Application::DeinitRcpMode(void)
 
 void Application::InitNcpMode(void)
 {
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    otbr::Ncp::NcpHost &ncpHost = static_cast<otbr::Ncp::NcpHost &>(mHost);
+    ncpHost.SetMdnsPublisher(mPublisher.get());
+    mMdnsStateSubject.AddObserver(ncpHost);
+    mPublisher->Start();
+#endif
 #if OTBR_ENABLE_DBUS_SERVER
     mDBusAgent->Init(*mBorderAgent);
 #endif
@@ -295,7 +301,9 @@ void Application::InitNcpMode(void)
 
 void Application::DeinitNcpMode(void)
 {
-    /* empty */
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    mPublisher->Stop();
+#endif
 }
 
 } // namespace otbr
diff --git a/src/agent/application.hpp b/src/agent/application.hpp
index 9ae141ba5d3..1c41efb5067 100644
--- a/src/agent/application.hpp
+++ b/src/agent/application.hpp
@@ -44,6 +44,7 @@
 #if OTBR_ENABLE_BORDER_AGENT
 #include "border_agent/border_agent.hpp"
 #endif
+#include "ncp/ncp_host.hpp"
 #include "ncp/rcp_host.hpp"
 #if OTBR_ENABLE_BACKBONE_ROUTER
 #include "backbone_router/backbone_agent.hpp"
diff --git a/src/ncp/ncp_host.cpp b/src/ncp/ncp_host.cpp
index 347de367553..568cc54585f 100644
--- a/src/ncp/ncp_host.cpp
+++ b/src/ncp/ncp_host.cpp
@@ -38,7 +38,6 @@
 #include <openthread/openthread-system.h>
 
 #include "lib/spinel/spinel_driver.hpp"
-
 #include "ncp/async_task.hpp"
 
 namespace otbr {
@@ -134,6 +133,16 @@ void NcpHost::Init(void)
     {
         mInfraIf.SetInfraIf(mConfig.mBackboneInterfaceName);
     }
+
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+#if OTBR_ENABLE_SRP_SERVER_AUTO_ENABLE_MODE
+    // Let SRP server use auto-enable mode. The auto-enable mode delegates the control of SRP server to the Border
+    // Routing Manager. SRP server automatically starts when bi-directional connectivity is ready.
+    mNcpSpinel.SrpServerSetAutoEnableMode(/* aEnabled */ true);
+#else
+    mNcpSpinel.SrpServerSetEnabled(/* aEnabled */ true);
+#endif
+#endif
 }
 
 void NcpHost::Deinit(void)
@@ -264,5 +273,17 @@ void NcpHost::Update(MainloopContext &aMainloop)
     mNetif.UpdateFdSet(&aMainloop);
 }
 
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+void NcpHost::SetMdnsPublisher(Mdns::Publisher *aPublisher)
+{
+    mNcpSpinel.SetMdnsPublisher(aPublisher);
+}
+
+void NcpHost::HandleMdnsState(Mdns::Publisher::State aState)
+{
+    mNcpSpinel.DnssdSetState(aState);
+}
+#endif
+
 } // namespace Ncp
 } // namespace otbr
diff --git a/src/ncp/ncp_host.hpp b/src/ncp/ncp_host.hpp
index 3e68a524b59..8fec83aa2a0 100644
--- a/src/ncp/ncp_host.hpp
+++ b/src/ncp/ncp_host.hpp
@@ -72,7 +72,13 @@ class NcpNetworkProperties : virtual public NetworkProperties, public PropsObser
     otOperationalDatasetTlvs mDatasetActiveTlvs;
 };
 
-class NcpHost : public MainloopProcessor, public ThreadHost, public NcpNetworkProperties
+class NcpHost : public MainloopProcessor,
+                public ThreadHost,
+                public NcpNetworkProperties
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    ,
+                public Mdns::StateObserver
+#endif
 {
 public:
     /**
@@ -119,7 +125,15 @@ class NcpHost : public MainloopProcessor, public ThreadHost, public NcpNetworkPr
     void Update(MainloopContext &aMainloop) override;
     void Process(const MainloopContext &aMainloop) override;
 
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    void SetMdnsPublisher(Mdns::Publisher *aPublisher);
+#endif
+
 private:
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    void HandleMdnsState(Mdns::Publisher::State aState) override;
+#endif
+
     ot::Spinel::SpinelDriver &mSpinelDriver;
     otPlatformConfig          mConfig;
     NcpSpinel                 mNcpSpinel;
diff --git a/src/ncp/ncp_spinel.cpp b/src/ncp/ncp_spinel.cpp
index ed40d54d823..6b29350e861 100644
--- a/src/ncp/ncp_spinel.cpp
+++ b/src/ncp/ncp_spinel.cpp
@@ -36,6 +36,7 @@
 
 #include <openthread/dataset.h>
 #include <openthread/thread.h>
+#include <openthread/platform/dnssd.h>
 
 #include "common/code_utils.hpp"
 #include "common/logging.hpp"
@@ -44,6 +45,7 @@
 #include "lib/spinel/spinel_driver.hpp"
 #include "lib/spinel/spinel_encoder.hpp"
 #include "lib/spinel/spinel_helper.hpp"
+#include "lib/spinel/spinel_prop_codec.hpp"
 
 namespace otbr {
 namespace Ncp {
@@ -58,6 +60,9 @@ NcpSpinel::NcpSpinel(void)
     , mEncoder(mNcpBuffer)
     , mIid(SPINEL_HEADER_INVALID_IID)
     , mPropsObserver(nullptr)
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    , mPublisher(nullptr)
+#endif
 {
     std::fill_n(mWaitingKeyTable, SPINEL_PROP_LAST_STATUS, sizeof(mWaitingKeyTable));
     memset(mCmdTable, 0, sizeof(mCmdTable));
@@ -76,6 +81,9 @@ void NcpSpinel::Deinit(void)
     mSpinelDriver              = nullptr;
     mIp6AddressTableCallback   = nullptr;
     mNetifStateChangedCallback = nullptr;
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    mPublisher = nullptr;
+#endif
 }
 
 otbrError NcpSpinel::SpinelDataUnpack(const uint8_t *aDataIn, spinel_size_t aDataLen, const char *aPackFormat, ...)
@@ -224,6 +232,45 @@ void NcpSpinel::ThreadErasePersistentInfo(AsyncTaskPtr aAsyncTask)
     }
 }
 
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+void NcpSpinel::SrpServerSetAutoEnableMode(bool aEnabled)
+{
+    otError      error;
+    EncodingFunc encodingFunc = [aEnabled](ot::Spinel::Encoder &aEncoder) { return aEncoder.WriteBool(aEnabled); };
+
+    error = SetProperty(SPINEL_PROP_SRP_SERVER_AUTO_ENABLE_MODE, encodingFunc);
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogWarning("Failed to call SrpServerSetAutoEnableMode, %s", otThreadErrorToString(error));
+    }
+}
+
+void NcpSpinel::SrpServerSetEnabled(bool aEnabled)
+{
+    otError      error;
+    EncodingFunc encodingFunc = [aEnabled](ot::Spinel::Encoder &aEncoder) { return aEncoder.WriteBool(aEnabled); };
+
+    error = SetProperty(SPINEL_PROP_SRP_SERVER_ENABLED, encodingFunc);
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogWarning("Failed to call SrpServerSetEnabled, %s", otThreadErrorToString(error));
+    }
+}
+
+void NcpSpinel::DnssdSetState(Mdns::Publisher::State aState)
+{
+    otError          error;
+    otPlatDnssdState state = (aState == Mdns::Publisher::State::kReady) ? OT_PLAT_DNSSD_READY : OT_PLAT_DNSSD_STOPPED;
+    EncodingFunc     encodingFunc = [state](ot::Spinel::Encoder &aEncoder) { return aEncoder.WriteUint8(state); };
+
+    error = SetProperty(SPINEL_PROP_DNSSD_STATE, encodingFunc);
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogWarning("Failed to call DnssdSetState, %s", otThreadErrorToString(error));
+    }
+}
+#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY
+
 void NcpSpinel::HandleReceivedFrame(const uint8_t *aFrame,
                                     uint16_t       aLength,
                                     uint8_t        aHeader,
@@ -272,8 +319,19 @@ void NcpSpinel::HandleNotification(const uint8_t *aFrame, uint16_t aLength)
 
     SuccessOrExit(error = SpinelDataUnpack(aFrame, aLength, kSpinelDataUnpackFormat, &header, &cmd, &key, &data, &len));
     VerifyOrExit(SPINEL_HEADER_GET_TID(header) == 0, error = OTBR_ERROR_PARSE);
-    VerifyOrExit(cmd == SPINEL_CMD_PROP_VALUE_IS);
-    HandleValueIs(key, data, static_cast<uint16_t>(len));
+
+    switch (cmd)
+    {
+    case SPINEL_CMD_PROP_VALUE_IS:
+        HandleValueIs(key, data, static_cast<uint16_t>(len));
+        break;
+    case SPINEL_CMD_PROP_VALUE_INSERTED:
+        HandleValueInserted(key, data, static_cast<uint16_t>(len));
+        break;
+    case SPINEL_CMD_PROP_VALUE_REMOVED:
+        HandleValueRemoved(key, data, static_cast<uint16_t>(len));
+        break;
+    }
 
 exit:
     otbrLogResult(error, "%s", __FUNCTION__);
@@ -439,6 +497,184 @@ void NcpSpinel::HandleValueIs(spinel_prop_key_t aKey, const uint8_t *aBuffer, ui
     return;
 }
 
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+static std::string KeyNameFor(const otPlatDnssdKey &aKey)
+{
+    std::string name(aKey.mName);
+
+    if (aKey.mServiceType != nullptr)
+    {
+        // TODO: current code would not work with service instance labels that include a '.'
+        name += ".";
+        name += aKey.mServiceType;
+    }
+    return name;
+}
+#endif
+
+void NcpSpinel::HandleValueInserted(spinel_prop_key_t aKey, const uint8_t *aBuffer, uint16_t aLength)
+{
+    otbrError           error = OTBR_ERROR_NONE;
+    ot::Spinel::Decoder decoder;
+
+    VerifyOrExit(aBuffer != nullptr, error = OTBR_ERROR_INVALID_ARGS);
+    decoder.Init(aBuffer, aLength);
+
+    switch (aKey)
+    {
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    case SPINEL_PROP_DNSSD_HOST:
+    {
+        Mdns::Publisher::AddressList addressList;
+        otPlatDnssdHost              host;
+        otPlatDnssdRequestId         requestId;
+        const uint8_t               *callbackData;
+        uint16_t                     callbackDataSize;
+        std::vector<uint8_t>         callbackDataCopy;
+
+        SuccessOrExit(ot::Spinel::DecodeDnssdHost(decoder, host, requestId, callbackData, callbackDataSize));
+        for (uint16_t i = 0; i < host.mAddressesLength; i++)
+        {
+            addressList.push_back(Ip6Address(host.mAddresses[i].mFields.m8));
+        }
+        callbackDataCopy.assign(callbackData, callbackData + callbackDataSize);
+
+        mPublisher->PublishHost(host.mHostName, addressList, [this, requestId, callbackDataCopy](otbrError aError) {
+            OT_UNUSED_VARIABLE(SendDnssdResult(requestId, callbackDataCopy, OtbrErrorToOtError(aError)));
+        });
+        break;
+    }
+    case SPINEL_PROP_DNSSD_SERVICE:
+    {
+        otPlatDnssdService           service;
+        Mdns::Publisher::SubTypeList subTypeList;
+        const char                  *subTypeArray[kMaxSubTypes];
+        uint16_t                     subTypeCount;
+        Mdns::Publisher::TxtData     txtData;
+        otPlatDnssdRequestId         requestId;
+        const uint8_t               *callbackData;
+        uint16_t                     callbackDataSize;
+        std::vector<uint8_t>         callbackDataCopy;
+
+        SuccessOrExit(ot::Spinel::DecodeDnssdService(decoder, service, subTypeArray, subTypeCount, requestId,
+                                                     callbackData, callbackDataSize));
+        for (uint16_t i = 0; i < subTypeCount; i++)
+        {
+            subTypeList.push_back(subTypeArray[i]);
+        }
+        txtData.assign(service.mTxtData, service.mTxtData + service.mTxtDataLength);
+        callbackDataCopy.assign(callbackData, callbackData + callbackDataSize);
+
+        mPublisher->PublishService(service.mHostName, service.mServiceInstance, service.mServiceType, subTypeList,
+                                   service.mPort, txtData, [this, requestId, callbackDataCopy](otbrError aError) {
+                                       OT_UNUSED_VARIABLE(
+                                           SendDnssdResult(requestId, callbackDataCopy, OtbrErrorToOtError(aError)));
+                                   });
+        break;
+    }
+    case SPINEL_PROP_DNSSD_KEY_RECORD:
+    {
+        otPlatDnssdKey           key;
+        Mdns::Publisher::KeyData keyData;
+        otPlatDnssdRequestId     requestId;
+        const uint8_t           *callbackData;
+        uint16_t                 callbackDataSize;
+        std::vector<uint8_t>     callbackDataCopy;
+
+        SuccessOrExit(ot::Spinel::DecodeDnssdKey(decoder, key, requestId, callbackData, callbackDataSize));
+        keyData.assign(key.mKeyData, key.mKeyData + key.mKeyDataLength);
+        callbackDataCopy.assign(callbackData, callbackData + callbackDataSize);
+
+        mPublisher->PublishKey(KeyNameFor(key), keyData, [this, requestId, callbackDataCopy](otbrError aError) {
+            OT_UNUSED_VARIABLE(SendDnssdResult(requestId, callbackDataCopy, OtbrErrorToOtError(aError)));
+        });
+        break;
+    }
+#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    default:
+        error = OTBR_ERROR_DROPPED;
+        break;
+    }
+
+exit:
+    otbrLogResult(error, "HandleValueInserted, key:%u", aKey);
+    return;
+}
+
+void NcpSpinel::HandleValueRemoved(spinel_prop_key_t aKey, const uint8_t *aBuffer, uint16_t aLength)
+{
+    otbrError           error = OTBR_ERROR_NONE;
+    ot::Spinel::Decoder decoder;
+
+    VerifyOrExit(aBuffer != nullptr, error = OTBR_ERROR_INVALID_ARGS);
+    decoder.Init(aBuffer, aLength);
+
+    switch (aKey)
+    {
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    case SPINEL_PROP_DNSSD_HOST:
+    {
+        otPlatDnssdHost      host;
+        otPlatDnssdRequestId requestId;
+        const uint8_t       *callbackData;
+        uint16_t             callbackDataSize;
+        std::vector<uint8_t> callbackDataCopy;
+
+        SuccessOrExit(ot::Spinel::DecodeDnssdHost(decoder, host, requestId, callbackData, callbackDataSize));
+        callbackDataCopy.assign(callbackData, callbackData + callbackDataSize);
+
+        mPublisher->UnpublishHost(host.mHostName, [this, requestId, callbackDataCopy](otbrError aError) {
+            OT_UNUSED_VARIABLE(SendDnssdResult(requestId, callbackDataCopy, OtbrErrorToOtError(aError)));
+        });
+        break;
+    }
+    case SPINEL_PROP_DNSSD_SERVICE:
+    {
+        otPlatDnssdService   service;
+        const char          *subTypeArray[kMaxSubTypes];
+        uint16_t             subTypeCount;
+        otPlatDnssdRequestId requestId;
+        const uint8_t       *callbackData;
+        uint16_t             callbackDataSize;
+        std::vector<uint8_t> callbackDataCopy;
+
+        SuccessOrExit(ot::Spinel::DecodeDnssdService(decoder, service, subTypeArray, subTypeCount, requestId,
+                                                     callbackData, callbackDataSize));
+        callbackDataCopy.assign(callbackData, callbackData + callbackDataSize);
+
+        mPublisher->UnpublishService(
+            service.mHostName, service.mServiceType, [this, requestId, callbackDataCopy](otbrError aError) {
+                OT_UNUSED_VARIABLE(SendDnssdResult(requestId, callbackDataCopy, OtbrErrorToOtError(aError)));
+            });
+        break;
+    }
+    case SPINEL_PROP_DNSSD_KEY_RECORD:
+    {
+        otPlatDnssdKey       key;
+        otPlatDnssdRequestId requestId;
+        const uint8_t       *callbackData;
+        uint16_t             callbackDataSize;
+        std::vector<uint8_t> callbackDataCopy;
+
+        SuccessOrExit(ot::Spinel::DecodeDnssdKey(decoder, key, requestId, callbackData, callbackDataSize));
+        callbackDataCopy.assign(callbackData, callbackData + callbackDataSize);
+
+        mPublisher->UnpublishKey(KeyNameFor(key), [this, requestId, callbackDataCopy](otbrError aError) {
+            OT_UNUSED_VARIABLE(SendDnssdResult(requestId, callbackDataCopy, OtbrErrorToOtError(aError)));
+        });
+        break;
+    }
+#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    default:
+        error = OTBR_ERROR_DROPPED;
+        break;
+    }
+
+exit:
+    otbrLogResult(error, "HandleValueRemoved, key:%u", aKey);
+    return;
+}
+
 otbrError NcpSpinel::HandleResponseForPropSet(spinel_tid_t      aTid,
                                               spinel_prop_key_t aKey,
                                               const uint8_t    *aData,
@@ -801,6 +1037,31 @@ otError NcpSpinel::ParseInfraIfIcmp6Nd(const uint8_t       *aBuf,
     return error;
 }
 
+otError NcpSpinel::SendDnssdResult(otPlatDnssdRequestId        aRequestId,
+                                   const std::vector<uint8_t> &aCallbackData,
+                                   otError                     aError)
+{
+    otError      error;
+    EncodingFunc encodingFunc = [aRequestId, &aCallbackData, aError](ot::Spinel::Encoder &aEncoder) {
+        otError error = OT_ERROR_NONE;
+
+        SuccessOrExit(aEncoder.WriteUint8(aError));
+        SuccessOrExit(aEncoder.WriteUint32(aRequestId));
+        SuccessOrExit(aEncoder.WriteData(aCallbackData.data(), aCallbackData.size()));
+
+    exit:
+        return error;
+    };
+
+    error = SetProperty(SPINEL_PROP_DNSSD_REQUEST_RESULT, encodingFunc);
+    if (error != OT_ERROR_NONE)
+    {
+        otbrLogWarning("Failed to SendDnssdResult, %s", otThreadErrorToString(error));
+    }
+
+    return error;
+}
+
 otbrError NcpSpinel::SetInfraIf(uint32_t aInfraIfIndex, bool aIsRunning, const std::vector<Ip6Address> &aIp6Addresses)
 {
     otbrError    error        = OTBR_ERROR_NONE;
diff --git a/src/ncp/ncp_spinel.hpp b/src/ncp/ncp_spinel.hpp
index bb79274413b..0b4ea9bbe7d 100644
--- a/src/ncp/ncp_spinel.hpp
+++ b/src/ncp/ncp_spinel.hpp
@@ -37,10 +37,13 @@
 #include <functional>
 #include <memory>
 
+#include <vector>
+
 #include <openthread/dataset.h>
 #include <openthread/error.h>
 #include <openthread/link.h>
 #include <openthread/thread.h>
+#include <openthread/platform/dnssd.h>
 
 #include "lib/spinel/spinel.h"
 #include "lib/spinel/spinel_buffer.hpp"
@@ -49,6 +52,7 @@
 
 #include "common/task_runner.hpp"
 #include "common/types.hpp"
+#include "mdns/mdns.hpp"
 #include "ncp/async_task.hpp"
 #include "ncp/posix/infra_if.hpp"
 #include "ncp/posix/netif.hpp"
@@ -245,10 +249,45 @@ class NcpSpinel : public Netif::Dependencies, public InfraIf::Dependencies
         mInfraIfIcmp6NdCallback = aCallback;
     }
 
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    /**
+     * This method enables/disables the SRP Server on NCP.
+     *
+     * @param[in] aEnable  A boolean to enable/disable the SRP server.
+     */
+    void SrpServerSetEnabled(bool aEnabled);
+
+    /**
+     * This method enables/disables the auto-enable mode on SRP Server on NCP.
+     *
+     * @param[in] aEnable  A boolean to enable/disable the SRP server.
+     */
+    void SrpServerSetAutoEnableMode(bool aEnabled);
+
+    /**
+     * This method sets the dnssd state on NCP.
+     *
+     * @param[in] aState  The dnssd state.
+     */
+    void DnssdSetState(Mdns::Publisher::State aState);
+
+    /**
+     * This method sets the mDNS Publisher object.
+     *
+     * @param[in] aPublisher  A pointer to the mDNS Publisher object.
+     */
+    void SetMdnsPublisher(otbr::Mdns::Publisher *aPublisher)
+    {
+        mPublisher = aPublisher;
+    }
+#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY
+
 private:
     using FailureHandler = std::function<void(otError)>;
 
-    static constexpr uint8_t kMaxTids = 16;
+    static constexpr uint8_t  kMaxTids             = 16;
+    static constexpr uint16_t kCallbackDataMaxSize = sizeof(uint64_t); // Maximum size of a function pointer.
+    static constexpr uint16_t kMaxSubTypes         = 8;                // Maximum number of sub types in a MDNS service.
 
     template <typename Function, typename... Args> static void SafeInvoke(Function &aFunc, Args &&...aArgs)
     {
@@ -282,6 +321,8 @@ class NcpSpinel : public Netif::Dependencies, public InfraIf::Dependencies
     void      HandleNotification(const uint8_t *aFrame, uint16_t aLength);
     void      HandleResponse(spinel_tid_t aTid, const uint8_t *aFrame, uint16_t aLength);
     void      HandleValueIs(spinel_prop_key_t aKey, const uint8_t *aBuffer, uint16_t aLength);
+    void      HandleValueInserted(spinel_prop_key_t aKey, const uint8_t *aBuffer, uint16_t aLength);
+    void      HandleValueRemoved(spinel_prop_key_t aKey, const uint8_t *aBuffer, uint16_t aLength);
     otbrError HandleResponseForPropSet(spinel_tid_t      aTid,
                                        spinel_prop_key_t aKey,
                                        const uint8_t    *aData,
@@ -320,6 +361,7 @@ class NcpSpinel : public Netif::Dependencies, public InfraIf::Dependencies
                                 const otIp6Address *&aAddr,
                                 const uint8_t      *&aData,
                                 uint16_t            &aDataLen);
+    otError SendDnssdResult(otPlatDnssdRequestId aRequestId, const std::vector<uint8_t> &aCallbackData, otError aError);
 
     otbrError SetInfraIf(uint32_t                       aInfraIfIndex,
                          bool                           aIsRunning,
@@ -346,6 +388,9 @@ class NcpSpinel : public Netif::Dependencies, public InfraIf::Dependencies
     TaskRunner mTaskRunner;
 
     PropsObserver *mPropsObserver;
+#if OTBR_ENABLE_SRP_ADVERTISING_PROXY
+    otbr::Mdns::Publisher *mPublisher;
+#endif
 
     AsyncTaskPtr mDatasetSetActiveTask;
     AsyncTaskPtr mDatasetMgmtSetPendingTask;
diff --git a/tests/scripts/bootstrap.sh b/tests/scripts/bootstrap.sh
index 6d32cea0a77..99e4b969b20 100755
--- a/tests/scripts/bootstrap.sh
+++ b/tests/scripts/bootstrap.sh
@@ -118,6 +118,10 @@ case "$(uname)" in
             configure_network
         fi
 
+        if [ "$BUILD_TARGET" == ncp_mode ]; then
+            sudo apt-get install --no-install-recommends -y avahi-daemon avahi-utils
+        fi
+
         if [ "$BUILD_TARGET" == scan-build ]; then
             pip3 install -U cmake
             sudo apt-get install --no-install-recommends -y clang clang-tools
diff --git a/tests/scripts/expect/ncp_schedule_migration.exp b/tests/scripts/expect/ncp_schedule_migration.exp
old mode 100644
new mode 100755
diff --git a/tests/scripts/expect/ncp_srp_server.exp b/tests/scripts/expect/ncp_srp_server.exp
new file mode 100755
index 00000000000..b7d6ab24529
--- /dev/null
+++ b/tests/scripts/expect/ncp_srp_server.exp
@@ -0,0 +1,77 @@
+#!/usr/bin/expect -f
+#
+#  Copyright (c) 2024, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+#
+source "tests/scripts/expect/_common.exp"
+
+set ptys [create_socat 1]
+set pty1 [lindex $ptys 0]
+set pty2 [lindex $ptys 1]
+set container "otbr-ncp"
+
+set dataset "0e080000000000010000000300001435060004001fffe002087d61eb42cdc48d6a0708fd0d07fca1b9f0500510ba088fc2bd6c3b3897f7a10f58263ff3030f4f70656e5468726561642d353234660102524f04109dc023ccd447b12b50997ef68020f19e0c0402a0f7f8"
+set dataset_dbus "0x0e,0x08,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x03,0x00,0x00,0x14,0x35,0x06,0x00,0x04,0x00,0x1f,0xff,0xe0,0x02,0x08,0x7d,0x61,0xeb,0x42,0xcd,0xc4,0x8d,0x6a,0x07,0x08,0xfd,0x0d,0x07,0xfc,0xa1,0xb9,0xf0,0x50,0x05,0x10,0xba,0x08,0x8f,0xc2,0xbd,0x6c,0x3b,0x38,0x97,0xf7,0xa1,0x0f,0x58,0x26,0x3f,0xf3,0x03,0x0f,0x4f,0x70,0x65,0x6e,0x54,0x68,0x72,0x65,0x61,0x64,0x2d,0x35,0x32,0x34,0x66,0x01,0x02,0x52,0x4f,0x04,0x10,0x9d,0xc0,0x23,0xcc,0xd4,0x47,0xb1,0x2b,0x50,0x99,0x7e,0xf6,0x80,0x20,0xf1,0x9e,0x0c,0x04,0x02,0xa0,0xf7,0xf8"
+
+start_otbr_docker $container $::env(EXP_OT_NCP_PATH) 2 $pty1 $pty2
+spawn_node 3 otbr-docker $container
+sleep 5
+
+send "dbus-send --system --dest=io.openthread.BorderRouter.wpan0 --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 io.openthread.BorderRouter.Join \"array:byte:${dataset_dbus}\"\n"
+expect "app#"
+sleep 20
+
+spawn_node 4 cli $::env(EXP_OT_CLI_PATH)
+send "dataset set active ${dataset}\n"
+expect_line "Done"
+send "mode rn\r\n"
+expect_line "Done"
+send "ifconfig up\r\n"
+expect_line "Done"
+send "thread start\r\n"
+expect_line "Done"
+wait_for "state" "child"
+set omr_addr [get_omr_addr]
+sleep 1
+
+send "srp client autostart enable\r\n"
+expect_line "Done"
+send "srp client host name otbr-ncp-test\r\n"
+expect_line "Done"
+send "srp client host address $omr_addr\r\n"
+expect_line "Done"
+send "srp client service add ot-service _ipps._tcp 12345\r\n"
+expect_line "Done"
+sleep 1
+
+spawn avahi-browse -r _ipps._tcp
+expect backbone1
+send "\003"
+expect eof
+
+exec sudo docker stop $container
+exec sudo docker rm $container
+dispose_all
diff --git a/tests/scripts/ncp_mode b/tests/scripts/ncp_mode
index b555f3fdec9..2544bf5b397 100755
--- a/tests/scripts/ncp_mode
+++ b/tests/scripts/ncp_mode
@@ -136,6 +136,7 @@ do_build_ot_simulation()
     OT_CMAKE_BUILD_DIR=${ABS_TOP_OT_BUILDDIR}/ncp "${ABS_TOP_OT_SRCDIR}"/script/cmake-build simulation \
         -DOT_MTD=OFF -DOT_RCP=OFF -DOT_APP_CLI=OFF -DOT_APP_RCP=OFF \
         -DOT_BORDER_ROUTING=ON -DOT_NCP_INFRA_IF=ON -DOT_SIMULATION_INFRA_IF=OFF \
+        -DOT_SRP_SERVER=ON -DOT_SRP_ADV_PROXY=ON -DOT_PLATFORM_DNSSD=ON -DOT_SIMULATION_DNSSD=OFF -DOT_NCP_DNSSD=ON \
         -DBUILD_TESTING=OFF
     OT_CMAKE_BUILD_DIR=${ABS_TOP_OT_BUILDDIR}/cli "${ABS_TOP_OT_SRCDIR}"/script/cmake-build simulation \
         -DOT_MTD=OFF -DOT_RCP=OFF -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
@@ -152,6 +153,7 @@ do_build_otbr_docker()
         "-DOTBR_TELEMETRY_DATA_API=ON"
         "-DOTBR_TREL=ON"
         "-DOTBR_LINK_METRICS_TELEMETRY=ON"
+        "-DOTBR_SRP_ADVERTISING_PROXY=ON"
     )
     sudo docker build -t "${OTBR_DOCKER_IMAGE}" \
         -f ./etc/docker/Dockerfile . \