[dnssd-server] determine query type & simplify processing of query name (#9349)

This commit simplifies and enhances the DNSSD Server implementation:

- A new method, `ParseQuestions()`, has been added to process the
  questions in a received `Request` message and determine the
  `QueryType`.

- The processing and appending of the query DNS name has been
  simplified. The query name is now read and copied label by label
  from the `Request` message into the `Response`. When matching the
  name against SRP entries, the names are compared with the query
  name directly as it is encoded in the `Response` message using the
  `Dns::Name::CompareName()` method. This approach is more flexible
  and simpler, and it works for all query types and name formats,
  including service instance names where the first label can itself
  contain a dot (`.`) character. This simplification allows us to
  remove the helper functions that were previously used to parse the
  query name and deal with the ambiguity that arises when service
  instance names are read as strings of dot-separated labels.

- The management of offsets for DNS name compression has also been
  simplified. A new method, `Response::ParseQueryName()`, has been
  added to validate the query name (e.g., that it contains the
  correct domain) and determine all offsets.

- The `ResolveBySrp()` method has also been simplified using query
  type and the newly added flavors of `Append{Ptr/Srv/Txt}Record()`
  methods.

- Finally, this commit adds the `Dns::Name::ExtractLabels()` helper
  method, which extracts label(s) from a full DNS name string by
  first checking that it contains a given suffix name (e.g., the
  suffix name can be a domain name or a service name) and then
  removing it, returning the label(s).
This commit is contained in:
Abtin Keshavarzian
2023-08-15 12:12:02 -07:00
committed by GitHub
parent 3b291108a9
commit 7e32165bee
6 changed files with 750 additions and 850 deletions
+30 -6
View File
@@ -166,11 +166,6 @@ exit:
}
Error Name::AppendMultipleLabels(const char *aLabels, Message &aMessage)
{
return AppendMultipleLabels(aLabels, kMaxNameLength, aMessage);
}
Error Name::AppendMultipleLabels(const char *aLabels, uint8_t aLength, Message &aMessage)
{
Error error = kErrorNone;
uint16_t index = 0;
@@ -181,7 +176,7 @@ Error Name::AppendMultipleLabels(const char *aLabels, uint8_t aLength, Message &
do
{
ch = index < aLength ? aLabels[index] : static_cast<char>(kNullChar);
ch = aLabels[index];
if ((ch == kNullChar) || (ch == kLabelSeparatorChar))
{
@@ -633,6 +628,35 @@ exit:
return error;
}
Error Name::ExtractLabels(const char *aName, const char *aSuffixName, char *aLabels, uint16_t aLabelsSize)
{
Error error = kErrorParse;
uint16_t nameLength = StringLength(aName, kMaxNameSize);
uint16_t suffixLength = StringLength(aSuffixName, kMaxNameSize);
const char *suffixStart;
VerifyOrExit(nameLength < kMaxNameSize);
VerifyOrExit(suffixLength < kMaxNameSize);
VerifyOrExit(nameLength > suffixLength);
suffixStart = aName + nameLength - suffixLength;
VerifyOrExit(StringMatch(suffixStart, aSuffixName, kStringCaseInsensitiveMatch));
suffixStart--;
VerifyOrExit(*suffixStart == kLabelSeparatorChar);
// Determine the labels length to copy
nameLength -= (suffixLength + 1);
VerifyOrExit(nameLength < aLabelsSize, error = kErrorNoBufs);
memcpy(aLabels, aName, nameLength);
aLabels[nameLength] = kNullChar;
error = kErrorNone;
exit:
return error;
}
bool Name::IsSubDomainOf(const char *aName, const char *aDomain)
{
bool match = false;
+21 -47
View File
@@ -686,26 +686,6 @@ public:
*/
static Error AppendLabel(const char *aLabel, Message &aMessage);
/**
* Encodes and appends a single name label of specified length to a message.
*
* The @p aLabel is assumed to contain a single name label of given @p aLength. @p aLabel must not contain
* '\0' characters within the length @p aLength. Unlike `AppendMultipleLabels()` which parses the label string
* and treats it as sequence of multiple (dot-separated) labels, this method always appends @p aLabel as a single
* whole label. This allows the label string to even contain dot '.' character, which, for example, is useful for
* "Service Instance Names" where <Instance> portion is a user-friendly name and can contain dot characters.
*
* @param[in] aLabel The label string to append. MUST NOT be `nullptr`.
* @param[in] aLength The length of the label to append.
* @param[in] aMessage The message to append to.
*
* @retval kErrorNone Successfully encoded and appended the name label to @p aMessage.
* @retval kErrorInvalidArgs @p aLabel is not valid (e.g., label length is not within valid range).
* @retval kErrorNoBufs Insufficient available buffers to grow the message.
*
*/
static Error AppendLabel(const char *aLabel, uint8_t aLength, Message &aMessage);
/**
* Encodes and appends a sequence of name labels to a given message.
*
@@ -728,33 +708,6 @@ public:
*/
static Error AppendMultipleLabels(const char *aLabels, Message &aMessage);
/**
* Encodes and appends a sequence of name labels within the specified length to a given message.
* Stops appending labels if @p aLength characters are read or '\0' is found before @p aLength
* characters.
*
* Is useful for appending a number of labels of the name instead of appending all labels.
*
* The @p aLabels must follow "<label1>.<label2>.<label3>", i.e., a sequence of labels separated by dot '.' char.
* E.g., "_http._tcp", "_http._tcp." (same as previous one), "host-1.test".
*
* Validates that the @p aLabels is a valid name format, i.e., no empty label, and labels are
* `kMaxLabelLength` (63) characters or less.
*
* @note This method NEVER adds a label terminator (empty label) to the message, even in the case where @p aLabels
* ends with a dot character, e.g., "host-1.test." is treated same as "host-1.test".
*
* @param[in] aLabels A name label string. Can be `nullptr` (then treated as "").
* @param[in] aLength The max length of the name labels to encode.
* @param[in] aMessage The message to which to append the encoded name.
*
* @retval kErrorNone Successfully encoded and appended the name label(s) to @p aMessage.
* @retval kErrorInvalidArgs Name label @p aLabels is not valid.
* @retval kErrorNoBufs Insufficient available buffers to grow the message.
*
*/
static Error AppendMultipleLabels(const char *aLabels, uint8_t aLength, Message &aMessage);
/**
* Appends a name label terminator to a message.
*
@@ -977,6 +930,25 @@ public:
*/
static Error CompareName(const Message &aMessage, uint16_t &aOffset, const Name &aName);
/**
* Extracts label(s) from a full name by checking that it contains a given suffix name (e.g., suffix name can be
* a domain name) and removing it.
*
* Both @p aName and @p aSuffixName must be full DNS name and end with ('.'), otherwise the behavior of this method
* is undefined.
*
* @param[in] aName The full name to extract labels from.
* @param[in] aSuffixName The suffix name (e.g. can be domain name).
* @param[out] aLabels Pointer to buffer to copy the extracted labels.
* @param[in] aLabelsSize Size of @p aLabels buffer.
*
* @retval kErrorNone Successfully extracted the labels, @p aLabels is updated.
* @retval kErrorParse @p aName does not contain @p aSuffixName.
* @retval kErrorNoBufs Could not fit the labels in @p aLabelsSize.
*
*/
static Error ExtractLabels(const char *aName, const char *aSuffixName, char *aLabels, uint16_t aLabelsSize);
/**
* Tests if a DNS name is a sub-domain of a given domain.
*
@@ -1056,6 +1028,8 @@ private:
{
}
static Error AppendLabel(const char *aLabel, uint8_t aLength, Message &aMessage);
const char *mString; // String containing the name or `nullptr` if name is not from string.
const Message *mMessage; // Message containing the encoded name, or `nullptr` if `Name` is not from message.
uint16_t mOffset; // Offset in `mMessage` to the start of name (used when name is from `mMessage`).
File diff suppressed because it is too large Load Diff
+78 -140
View File
@@ -291,160 +291,99 @@ public:
void SetTestMode(uint8_t aTestMode) { mTestMode = aTestMode; }
private:
class NameCompressInfo : public Clearable<NameCompressInfo>
{
public:
static constexpr uint16_t kUnknownOffset = 0; // Unknown offset value (used when offset is not yet set).
NameCompressInfo(void) { Clear(); }
uint16_t GetDomainNameOffset(void) const { return mDomainNameOffset; }
void SetDomainNameOffset(uint16_t aOffset) { mDomainNameOffset = aOffset; }
uint16_t GetServiceNameOffset(const Message &aMessage, const char *aServiceName) const
{
return MatchCompressedName(aMessage, mServiceNameOffset, aServiceName)
? mServiceNameOffset
: static_cast<uint16_t>(kUnknownOffset);
};
void SetServiceNameOffset(uint16_t aOffset)
{
if (mServiceNameOffset == kUnknownOffset)
{
mServiceNameOffset = aOffset;
}
}
uint16_t GetInstanceNameOffset(const Message &aMessage, const char *aName) const
{
return MatchCompressedName(aMessage, mInstanceNameOffset, aName) ? mInstanceNameOffset
: static_cast<uint16_t>(kUnknownOffset);
}
void SetInstanceNameOffset(uint16_t aOffset)
{
if (mInstanceNameOffset == kUnknownOffset)
{
mInstanceNameOffset = aOffset;
}
}
uint16_t GetHostNameOffset(const Message &aMessage, const char *aName) const
{
return MatchCompressedName(aMessage, mHostNameOffset, aName) ? mHostNameOffset
: static_cast<uint16_t>(kUnknownOffset);
}
void SetHostNameOffset(uint16_t aOffset)
{
if (mHostNameOffset == kUnknownOffset)
{
mHostNameOffset = aOffset;
}
}
private:
static bool MatchCompressedName(const Message &aMessage, uint16_t aOffset, const char *aName)
{
return aOffset != kUnknownOffset && Name::CompareName(aMessage, aOffset, aName) == kErrorNone;
}
uint16_t mDomainNameOffset; // Offset of domain name serialization into the response message.
uint16_t mServiceNameOffset; // Offset of service name serialization into the response message.
uint16_t mInstanceNameOffset; // Offset of instance name serialization into the response message.
uint16_t mHostNameOffset; // Offset of host name serialization into the response message.
};
static constexpr bool kBindUnspecifiedNetif = OPENTHREAD_CONFIG_DNSSD_SERVER_BIND_UNSPECIFIED_NETIF;
static constexpr uint8_t kProtocolLabelLength = 4;
static constexpr uint8_t kSubTypeLabelLength = 4;
static constexpr uint16_t kMaxConcurrentQueries = 32;
static constexpr uint16_t kMaxConcurrentUpstreamQueries = 32;
// This structure represents the splitting information of a full name.
struct NameComponentsOffsetInfo
typedef Header::Response ResponseCode;
typedef char DnsName[Name::kMaxNameSize];
typedef char DnsLabel[Name::kMaxLabelSize];
enum QueryType : uint8_t
{
static constexpr uint8_t kNotPresent = 0xff; // Indicates the component is not present.
kPtrQuery,
kSrvQuery,
kTxtQuery,
kSrvTxtQuery,
kAaaaQuery,
};
explicit NameComponentsOffsetInfo(void)
: mDomainOffset(kNotPresent)
, mProtocolOffset(kNotPresent)
, mServiceOffset(kNotPresent)
, mSubTypeOffset(kNotPresent)
, mInstanceOffset(kNotPresent)
{
}
bool IsServiceInstanceName(void) const { return mInstanceOffset != kNotPresent; }
bool IsServiceName(void) const { return mServiceOffset != kNotPresent && mInstanceOffset == kNotPresent; }
bool IsHostName(void) const { return mProtocolOffset == kNotPresent && mDomainOffset != 0; }
uint8_t mDomainOffset; // Offset to <Domain>.
uint8_t mProtocolOffset; // Offset to <Protocol> (i.e. _tcp or _udp) or `kNotPresent` if not service name.
uint8_t mServiceOffset; // Offset to <Service> or `kNotPresent` if not service or instance.
uint8_t mSubTypeOffset; // Offset to sub-type label or `kNotPresent` is not a sub-type.
uint8_t mInstanceOffset; // Offset to <Instance> or `kNotPresent` if the name is not a instance.
enum Section : uint8_t
{
kAnswerSection,
kAdditionalDataSection,
};
struct Request
{
ResponseCode ParseQuestions(uint8_t aTestMode);
const Message *mMessage;
const Ip6::MessageInfo *mMessageInfo;
Header mHeader;
QueryType mType;
};
struct Response : public GetProvider<Response>
class Response : public GetProvider<Response>, public Clearable<Response>
{
Response(void)
: mMessage(nullptr)
, mAdditional(false)
{
}
Instance &GetInstance(void) const { return mMessage->GetInstance(); }
Error AddQuestionsFrom(const Request &aRequest);
Error AppendQuestion(const char *aName, const Question &aQuestion);
Error AppendPtrRecord(const char *aServiceName, const char *aInstanceName, uint32_t aTtl);
Error AppendSrvRecord(const char *aInstanceName,
const char *aHostName,
uint32_t aTtl,
uint16_t aPriority,
uint16_t aWeight,
uint16_t aPort);
Error AppendTxtRecord(const char *aInstanceName, const void *aTxtData, uint16_t aTxtLength, uint32_t aTtl);
Error AppendAaaaRecord(const char *aHostName, const Ip6::Address &aAddress, uint32_t aTtl);
Error AppendServiceName(const char *aName);
Error AppendInstanceName(const char *aName);
Error AppendHostName(const char *aName);
void IncResourceRecordCount(void);
bool HasQuestion(const char *aName, uint16_t aQuestionType) const;
void Send(const Ip6::MessageInfo &aMessageInfo);
void GetQueryTypeAndName(DnsQueryType &aType, char (&aName)[Name::kMaxNameSize]) const;
public:
Response(void) { Clear(); }
Instance &GetInstance(void) const { return mMessage->GetInstance(); }
void SetResponseCode(ResponseCode aResponseCode) { mHeader.SetResponseCode(aResponseCode); }
ResponseCode AddQuestionsFrom(const Request &aRequest);
Error ParseQueryName(void);
void ReadQueryName(DnsName &aName) const;
bool QueryNameMatches(const char *aName) const;
Error AppendQueryName(void) const;
Error AppendPtrRecord(const char *aInstanceLabel, uint32_t aTtl);
Error AppendSrvRecord(const ServiceInstanceInfo &aInstanceInfo);
Error AppendSrvRecord(const char *aHostName,
uint32_t aTtl,
uint16_t aPriority,
uint16_t aWeight,
uint16_t aPort);
Error AppendTxtRecord(const ServiceInstanceInfo &aInstanceInfo);
Error AppendTxtRecord(const void *aTxtData, uint16_t aTxtLength, uint32_t aTtl);
Error AppendHostAddresses(const HostInfo &aHostInfo);
Error AppendHostAddresses(const ServiceInstanceInfo &aInstanceInfo);
Error AppendHostAddresses(const Ip6::Address *aAddrs, uint16_t aAddrsLength, uint32_t aTtl);
void UpdateRecordLength(ResourceRecord &aRecord, uint16_t aOffset) const;
void IncResourceRecordCount(void);
void Send(const Ip6::MessageInfo &aMessageInfo);
void GetQueryTypeAndName(DnsQueryType &aType, DnsName &aName) const;
#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
void ResolveBySrp(void);
void ResolveQuestionBySrp(const char *aName, const Question &aQuestion);
Error ResolveBySrp(void);
bool QueryNameMatchesService(const Srp::Server::Service &aService) const;
Error AppendSrvRecord(const Srp::Server::Service &aService);
Error AppendTxtRecord(const Srp::Server::Service &aService);
Error AppendHostAddresses(const Srp::Server::Host &aHost);
#endif
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
void Log(void) const;
static const char *QueryTypeToString(QueryType aType);
#endif
Message *mMessage;
Header mHeader;
NameCompressInfo mCompressInfo;
bool mAdditional; // Whether or not appending new records in additional data section.
Message *mMessage;
Header mHeader;
QueryType mType;
Section mSection;
uint16_t mDomainOffset;
uint16_t mServiceOffset;
uint16_t mInstanceOffset;
uint16_t mHostOffset;
};
struct QueryTransaction : public Response
{
bool IsValid(void) const { return mMessage != nullptr; }
bool CanAnswer(const char *aServiceFullName, const ServiceInstanceInfo &aInstanceInfo) const;
bool CanAnswer(const char *aHostFullName) const;
void Answer(const char *aServiceFullName, const ServiceInstanceInfo &aInstanceInfo);
void Answer(const char *aHostFullName, const HostInfo &aHostInfo);
void Finalize(Header::Response aResponseCode);
bool IsValid(void) const { return mMessage != nullptr; }
Error ExtractServiceInstanceLabel(const char *aInstanceName, DnsLabel &aLabel);
bool CanAnswer(const char *aServiceFullName, const ServiceInstanceInfo &aInstanceInfo) const;
bool CanAnswer(const char *aHostFullName) const;
void Answer(const ServiceInstanceInfo &aInstanceInfo);
void Answer(const HostInfo &aHostInfo);
void Finalize(Error aError);
Ip6::MessageInfo mMessageInfo;
TimeMilli mExpireTime;
@@ -452,12 +391,11 @@ private:
static constexpr uint32_t kQueryTimeout = OPENTHREAD_CONFIG_DNSSD_QUERY_TIMEOUT;
bool IsRunning(void) const { return mSocket.IsBound(); }
static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
void HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
void ProcessQuery(const Request &aRequest);
static Error FindNameComponents(const char *aName, const char *aDomain, NameComponentsOffsetInfo &aInfo);
static Error FindPreviousLabel(const char *aName, uint8_t &aStart, uint8_t &aStop);
bool IsRunning(void) const { return mSocket.IsBound(); }
static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
void HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
void ProcessQuery(Request &aRequest);
static uint8_t GetNameLength(const char *aName);
#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
static bool ShouldForwardToUpstream(const Request &aRequest);
@@ -472,14 +410,15 @@ private:
void HandleTimer(void);
void ResetTimer(void);
void UpdateResponseCounters(Header::Response aResponseCode);
void UpdateResponseCounters(ResponseCode aResponseCode);
using ServerTimer = TimerMilliIn<Server, &Server::HandleTimer>;
static const char kDnssdProtocolUdp[];
static const char kDnssdProtocolTcp[];
static const char kDnssdSubTypeLabel[];
static const char kDefaultDomainName[];
static const char kSubLabel[];
#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
static const char *kBlockedDomains[];
#endif
Ip6::Udp::Socket mSocket;
@@ -487,7 +426,6 @@ private:
Callback<SubscribeCallback> mQuerySubscribe;
Callback<UnsubscribeCallback> mQueryUnsubscribe;
static const char *kBlockedDomains[];
#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
bool mEnableUpstreamQuery;
UpstreamQueryTransaction mUpstreamQueryTransactions[kMaxConcurrentUpstreamQueries];
+53
View File
@@ -68,6 +68,8 @@ void TestDnsName(void)
const char *subDomain;
const char *domain;
const char *domain2;
const char *fullName;
const char *suffixName;
static const uint8_t kEncodedName1[] = {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0};
static const uint8_t kEncodedName2[] = {3, 'f', 'o', 'o', 1, 'a', 2, 'b', 'b', 3, 'e', 'd', 'u', 0};
@@ -247,6 +249,57 @@ void TestDnsName(void)
domain2 = ".example.com.";
VerifyOrQuit(!Dns::Name::IsSameDomain(domain, domain2));
printf("----------------------------------------------------------------\n");
printf("Extracting label(s) and removing domains:\n");
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = "default.service.arpa.";
SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)));
VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
fullName = "my.service._ipps._tcp.default.service.arpa.";
suffixName = "_ipps._tcp.default.service.arpa.";
SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)));
VerifyOrQuit(strcmp(name, "my.service") == 0);
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = "DeFault.SerVice.ARPA.";
SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)));
VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = "efault.service.arpa.";
VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)) == kErrorParse);
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = "xdefault.service.arpa.";
VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)) == kErrorParse);
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = ".default.service.arpa.";
VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)) == kErrorParse);
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = "default.service.arp.";
VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)) == kErrorParse);
fullName = "default.service.arpa.";
suffixName = "default.service.arpa.";
VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)) == kErrorParse);
fullName = "efault.service.arpa.";
suffixName = "default.service.arpa.";
VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, sizeof(name)) == kErrorParse);
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = "default.service.arpa.";
SuccessOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, 22));
VerifyOrQuit(strcmp(name, "my-service._ipps._tcp") == 0);
fullName = "my-service._ipps._tcp.default.service.arpa.";
suffixName = "default.service.arpa.";
VerifyOrQuit(Dns::Name::ExtractLabels(fullName, suffixName, name, 21) == kErrorNoBufs);
printf("----------------------------------------------------------------\n");
printf("Append names, check encoded bytes, parse name and read labels:\n");
+14 -4
View File
@@ -242,9 +242,10 @@ static const char kService1Name[] = "_srv._udp";
static const char kService1FullName[] = "_srv._udp.default.service.arpa.";
static const char kInstance1Label[] = "srv-instance";
static const char kService2Name[] = "_game._udp";
static const char kService2FullName[] = "_game._udp.default.service.arpa.";
static const char kInstance2Label[] = "last-ninja";
static const char kService2Name[] = "_game._udp";
static const char kService2FullName[] = "_game._udp.default.service.arpa.";
static const char kService2SubTypeFullName[] = "_best._sub._game._udp.default.service.arpa.";
static const char kInstance2Label[] = "last-ninja";
void PrepareService1(Srp::Client::Service &aService)
{
@@ -277,7 +278,7 @@ void PrepareService1(Srp::Client::Service &aService)
void PrepareService2(Srp::Client::Service &aService)
{
static const char kSub4[] = "_44444444";
static const char kSub4[] = "_best";
static const char *kSubLabels2[] = {kSub4, nullptr};
memset(&aService, 0, sizeof(aService));
@@ -598,6 +599,15 @@ void TestDnsClient(void)
SuccessOrQuit(sBrowseInfo.mError);
VerifyOrQuit(sBrowseInfo.mNumInstances == 1);
sBrowseInfo.Reset();
Log("Browse(%s)", kService2SubTypeFullName);
SuccessOrQuit(dnsClient->Browse(kService2SubTypeFullName, BrowseCallback, sInstance));
AdvanceTime(100);
VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
SuccessOrQuit(sBrowseInfo.mError);
VerifyOrQuit(sBrowseInfo.mNumInstances == 1);
sBrowseInfo.Reset();
Log("Browse() for unknown service");
SuccessOrQuit(dnsClient->Browse("_unknown._udp.default.service.arpa.", BrowseCallback, sInstance));