blob: ce46137c9a00e2edde46f2d9c40609cabb8c581b [file] [log] [blame]
/*
*
* Copyright (c) 2020-2022 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "Advertiser.h"
#include <inttypes.h>
#include <stdio.h>
#include "MinimalMdnsServer.h"
#include "ServiceNaming.h"
#include <app/icd/server/ICDServerConfig.h>
#include <crypto/RandUtils.h>
#include <lib/dnssd/Advertiser_ImplMinimalMdnsAllocator.h>
#include <lib/dnssd/minimal_mdns/AddressPolicy.h>
#include <lib/dnssd/minimal_mdns/ResponseSender.h>
#include <lib/dnssd/minimal_mdns/Server.h>
#include <lib/dnssd/minimal_mdns/core/FlatAllocatedQName.h>
#include <lib/dnssd/minimal_mdns/responders/IP.h>
#include <lib/dnssd/minimal_mdns/responders/Ptr.h>
#include <lib/dnssd/minimal_mdns/responders/QueryResponder.h>
#include <lib/dnssd/minimal_mdns/responders/Srv.h>
#include <lib/dnssd/minimal_mdns/responders/Txt.h>
#include <lib/support/BytesToHex.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/IntrusiveList.h>
#include <lib/support/StringBuilder.h>
// Enable detailed mDNS logging for received queries
#undef DETAIL_LOGGING
// #define DETAIL_LOGGING
namespace chip {
namespace Dnssd {
namespace {
using chip::Platform::UniquePtr;
using namespace mdns::Minimal;
#ifdef DETAIL_LOGGING
const char * ToString(QClass qClass)
{
switch (qClass)
{
case QClass::IN:
return "IN";
default:
return "???";
}
}
const char * ToString(QType qType)
{
switch (qType)
{
case QType::ANY:
return "ANY";
case QType::A:
return "A";
case QType::AAAA:
return "AAAA";
case QType::TXT:
return "TXT";
case QType::SRV:
return "SRV";
case QType::PTR:
return "PTR";
default:
return "???";
}
}
void LogQuery(const QueryData & data)
{
StringBuilder<128> logString;
logString.Add("QUERY ").Add(ToString(data.GetClass())).Add("/").Add(ToString(data.GetType())).Add(": ");
SerializedQNameIterator name = data.GetName();
while (name.Next())
{
logString.Add(name.Value()).Add(".");
}
ChipLogDetail(Discovery, "%s", logString.c_str());
}
#else
void LogQuery(const QueryData & data) {}
#endif
// Max number of records for operational = PTR, SRV, TXT, A, AAAA, I subtype.
constexpr size_t kMaxOperationalRecords = 6;
/// Represents an allocated operational responder.
///
/// Wraps a QueryResponderAllocator.
class OperationalQueryAllocator : public chip::IntrusiveListNodeBase<>
{
public:
using Allocator = QueryResponderAllocator<kMaxOperationalRecords>;
/// Prefer to use `::New` for allocations instead of this direct call
explicit OperationalQueryAllocator(Allocator * allocator) : mAllocator(allocator) {}
~OperationalQueryAllocator()
{
chip::Platform::Delete(mAllocator);
mAllocator = nullptr;
}
Allocator * GetAllocator() { return mAllocator; }
const Allocator * GetAllocator() const { return mAllocator; }
/// Allocate a new entry for this type.
///
/// May return null on allocation failures.
static OperationalQueryAllocator * New()
{
Allocator * allocator = chip::Platform::New<Allocator>();
if (allocator == nullptr)
{
return nullptr;
}
OperationalQueryAllocator * result = chip::Platform::New<OperationalQueryAllocator>(allocator);
if (result == nullptr)
{
chip::Platform::Delete(allocator);
return nullptr;
}
return result;
}
private:
Allocator * mAllocator = nullptr;
};
enum BroadcastAdvertiseType
{
kStarted, // Advertise at startup of all records added, as required by RFC 6762.
kRemovingAll, // sent a TTL 0 for all records, as records are removed
};
class AdvertiserMinMdns : public ServiceAdvertiser,
public MdnsPacketDelegate, // receive query packets
public ParserDelegate // parses queries
{
public:
AdvertiserMinMdns() : mResponseSender(&GlobalMinimalMdnsServer::Server())
{
GlobalMinimalMdnsServer::Instance().SetQueryDelegate(this);
CHIP_ERROR err = mResponseSender.AddQueryResponder(mQueryResponderAllocatorCommissionable.GetQueryResponder());
if (err != CHIP_NO_ERROR)
{
ChipLogError(Discovery, "Failed to set up commissionable responder: %" CHIP_ERROR_FORMAT, err.Format());
}
err = mResponseSender.AddQueryResponder(mQueryResponderAllocatorCommissioner.GetQueryResponder());
if (err != CHIP_NO_ERROR)
{
ChipLogError(Discovery, "Failed to set up commissioner responder: %" CHIP_ERROR_FORMAT, err.Format());
}
}
~AdvertiserMinMdns() override { ClearServices(); }
// Service advertiser
CHIP_ERROR Init(chip::Inet::EndPointManager<chip::Inet::UDPEndPoint> * udpEndPointManager) override;
bool IsInitialized() override { return mIsInitialized; }
void Shutdown() override;
CHIP_ERROR RemoveServices() override;
CHIP_ERROR Advertise(const OperationalAdvertisingParameters & params) override;
CHIP_ERROR Advertise(const CommissionAdvertisingParameters & params) override;
CHIP_ERROR FinalizeServiceUpdate() override;
CHIP_ERROR GetCommissionableInstanceName(char * instanceName, size_t maxLength) const override;
CHIP_ERROR UpdateCommissionableInstanceName() override;
// MdnsPacketDelegate
void OnMdnsPacketData(const BytesRange & data, const chip::Inet::IPPacketInfo * info) override;
// ParserDelegate
void OnHeader(ConstHeaderRef & header) override { mMessageId = header.GetMessageId(); }
void OnResource(ResourceType type, const ResourceData & data) override {}
void OnQuery(const QueryData & data) override;
private:
/// Advertise available records configured within the server.
///
/// Establishes a type of 'Advertise all currently configured items'
/// for a specific purpose (e.g. boot time advertises everything, shut-down
/// removes all records by advertising a 0 TTL)
void AdvertiseRecords(BroadcastAdvertiseType type);
FullQName GetCommissioningTxtEntries(const CommissionAdvertisingParameters & params);
FullQName GetOperationalTxtEntries(OperationalQueryAllocator::Allocator * allocator,
const OperationalAdvertisingParameters & params);
struct CommonTxtEntryStorage
{
// +2 for all to account for '=' and terminating nullchar
char sessionIdleIntervalBuf[KeySize(TxtFieldKey::kSessionIdleInterval) + ValSize(TxtFieldKey::kSessionIdleInterval) + 2];
char sessionActiveIntervalBuf[KeySize(TxtFieldKey::kSessionActiveInterval) + ValSize(TxtFieldKey::kSessionActiveInterval) +
2];
char sessionActiveThresholdBuf[KeySize(TxtFieldKey::kSessionActiveThreshold) +
ValSize(TxtFieldKey::kSessionActiveThreshold) + 2];
char tcpSupportedBuf[KeySize(TxtFieldKey::kTcpSupported) + ValSize(TxtFieldKey::kTcpSupported) + 2];
char operatingICDAsLITBuf[KeySize(TxtFieldKey::kLongIdleTimeICD) + ValSize(TxtFieldKey::kLongIdleTimeICD) + 2];
};
template <class Derived>
CHIP_ERROR AddCommonTxtEntries(const BaseAdvertisingParams<Derived> & params, CommonTxtEntryStorage & storage,
char ** txtFields, size_t & numTxtFields)
{
if (const auto & optionalMrp = params.GetLocalMRPConfig(); optionalMrp.has_value())
{
auto mrp = *optionalMrp;
// An ICD operating as a LIT shall not advertise its slow polling interval.
// Don't include the SII key in the advertisement when operating as so.
if (params.GetICDModeToAdvertise() != ICDModeAdvertise::kLIT)
{
if (mrp.mIdleRetransTimeout > kMaxRetryInterval)
{
ChipLogProgress(Discovery,
"MRP retry interval idle value exceeds allowed range of 1 hour, using maximum available");
mrp.mIdleRetransTimeout = kMaxRetryInterval;
}
size_t writtenCharactersNumber =
static_cast<size_t>(snprintf(storage.sessionIdleIntervalBuf, sizeof(storage.sessionIdleIntervalBuf),
"SII=%" PRIu32, mrp.mIdleRetransTimeout.count()));
VerifyOrReturnError((writtenCharactersNumber > 0) &&
(writtenCharactersNumber < sizeof(storage.sessionIdleIntervalBuf)),
CHIP_ERROR_INVALID_STRING_LENGTH);
txtFields[numTxtFields++] = storage.sessionIdleIntervalBuf;
}
{
if (mrp.mActiveRetransTimeout > kMaxRetryInterval)
{
ChipLogProgress(Discovery,
"MRP retry interval active value exceeds allowed range of 1 hour, using maximum available");
mrp.mActiveRetransTimeout = kMaxRetryInterval;
}
size_t writtenCharactersNumber =
static_cast<size_t>(snprintf(storage.sessionActiveIntervalBuf, sizeof(storage.sessionActiveIntervalBuf),
"SAI=%" PRIu32, mrp.mActiveRetransTimeout.count()));
VerifyOrReturnError((writtenCharactersNumber > 0) &&
(writtenCharactersNumber < sizeof(storage.sessionActiveIntervalBuf)),
CHIP_ERROR_INVALID_STRING_LENGTH);
txtFields[numTxtFields++] = storage.sessionActiveIntervalBuf;
}
{
size_t writtenCharactersNumber =
static_cast<size_t>(snprintf(storage.sessionActiveThresholdBuf, sizeof(storage.sessionActiveThresholdBuf),
"SAT=%u", mrp.mActiveThresholdTime.count()));
VerifyOrReturnError((writtenCharactersNumber > 0) &&
(writtenCharactersNumber < sizeof(storage.sessionActiveThresholdBuf)),
CHIP_ERROR_INVALID_STRING_LENGTH);
txtFields[numTxtFields++] = storage.sessionActiveThresholdBuf;
}
}
if (params.GetTCPSupportModes() != TCPModeAdvertise::kNone)
{
size_t writtenCharactersNumber = static_cast<size_t>(snprintf(storage.tcpSupportedBuf, sizeof(storage.tcpSupportedBuf),
"T=%d", static_cast<int>(params.GetTCPSupportModes())));
VerifyOrReturnError((writtenCharactersNumber > 0) && (writtenCharactersNumber < sizeof(storage.tcpSupportedBuf)),
CHIP_ERROR_INVALID_STRING_LENGTH);
txtFields[numTxtFields++] = storage.tcpSupportedBuf;
}
if (params.GetICDModeToAdvertise() != ICDModeAdvertise::kNone)
{
size_t writtenCharactersNumber =
static_cast<size_t>(snprintf(storage.operatingICDAsLITBuf, sizeof(storage.operatingICDAsLITBuf), "ICD=%d",
(params.GetICDModeToAdvertise() == ICDModeAdvertise::kLIT)));
VerifyOrReturnError((writtenCharactersNumber > 0) && (writtenCharactersNumber < sizeof(storage.operatingICDAsLITBuf)),
CHIP_ERROR_INVALID_STRING_LENGTH);
txtFields[numTxtFields++] = storage.operatingICDAsLITBuf;
}
return CHIP_NO_ERROR;
}
IntrusiveList<OperationalQueryAllocator> mOperationalResponders;
// Max number of records for commissionable = 7 x PTR (base + 6 sub types - _S, _L, _D, _T, _C, _A), SRV, TXT, A, AAAA
static constexpr size_t kMaxCommissionRecords = 11;
QueryResponderAllocator<kMaxCommissionRecords> mQueryResponderAllocatorCommissionable;
QueryResponderAllocator<kMaxCommissionRecords> mQueryResponderAllocatorCommissioner;
OperationalQueryAllocator::Allocator * FindOperationalAllocator(const FullQName & qname);
OperationalQueryAllocator::Allocator * FindEmptyOperationalAllocator();
void ClearServices();
ResponseSender mResponseSender;
uint8_t mCommissionableInstanceName[sizeof(uint64_t)];
bool mIsInitialized = false;
// current request handling
const chip::Inet::IPPacketInfo * mCurrentSource = nullptr;
uint16_t mMessageId = 0;
const char * mEmptyTextEntries[1] = {
"=",
};
};
void AdvertiserMinMdns::OnMdnsPacketData(const BytesRange & data, const chip::Inet::IPPacketInfo * info)
{
#ifdef DETAIL_LOGGING
char srcAddressString[chip::Inet::IPAddress::kMaxStringLength];
VerifyOrDie(info->SrcAddress.ToString(srcAddressString) != nullptr);
ChipLogDetail(Discovery, "Received an mDNS query from %s", srcAddressString);
#endif
mCurrentSource = info;
if (!ParsePacket(data, this))
{
ChipLogError(Discovery, "Failed to parse mDNS query");
}
mCurrentSource = nullptr;
}
void AdvertiserMinMdns::OnQuery(const QueryData & data)
{
if (mCurrentSource == nullptr)
{
ChipLogError(Discovery, "INTERNAL CONSISTENCY ERROR: missing query source");
return;
}
LogQuery(data);
const ResponseConfiguration defaultResponseConfiguration;
CHIP_ERROR err = mResponseSender.Respond(mMessageId, data, mCurrentSource, defaultResponseConfiguration);
if (err != CHIP_NO_ERROR)
{
ChipLogError(Discovery, "Failed to reply to query: %" CHIP_ERROR_FORMAT, err.Format());
}
}
CHIP_ERROR AdvertiserMinMdns::Init(chip::Inet::EndPointManager<chip::Inet::UDPEndPoint> * udpEndPointManager)
{
// TODO: Per API documentation, Init() should be a no-op if mIsInitialized
// is true. But we don't handle updates to our set of interfaces right now,
// so rely on the logic in this function to shut down and restart the
// GlobalMinimalMdnsServer to handle that.
GlobalMinimalMdnsServer::Server().ShutdownEndpoints();
if (!mIsInitialized)
{
UpdateCommissionableInstanceName();
}
// Re-set the server in the response sender in case this has been swapped in the
// GlobalMinimalMdnsServer (used for testing).
mResponseSender.SetServer(&GlobalMinimalMdnsServer::Server());
ReturnErrorOnFailure(GlobalMinimalMdnsServer::Instance().StartServer(udpEndPointManager, kMdnsPort));
ChipLogProgress(Discovery, "CHIP minimal mDNS started advertising.");
AdvertiseRecords(BroadcastAdvertiseType::kStarted);
mIsInitialized = true;
return CHIP_NO_ERROR;
}
void AdvertiserMinMdns::Shutdown()
{
VerifyOrReturn(mIsInitialized);
AdvertiseRecords(BroadcastAdvertiseType::kRemovingAll);
GlobalMinimalMdnsServer::Server().Shutdown();
mIsInitialized = false;
}
CHIP_ERROR AdvertiserMinMdns::RemoveServices()
{
VerifyOrReturnError(mIsInitialized, CHIP_ERROR_INCORRECT_STATE);
// Send a "goodbye" packet for each RR being removed, as defined in RFC 6762.
// This allows mDNS clients to remove stale cached records which may not be re-added with
// subsequent Advertise() calls. In the case the same records are re-added, this extra
// is not harmful though suboptimal, so this is a subject to improvement in the future.
AdvertiseRecords(BroadcastAdvertiseType::kRemovingAll);
ClearServices();
return CHIP_NO_ERROR;
}
void AdvertiserMinMdns::ClearServices()
{
while (mOperationalResponders.begin() != mOperationalResponders.end())
{
auto it = mOperationalResponders.begin();
// Need to free the memory once it is out of the list
OperationalQueryAllocator * ptr = &*it;
// Mark as unused
ptr->GetAllocator()->Clear();
CHIP_ERROR err = mResponseSender.RemoveQueryResponder(ptr->GetAllocator()->GetQueryResponder());
if (err != CHIP_NO_ERROR)
{
ChipLogError(Discovery, "Failed to remove query responder: %" CHIP_ERROR_FORMAT, err.Format());
}
mOperationalResponders.Remove(ptr);
// Finally release the memory
chip::Platform::Delete(ptr);
}
mQueryResponderAllocatorCommissionable.Clear();
mQueryResponderAllocatorCommissioner.Clear();
}
OperationalQueryAllocator::Allocator * AdvertiserMinMdns::FindOperationalAllocator(const FullQName & qname)
{
for (auto & it : mOperationalResponders)
{
if (it.GetAllocator()->GetResponder(QType::SRV, qname) != nullptr)
{
return it.GetAllocator();
}
}
return nullptr;
}
OperationalQueryAllocator::Allocator * AdvertiserMinMdns::FindEmptyOperationalAllocator()
{
OperationalQueryAllocator * result = OperationalQueryAllocator::New();
if (result == nullptr)
{
return nullptr;
}
CHIP_ERROR err = mResponseSender.AddQueryResponder(result->GetAllocator()->GetQueryResponder());
if (err != CHIP_NO_ERROR)
{
ChipLogError(Discovery, "Failed to register query responder: %" CHIP_ERROR_FORMAT, err.Format());
Platform::Delete(result);
return nullptr;
}
mOperationalResponders.PushBack(result);
return result->GetAllocator();
}
CHIP_ERROR AdvertiserMinMdns::Advertise(const OperationalAdvertisingParameters & params)
{
VerifyOrReturnError(mIsInitialized, CHIP_ERROR_INCORRECT_STATE);
char nameBuffer[Operational::kInstanceNameMaxLength + 1] = "";
// need to set server name
ReturnErrorOnFailure(MakeInstanceName(nameBuffer, sizeof(nameBuffer), params.GetPeerId()));
QNamePart nameCheckParts[] = { nameBuffer, kOperationalServiceName, kOperationalProtocol, kLocalDomain };
FullQName nameCheck = FullQName(nameCheckParts);
auto * operationalAllocator = FindOperationalAllocator(nameCheck);
if (operationalAllocator != nullptr)
{
operationalAllocator->Clear();
}
else
{
operationalAllocator = FindEmptyOperationalAllocator();
if (operationalAllocator == nullptr)
{
ChipLogError(Discovery, "Failed to find an open operational allocator");
return CHIP_ERROR_NO_MEMORY;
}
}
FullQName serviceName = operationalAllocator->AllocateQName(kOperationalServiceName, kOperationalProtocol, kLocalDomain);
FullQName instanceName =
operationalAllocator->AllocateQName(nameBuffer, kOperationalServiceName, kOperationalProtocol, kLocalDomain);
ReturnErrorOnFailure(MakeHostName(nameBuffer, sizeof(nameBuffer), params.GetMac()));
FullQName hostName = operationalAllocator->AllocateQName(nameBuffer, kLocalDomain);
if ((serviceName.nameCount == 0) || (instanceName.nameCount == 0) || (hostName.nameCount == 0))
{
ChipLogError(Discovery, "Failed to allocate QNames.");
return CHIP_ERROR_NO_MEMORY;
}
if (!operationalAllocator->AddResponder<PtrResponder>(serviceName, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add service PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
// We are the sole owner of our instanceName, so records keyed on the
// instanceName should have the cache-flush bit set.
SrvResourceRecord srvRecord(instanceName, hostName, params.GetPort());
srvRecord.SetCacheFlush(true);
if (!operationalAllocator->AddResponder<SrvResponder>(srvRecord).SetReportAdditional(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add SRV record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
TxtResourceRecord txtRecord(instanceName, GetOperationalTxtEntries(operationalAllocator, params));
txtRecord.SetCacheFlush(true);
if (!operationalAllocator->AddResponder<TxtResponder>(txtRecord).SetReportAdditional(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add TXT record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
if (!operationalAllocator->AddResponder<IPv6Responder>(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add IPv6 mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
if (params.IsIPv4Enabled())
{
if (!operationalAllocator->AddResponder<IPv4Responder>(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add IPv4 mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
}
MakeServiceSubtype(nameBuffer, sizeof(nameBuffer),
DiscoveryFilter(DiscoveryFilterType::kCompressedFabricId, params.GetPeerId().GetCompressedFabricId()));
FullQName compressedFabricIdSubtype = operationalAllocator->AllocateQName(
nameBuffer, kSubtypeServiceNamePart, kOperationalServiceName, kOperationalProtocol, kLocalDomain);
ReturnErrorCodeIf(compressedFabricIdSubtype.nameCount == 0, CHIP_ERROR_NO_MEMORY);
if (!operationalAllocator->AddResponder<PtrResponder>(compressedFabricIdSubtype, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add device type PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
ChipLogProgress(Discovery, "CHIP minimal mDNS configured as 'Operational device'; instance name: %s.", instanceName.names[0]);
AdvertiseRecords(BroadcastAdvertiseType::kStarted);
ChipLogProgress(Discovery, "mDNS service published: %s.%s", StringOrNullMarker(instanceName.names[1]),
StringOrNullMarker(instanceName.names[2]));
return CHIP_NO_ERROR;
}
CHIP_ERROR AdvertiserMinMdns::FinalizeServiceUpdate()
{
VerifyOrReturnError(mIsInitialized, CHIP_ERROR_INCORRECT_STATE);
return CHIP_NO_ERROR;
}
CHIP_ERROR AdvertiserMinMdns::GetCommissionableInstanceName(char * instanceName, size_t maxLength) const
{
if (maxLength < (Commission::kInstanceNameMaxLength + 1))
{
return CHIP_ERROR_NO_MEMORY;
}
return chip::Encoding::BytesToUppercaseHexString(&mCommissionableInstanceName[0], sizeof(mCommissionableInstanceName),
instanceName, maxLength);
}
CHIP_ERROR AdvertiserMinMdns::UpdateCommissionableInstanceName()
{
uint64_t random_instance_name = chip::Crypto::GetRandU64();
static_assert(sizeof(mCommissionableInstanceName) == sizeof(random_instance_name), "Not copying the right amount of data");
memcpy(&mCommissionableInstanceName[0], &random_instance_name, sizeof(mCommissionableInstanceName));
return CHIP_NO_ERROR;
}
CHIP_ERROR AdvertiserMinMdns::Advertise(const CommissionAdvertisingParameters & params)
{
VerifyOrReturnError(mIsInitialized, CHIP_ERROR_INCORRECT_STATE);
if (params.GetCommissionAdvertiseMode() == CommssionAdvertiseMode::kCommissionableNode)
{
mQueryResponderAllocatorCommissionable.Clear();
}
else
{
mQueryResponderAllocatorCommissioner.Clear();
}
// TODO: need to detect colisions here
char nameBuffer[64] = "";
ReturnErrorOnFailure(GetCommissionableInstanceName(nameBuffer, sizeof(nameBuffer)));
QueryResponderAllocator<kMaxCommissionRecords> * allocator =
params.GetCommissionAdvertiseMode() == CommssionAdvertiseMode::kCommissionableNode ? &mQueryResponderAllocatorCommissionable
: &mQueryResponderAllocatorCommissioner;
const char * serviceType = params.GetCommissionAdvertiseMode() == CommssionAdvertiseMode::kCommissionableNode
? kCommissionableServiceName
: kCommissionerServiceName;
FullQName serviceName = allocator->AllocateQName(serviceType, kCommissionProtocol, kLocalDomain);
FullQName instanceName = allocator->AllocateQName(nameBuffer, serviceType, kCommissionProtocol, kLocalDomain);
ReturnErrorOnFailure(MakeHostName(nameBuffer, sizeof(nameBuffer), params.GetMac()));
FullQName hostName = allocator->AllocateQName(nameBuffer, kLocalDomain);
if ((serviceName.nameCount == 0) || (instanceName.nameCount == 0) || (hostName.nameCount == 0))
{
ChipLogError(Discovery, "Failed to allocate QNames.");
return CHIP_ERROR_NO_MEMORY;
}
if (!allocator->AddResponder<PtrResponder>(serviceName, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add service PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
SrvResourceRecord srvRecord(instanceName, hostName, params.GetPort());
srvRecord.SetCacheFlush(true);
if (!allocator->AddResponder<SrvResponder>(srvRecord).SetReportAdditional(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add SRV record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
if (!allocator->AddResponder<IPv6Responder>(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add IPv6 mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
if (params.IsIPv4Enabled())
{
if (!allocator->AddResponder<IPv4Responder>(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add IPv4 mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
}
if (const auto & vendorId = params.GetVendorId(); vendorId.has_value())
{
MakeServiceSubtype(nameBuffer, sizeof(nameBuffer), DiscoveryFilter(DiscoveryFilterType::kVendorId, *vendorId));
FullQName vendorServiceName =
allocator->AllocateQName(nameBuffer, kSubtypeServiceNamePart, serviceType, kCommissionProtocol, kLocalDomain);
ReturnErrorCodeIf(vendorServiceName.nameCount == 0, CHIP_ERROR_NO_MEMORY);
if (!allocator->AddResponder<PtrResponder>(vendorServiceName, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add vendor PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
}
if (const auto & deviceType = params.GetDeviceType(); deviceType.has_value())
{
MakeServiceSubtype(nameBuffer, sizeof(nameBuffer), DiscoveryFilter(DiscoveryFilterType::kDeviceType, *deviceType));
FullQName vendorServiceName =
allocator->AllocateQName(nameBuffer, kSubtypeServiceNamePart, serviceType, kCommissionProtocol, kLocalDomain);
ReturnErrorCodeIf(vendorServiceName.nameCount == 0, CHIP_ERROR_NO_MEMORY);
if (!allocator->AddResponder<PtrResponder>(vendorServiceName, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add device type PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
}
// the following sub types only apply to commissionable node advertisements
if (params.GetCommissionAdvertiseMode() == CommssionAdvertiseMode::kCommissionableNode)
{
{
MakeServiceSubtype(nameBuffer, sizeof(nameBuffer),
DiscoveryFilter(DiscoveryFilterType::kShortDiscriminator, params.GetShortDiscriminator()));
FullQName shortServiceName =
allocator->AllocateQName(nameBuffer, kSubtypeServiceNamePart, serviceType, kCommissionProtocol, kLocalDomain);
ReturnErrorCodeIf(shortServiceName.nameCount == 0, CHIP_ERROR_NO_MEMORY);
if (!allocator->AddResponder<PtrResponder>(shortServiceName, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add short discriminator PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
}
{
MakeServiceSubtype(nameBuffer, sizeof(nameBuffer),
DiscoveryFilter(DiscoveryFilterType::kLongDiscriminator, params.GetLongDiscriminator()));
FullQName longServiceName =
allocator->AllocateQName(nameBuffer, kSubtypeServiceNamePart, serviceType, kCommissionProtocol, kLocalDomain);
ReturnErrorCodeIf(longServiceName.nameCount == 0, CHIP_ERROR_NO_MEMORY);
if (!allocator->AddResponder<PtrResponder>(longServiceName, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add long discriminator PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
}
if (params.GetCommissioningMode() != CommissioningMode::kDisabled)
{
MakeServiceSubtype(nameBuffer, sizeof(nameBuffer), DiscoveryFilter(DiscoveryFilterType::kCommissioningMode));
FullQName longServiceName =
allocator->AllocateQName(nameBuffer, kSubtypeServiceNamePart, serviceType, kCommissionProtocol, kLocalDomain);
ReturnErrorCodeIf(longServiceName.nameCount == 0, CHIP_ERROR_NO_MEMORY);
if (!allocator->AddResponder<PtrResponder>(longServiceName, instanceName)
.SetReportAdditional(instanceName)
.SetReportInServiceListing(true)
.IsValid())
{
ChipLogError(Discovery, "Failed to add commissioning mode PTR record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
}
}
TxtResourceRecord txtRecord(instanceName, GetCommissioningTxtEntries(params));
txtRecord.SetCacheFlush(true);
if (!allocator->AddResponder<TxtResponder>(txtRecord).SetReportAdditional(hostName).IsValid())
{
ChipLogError(Discovery, "Failed to add TXT record mDNS responder");
return CHIP_ERROR_NO_MEMORY;
}
if (params.GetCommissionAdvertiseMode() == CommssionAdvertiseMode::kCommissionableNode)
{
ChipLogProgress(Discovery, "CHIP minimal mDNS configured as 'Commissionable node device'; instance name: %s.",
StringOrNullMarker(instanceName.names[0]));
}
else
{
ChipLogProgress(Discovery, "CHIP minimal mDNS configured as 'Commissioner device'; instance name: %s.",
StringOrNullMarker(instanceName.names[0]));
}
AdvertiseRecords(BroadcastAdvertiseType::kStarted);
ChipLogProgress(Discovery, "mDNS service published: %s.%s", StringOrNullMarker(instanceName.names[1]),
StringOrNullMarker(instanceName.names[2]));
return CHIP_NO_ERROR;
}
FullQName AdvertiserMinMdns::GetOperationalTxtEntries(OperationalQueryAllocator::Allocator * allocator,
const OperationalAdvertisingParameters & params)
{
char * txtFields[OperationalAdvertisingParameters::kTxtMaxNumber];
size_t numTxtFields = 0;
struct CommonTxtEntryStorage commonStorage;
AddCommonTxtEntries<OperationalAdvertisingParameters>(params, commonStorage, txtFields, numTxtFields);
if (numTxtFields == 0)
{
return allocator->AllocateQNameFromArray(mEmptyTextEntries, 1);
}
return allocator->AllocateQNameFromArray(txtFields, numTxtFields);
}
FullQName AdvertiserMinMdns::GetCommissioningTxtEntries(const CommissionAdvertisingParameters & params)
{
char * txtFields[CommissionAdvertisingParameters::kTxtMaxNumber];
size_t numTxtFields = 0;
QueryResponderAllocator<kMaxCommissionRecords> * allocator =
params.GetCommissionAdvertiseMode() == CommssionAdvertiseMode::kCommissionableNode ? &mQueryResponderAllocatorCommissionable
: &mQueryResponderAllocatorCommissioner;
char txtVidPid[chip::Dnssd::kKeyVendorProductMaxLength + 4];
{
const auto & productId = params.GetProductId();
const auto & vendorId = params.GetVendorId();
if (productId.has_value() && vendorId.has_value())
{
snprintf(txtVidPid, sizeof(txtVidPid), "VP=%d+%d", *vendorId, *productId);
txtFields[numTxtFields++] = txtVidPid;
}
else if (vendorId.has_value())
{
snprintf(txtVidPid, sizeof(txtVidPid), "VP=%d", *vendorId);
txtFields[numTxtFields++] = txtVidPid;
}
}
char txtDeviceType[chip::Dnssd::kKeyDeviceTypeMaxLength + 4];
if (const auto & deviceType = params.GetDeviceType(); deviceType.has_value())
{
snprintf(txtDeviceType, sizeof(txtDeviceType), "DT=%" PRIu32, *deviceType);
txtFields[numTxtFields++] = txtDeviceType;
}
char txtDeviceName[chip::Dnssd::kKeyDeviceNameMaxLength + 4];
if (const auto & deviceName = params.GetDeviceName(); deviceName.has_value())
{
snprintf(txtDeviceName, sizeof(txtDeviceName), "DN=%s", *deviceName);
txtFields[numTxtFields++] = txtDeviceName;
}
CommonTxtEntryStorage commonStorage;
AddCommonTxtEntries<CommissionAdvertisingParameters>(params, commonStorage, txtFields, numTxtFields);
// the following sub types only apply to commissionable node advertisements
char txtDiscriminator[chip::Dnssd::kKeyLongDiscriminatorMaxLength + 3];
char txtCommissioningMode[chip::Dnssd::kKeyCommissioningModeMaxLength + 4];
char txtRotatingDeviceId[chip::Dnssd::kKeyRotatingDeviceIdMaxLength + 4];
char txtPairingHint[chip::Dnssd::kKeyPairingInstructionMaxLength + 4];
char txtPairingInstr[chip::Dnssd::kKeyPairingInstructionMaxLength + 4];
// the following sub types only apply to commissioner discovery advertisements
char txtCommissionerPasscode[chip::Dnssd::kKeyCommissionerPasscodeMaxLength + 4];
if (params.GetCommissionAdvertiseMode() == CommssionAdvertiseMode::kCommissionableNode)
{
// a discriminator always exists
snprintf(txtDiscriminator, sizeof(txtDiscriminator), "D=%d", params.GetLongDiscriminator());
txtFields[numTxtFields++] = txtDiscriminator;
snprintf(txtCommissioningMode, sizeof(txtCommissioningMode), "CM=%d", static_cast<int>(params.GetCommissioningMode()));
txtFields[numTxtFields++] = txtCommissioningMode;
if (const auto & rotatingDeviceId = params.GetRotatingDeviceId(); rotatingDeviceId.has_value())
{
snprintf(txtRotatingDeviceId, sizeof(txtRotatingDeviceId), "RI=%s", *rotatingDeviceId);
txtFields[numTxtFields++] = txtRotatingDeviceId;
}
if (const auto & pairingHint = params.GetPairingHint(); pairingHint.has_value())
{
snprintf(txtPairingHint, sizeof(txtPairingHint), "PH=%d", *pairingHint);
txtFields[numTxtFields++] = txtPairingHint;
}
if (const auto & pairingInstruction = params.GetPairingInstruction(); pairingInstruction.has_value())
{
snprintf(txtPairingInstr, sizeof(txtPairingInstr), "PI=%s", *pairingInstruction);
txtFields[numTxtFields++] = txtPairingInstr;
}
}
else
{
if (params.GetCommissionerPasscodeSupported().value_or(false))
{
snprintf(txtCommissionerPasscode, sizeof(txtCommissionerPasscode), "CP=%d", static_cast<int>(1));
txtFields[numTxtFields++] = txtCommissionerPasscode;
}
}
if (numTxtFields == 0)
{
return allocator->AllocateQNameFromArray(mEmptyTextEntries, 1);
}
return allocator->AllocateQNameFromArray(txtFields, numTxtFields);
}
void AdvertiserMinMdns::AdvertiseRecords(BroadcastAdvertiseType type)
{
ResponseConfiguration responseConfiguration;
if (type == BroadcastAdvertiseType::kRemovingAll)
{
// make a "remove all records now" broadcast
responseConfiguration.SetTtlSecondsOverride(0);
}
UniquePtr<ListenIterator> allInterfaces = GetAddressPolicy()->GetListenEndpoints();
VerifyOrDieWithMsg(allInterfaces != nullptr, Discovery, "Failed to allocate memory for endpoints.");
chip::Inet::InterfaceId interfaceId;
chip::Inet::IPAddressType addressType;
while (allInterfaces->Next(&interfaceId, &addressType))
{
UniquePtr<IpAddressIterator> allIps = GetAddressPolicy()->GetIpAddressesForEndpoint(interfaceId, addressType);
VerifyOrDieWithMsg(allIps != nullptr, Discovery, "Failed to allocate memory for ip addresses.");
chip::Inet::IPPacketInfo packetInfo;
packetInfo.Clear();
// advertising on every interface requires a valid IP address
// since we use "BROADCAST" (unicast is false), we do not actually care about
// the source IP address value, just that it has the right "type"
//
// NOTE: cannot use Broadcast address as the source as they have the type kAny.
//
// TODO: ideally we may want to have a destination that is explicit as "unicast/destIp"
// vs "multicast/addressType". Such a change requires larger code updates.
packetInfo.SrcAddress = chip::Inet::IPAddress::Loopback(addressType);
packetInfo.DestAddress = BroadcastIpAddresses::Get(addressType);
packetInfo.SrcPort = kMdnsPort;
packetInfo.DestPort = kMdnsPort;
packetInfo.Interface = interfaceId;
// Advertise all records
//
// TODO: Consider advertising delta changes.
//
// Current advertisement does not have a concept of "delta" to only
// advertise changes. Current implementation is to always
// 1. advertise TTL=0 (clear all caches)
// 2. advertise available records (with longer TTL)
//
// It would be nice if we could selectively advertise what changes, like
// send TTL=0 for anything removed/about to be removed (and only those),
// then only advertise new items added.
//
// This optimization likely will take more logic and state storage, so
// for now it is not done.
QueryData queryData(QType::PTR, QClass::IN, false /* unicast */);
queryData.SetIsAnnounceBroadcast(true);
for (auto & it : mOperationalResponders)
{
it.GetAllocator()->GetQueryResponder()->ClearBroadcastThrottle();
}
mQueryResponderAllocatorCommissionable.GetQueryResponder()->ClearBroadcastThrottle();
mQueryResponderAllocatorCommissioner.GetQueryResponder()->ClearBroadcastThrottle();
CHIP_ERROR err = mResponseSender.Respond(0, queryData, &packetInfo, responseConfiguration);
if (err != CHIP_NO_ERROR)
{
ChipLogError(Discovery, "Failed to advertise records: %" CHIP_ERROR_FORMAT, err.Format());
}
}
// Once all automatic broadcasts are done, allow immediate replies once.
for (auto & it : mOperationalResponders)
{
it.GetAllocator()->GetQueryResponder()->ClearBroadcastThrottle();
}
mQueryResponderAllocatorCommissionable.GetQueryResponder()->ClearBroadcastThrottle();
mQueryResponderAllocatorCommissioner.GetQueryResponder()->ClearBroadcastThrottle();
}
AdvertiserMinMdns gAdvertiser;
} // namespace
#if CHIP_DNSSD_DEFAULT_MINIMAL
ServiceAdvertiser & GetDefaultAdvertiser()
{
return gAdvertiser;
}
#endif // CHIP_DNSSD_DEFAULT_MINIMAL
} // namespace Dnssd
} // namespace chip