[dnssd] support RDATA translation in discovery proxy (#11437)

This commit adds implementation for RDATA translation in the
OpenThread native discovery proxy. Specifically, for certain record
types (like CNAME) where the record data includes one or more
embedded DNS names, this translation applies. If the embedded DNS
name in RDATA uses the local mDNS domain (`local.`), it is replaced
with the corresponding domain name for the Thread mesh network
(`default.service.arpa.`). Otherwise, the name is included unchanged
in the record data.

A new method, `AppendTranslatedRecordDataTo()`, is added to perform
this translation. It utilizes the `DataRecipe` table, similar to
`DecompressRecordData()`, to parse the record data and update the
embedded DNS names as needed.

The `test_dnssd_discovery_proxy` unit test is updated to cover the new
record data translation behavior.
This commit is contained in:
Abtin Keshavarzian
2025-04-28 09:37:40 -07:00
committed by GitHub
parent 819938d05d
commit d6c35621bb
5 changed files with 253 additions and 26 deletions

View File

@ -1036,26 +1036,8 @@ exit:
return error;
}
Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr<Message> &aDataMsg)
const ResourceRecord::DataRecipe *ResourceRecord::FindDataRecipeFor(uint16_t aRecordType)
{
// Reads the `ResourceRecord` header to identify the record type
// and uses a predefined recipe to parse the record data.
struct DataRecipe
{
int Compare(uint16_t aRecordType) const { return (aRecordType - mRecordType); }
constexpr static bool AreInOrder(const DataRecipe &aFirst, const DataRecipe &aSecond)
{
return (aFirst.mRecordType < aSecond.mRecordType);
}
uint16_t mRecordType; // The record type.
uint8_t mNumPrefixBytes; // Number of bytes in RDATA before the first name.
uint8_t mNumNames; // Number of DNS names embedded in the RDATA.
uint16_t mMinNumSuffixBytes; // Minimum number of expected bytes in RDATA after the last name.
};
static constexpr DataRecipe kRecipes[] = {
{kTypeNs, 0, 1, 0},
{kTypeCname, 0, 1, 0},
@ -1074,6 +1056,14 @@ Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOf
static_assert(BinarySearch::IsSorted(kRecipes), "kRecipes is not sorted");
return BinarySearch::Find(aRecordType, kRecipes);
}
Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr<Message> &aDataMsg)
{
// Reads the `ResourceRecord` header to identify the record type
// and uses a predefined recipe to parse the record data.
Error error;
ResourceRecord record;
const DataRecipe *recipe;
@ -1083,7 +1073,7 @@ Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOf
SuccessOrExit(error = record.ReadFrom(aMessage, aOffset));
aOffset += sizeof(ResourceRecord);
recipe = BinarySearch::Find(record.GetType(), kRecipes);
recipe = FindDataRecipeFor(record.GetType());
if (recipe == nullptr)
{
@ -1131,6 +1121,89 @@ exit:
return error;
}
Error ResourceRecord::AppendTranslatedRecordDataTo(Message &aMessage,
uint16_t aRecordType,
const Data<kWithUint16Length> &aData,
const char *aOriginalDomain,
uint16_t aTranslatedDomainOffset)
{
Error error = kErrorNone;
const DataRecipe *recipe = FindDataRecipeFor(aRecordType);
OwnedPtr<Message> dataMsg;
uint16_t offset;
uint16_t remainingLength;
if (recipe == nullptr)
{
error = aMessage.AppendData(aData);
ExitNow();
}
dataMsg.Reset(aMessage.Get<MessagePool>().Allocate(Message::kTypeOther));
VerifyOrExit(dataMsg != nullptr, error = kErrorNoBufs);
SuccessOrExit(error = dataMsg->AppendData(aData));
// Append the prefix bytes in the record data.
offset = 0;
SuccessOrExit(error = aMessage.AppendBytesFromMessage(*dataMsg, offset, recipe->mNumPrefixBytes));
offset += recipe->mNumPrefixBytes;
// Translate and append the embedded DNS names
for (uint8_t numNames = 0; numNames < recipe->mNumNames; numNames++)
{
Name::LabelBuffer label;
uint8_t labelLength;
uint16_t labelOffset;
// Read labels one by one and append them to `aMessage`.
// First, check if the remaining labels match the original
// domain name and if so, append the translated domain name
// (as a compressed pointer label) instead.
labelOffset = offset;
while (true)
{
uint16_t compareOffset = labelOffset;
if (Name::CompareName(*dataMsg, compareOffset, aOriginalDomain) == kErrorNone)
{
SuccessOrExit(error = Name::AppendPointerLabel(aTranslatedDomainOffset, aMessage));
break;
}
labelLength = sizeof(label);
error = Name::ReadLabel(*dataMsg, labelOffset, label, labelLength);
if (error == kErrorNotFound)
{
// Reached end of the label
break;
}
SuccessOrExit(error);
SuccessOrExit(error = Name::AppendLabel(label, aMessage));
}
// Parse name and update `offset` to the end of name field.
SuccessOrExit(error = Name::ParseName(*dataMsg, offset));
}
// Append the extra bytes after the name(s).
VerifyOrExit(offset <= dataMsg->GetLength(), error = kErrorParse);
remainingLength = dataMsg->GetLength() - offset;
VerifyOrExit(remainingLength >= recipe->mMinNumSuffixBytes, error = kErrorParse);
SuccessOrExit(error = aMessage.AppendBytesFromMessage(*dataMsg, offset, remainingLength));
exit:
return error;
}
ResourceRecord::TypeInfoString ResourceRecord::TypeToString(uint16_t aRecordType)
{
static constexpr Stringify::Entry kRecordTypeTable[] = {

View File

@ -42,6 +42,7 @@
#include "common/appender.hpp"
#include "common/as_core_type.hpp"
#include "common/clearable.hpp"
#include "common/data.hpp"
#include "common/encoding.hpp"
#include "common/equatable.hpp"
#include "common/message.hpp"
@ -1550,6 +1551,35 @@ public:
*/
static Error DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr<Message> &aDataMsg);
/**
* Translates embedded DNS names in record data (if needed) and appends the translated data to a given message.
*
* For records with type NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, or NSEC, the record data
* includes one or more embedded DNS names. For these record types, if the embedded DNS name uses the given
* @p aOriginalDomain, it is replaced with the translated domain name before appending it to @p aMessage.
* Otherwise, the name is appended as it appears in the record data. This is intended for use by the Discovery
* Proxy where the RDATA from mDNS will use the `.local.` domain name, which then needs to be translated to the
* Thread network domain name (`default.service.arpa.`).
*
* For other record types, the record data @p aData is appended as is.
*
* @param[in] aMessage The message to append the (translated) record data to.
* @param[in] aRecordType The record type.
* @param[in] aData The record data (to translate and append).
* @param[in] aOriginalDomain The original domain name.
* @param[in] aTranslatedDomainOffset The offset of the translated domain name in @p aMessage.
*
* @retval kErrorNone The (translated) record data was successfully appended to @p aMessage.
* @retval kErrorNoBufs Failed to allocate new buffers.
* @retval kErrorParse The given @p aData format is not valid.
*
*/
static Error AppendTranslatedRecordDataTo(Message &aMessage,
uint16_t aRecordType,
const Data<kWithUint16Length> &aData,
const char *aOriginalDomain,
uint16_t aTranslatedDomainOffset);
/**
* Returns a human-readable string representation of a given resource record type.
*
@ -1571,6 +1601,21 @@ protected:
private:
static constexpr uint16_t kType = kTypeAny; // This is intended for used by `ReadRecord<RecordType>()` only.
struct DataRecipe // RDATA recipe for record types that contain one or more embedded DNS names
{
int Compare(uint16_t aRecordType) const { return (aRecordType - mRecordType); }
constexpr static bool AreInOrder(const DataRecipe &aFirst, const DataRecipe &aSecond)
{
return (aFirst.mRecordType < aSecond.mRecordType);
}
uint16_t mRecordType; // The record type.
uint8_t mNumPrefixBytes; // Number of bytes in RDATA before the first name.
uint8_t mNumNames; // Number of DNS names embedded in the RDATA.
uint16_t mMinNumSuffixBytes; // Minimum number of expected bytes in RDATA after the last name.
};
static Error FindRecord(const Message &aMessage,
uint16_t &aOffset,
uint16_t aNumRecords,
@ -1586,6 +1631,8 @@ private:
ResourceRecord &aRecord,
uint16_t aMinRecordSize);
static const DataRecipe *FindDataRecipeFor(uint16_t aRecordType);
Error CheckRecord(const Message &aMessage, uint16_t aOffset) const;
Error ReadFrom(const Message &aMessage, uint16_t aOffset);

View File

@ -45,6 +45,7 @@ RegisterLogModule("DnssdServer");
const char Server::kDefaultDomainName[] = "default.service.arpa.";
const char Server::kSubLabel[] = "_sub";
const char Server::kMdnsDomainName[] = "local.";
#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
const char *Server::kBlockedDomains[] = {"ipv4only.arpa."};
@ -678,6 +679,7 @@ exit:
Error Server::Response::AppendKeyRecord(const Srp::Server::Host &aHost)
{
Ecdsa256KeyRecord keyRecord;
RecordData keyData;
uint32_t ttl;
keyRecord.Init();
@ -686,25 +688,31 @@ Error Server::Response::AppendKeyRecord(const Srp::Server::Host &aHost)
keyRecord.SetAlgorithm(KeyRecord::kAlgorithmEcdsaP256Sha256);
keyRecord.SetLength(sizeof(Ecdsa256KeyRecord) - sizeof(ResourceRecord));
keyRecord.SetKey(aHost.GetKey());
keyData.InitFrom(keyRecord);
ttl = TimeMilli::MsecToSec(aHost.GetExpireTime() - TimerMilli::GetNow());
return AppendGenericRecord(Ecdsa256KeyRecord::kType, &keyRecord, sizeof(keyRecord), ttl);
return AppendGenericRecord(Ecdsa256KeyRecord::kType, keyData, ttl);
}
#endif
Error Server::Response::AppendGenericRecord(uint16_t aRrType, const void *aData, uint16_t aDataLength, uint32_t aTtl)
Error Server::Response::AppendGenericRecord(uint16_t aRrType, const RecordData &aData, uint32_t aTtl)
{
Error error = kErrorNone;
ResourceRecord record;
uint16_t recordOffset;
record.Init(aRrType);
record.SetTtl(aTtl);
record.SetLength(aDataLength);
SuccessOrExit(error = Name::AppendPointerLabel(mOffsets.mHostName, *mMessage));
recordOffset = mMessage->GetLength();
SuccessOrExit(error = mMessage->Append(record));
SuccessOrExit(error = mMessage->AppendBytes(aData, aDataLength));
SuccessOrExit(error = ResourceRecord::AppendTranslatedRecordDataTo(*mMessage, aRrType, aData, kMdnsDomainName,
mOffsets.mDomainName));
ResourceRecord::UpdateRecordLengthInMessage(*mMessage, recordOffset);
IncResourceRecordCount();
@ -2322,10 +2330,13 @@ exit:
Error Server::Response::AppendGenericRecord(const ProxyResult &aResult)
{
const Dnssd::RecordResult *result = aResult.mRecordResult;
RecordData data;
mSection = kAnswerSection;
return AppendGenericRecord(result->mRecordType, result->mRecordData, result->mRecordDataLength, result->mTtl);
data.Init(result->mRecordData, result->mRecordDataLength);
return AppendGenericRecord(result->mRecordType, data, result->mTtl);
}
bool Server::IsProxyAddressValid(const Ip6::Address &aAddress)

View File

@ -350,6 +350,8 @@ private:
};
#endif
typedef Data<kWithUint16Length> RecordData;
struct Questions
{
Questions(void) { mFirstRrType = 0, mSecondRrType = 0; }
@ -420,7 +422,7 @@ private:
uint16_t aPort);
Error AppendTxtRecord(const ServiceInstanceInfo &aInstanceInfo);
Error AppendTxtRecord(const void *aTxtData, uint16_t aTxtLength, uint32_t aTtl);
Error AppendGenericRecord(uint16_t aRrType, const void *aData, uint16_t aDataLength, uint32_t aTtl);
Error AppendGenericRecord(uint16_t aRrType, const RecordData &aData, uint32_t aTtl);
Error AppendHostAddresses(AddrType aAddrType, const HostInfo &aHostInfo);
Error AppendHostAddresses(const ServiceInstanceInfo &aInstanceInfo);
Error AppendHostAddresses(AddrType aAddrType, const Ip6::Address *aAddrs, uint16_t aAddrsLength, uint32_t aTtl);
@ -592,6 +594,7 @@ private:
static const char kDefaultDomainName[];
static const char kSubLabel[];
static const char kMdnsDomainName[];
#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
static const char *kBlockedDomains[];
#endif

View File

@ -1041,6 +1041,20 @@ void TestProxyBasic(void)
const uint8_t kTxtData[] = {3, 'A', '=', '1', 0};
const uint8_t kKeyData[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
const uint8_t kCnameData[] = {
10, 'p', 'r', 'o', 't', 'e', 'c', 't', 'o', 'r', 's', /* protectors */
5, 'l', 'o', 'c', 'a', 'l', /* local */
0,
};
const uint8_t kTranslatedCnameData[] = {
10, 'p', 'r', 'o', 't', 'e', 'c', 't', 'o', 'r', 's', /* protectors */
7, 'd', 'e', 'f', 'a', 'u', 'l', 't', /* default */
7, 's', 'e', 'r', 'v', 'i', 'c', 'e', /* service */
4, 'a', 'r', 'p', 'a', /* arpa */
0,
};
Srp::Server *srpServer;
Srp::Client *srpClient;
Dns::Client *dnsClient;
@ -1758,6 +1772,85 @@ void TestProxyBasic(void)
VerifyOrQuit(!memcmp(sQueryRecordInfo.mRecords[0].mDataBuffer, kKeyData, sizeof(kKeyData)));
VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);
Log("--------------------------------------------------------------------------------------------");
ResetPlatDnssdApiInfo();
sQueryRecordInfo.Reset();
Log("QueryRecord() for CNAME that requires RDATA translation");
SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeCname, "avengers", "default.service.arpa.",
RecordCallback, sInstance));
AdvanceTime(10);
// Check that a record querier is started
VerifyOrQuit(sStartBrowserInfo.mCallCount == 0);
VerifyOrQuit(sStopBrowserInfo.mCallCount == 0);
VerifyOrQuit(sStartSrvResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopSrvResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartTxtResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopTxtResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartIp6AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopIp6AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartIp4AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopIp4AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartRecordQuerierInfo.mCallCount == 1);
VerifyOrQuit(sStopRecordQuerierInfo.mCallCount == 0);
VerifyOrQuit(sStartRecordQuerierInfo.NameMatches("avengers", nullptr));
VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 0);
Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
Log("Invoke Record Querier callback");
recordResult.mFirstLabel = "avengers";
recordResult.mNextLabels = nullptr;
recordResult.mRecordType = Dns::ResourceRecord::kTypeCname;
recordResult.mRecordData = kCnameData;
recordResult.mRecordDataLength = sizeof(kCnameData);
recordResult.mTtl = kTtl;
recordResult.mInfraIfIndex = kInfraIfIndex;
InvokeRecordQuerierCallback(sStartRecordQuerierInfo.mCallback, recordResult);
AdvanceTime(10);
// Check that the record querier is stopped
VerifyOrQuit(sStartBrowserInfo.mCallCount == 0);
VerifyOrQuit(sStopBrowserInfo.mCallCount == 0);
VerifyOrQuit(sStartSrvResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopSrvResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartTxtResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopTxtResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartIp6AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopIp6AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartIp4AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStopIp4AddrResolverInfo.mCallCount == 0);
VerifyOrQuit(sStartRecordQuerierInfo.mCallCount == 1);
VerifyOrQuit(sStopRecordQuerierInfo.mCallCount == 1);
VerifyOrQuit(sStopRecordQuerierInfo.NameMatches("avengers", nullptr));
VerifyOrQuit(sStopRecordQuerierInfo.mCallback == sStartRecordQuerierInfo.mCallback);
// Check that response is sent to client and validate
// that the CNAME RDATA is correctly translated.
VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
SuccessOrQuit(sQueryRecordInfo.mError);
VerifyOrQuit(!strcmp(sQueryRecordInfo.mQueryName, "avengers.default.service.arpa."));
VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1);
VerifyOrQuit(!strcmp(sQueryRecordInfo.mRecords[0].mNameBuffer, "avengers.default.service.arpa."));
VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypeCname);
VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength == sizeof(kTranslatedCnameData));
VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl == kTtl);
VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sizeof(kTranslatedCnameData));
VerifyOrQuit(!memcmp(sQueryRecordInfo.mRecords[0].mDataBuffer, kTranslatedCnameData, sizeof(kTranslatedCnameData)));
VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);
Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
Log("Stop DNS-SD server");