mirror of
https://github.com/espressif/openthread.git
synced 2025-08-06 14:52:18 +08:00
[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:

committed by
GitHub

parent
819938d05d
commit
d6c35621bb
@ -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[] = {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
||||
|
Reference in New Issue
Block a user