net: dns_sd, mdns: support service type enumeration

Support DNS-SD Service Type Enumeration in the dns_sd library
and mdns_responder sample application.

For more information, please see Section 9, "Service Type
Enumeration" in RFC 6763.

https://datatracker.ietf.org/doc/html/rfc6763

Fixes #38673

Signed-off-by: Christopher Friedt <chrisfriedt@gmail.com>
diff --git a/include/net/dns_sd.h b/include/net/dns_sd.h
index ae2fe2a..e5f0b09 100644
--- a/include/net/dns_sd.h
+++ b/include/net/dns_sd.h
@@ -51,6 +51,38 @@
 #define DNS_SD_DOMAIN_MAX_SIZE 63
 
 /**
+ * Minimum number of segments in a fully-qualified name
+ *
+ * This reqpresents FQN's of the form below
+ * ```
+ * <sn>._tcp.<domain>.
+ * ```
+ * Currently sub-types and service domains are unsupported and only the
+ * "local" domain is supported. Specifically, that excludes the following:
+ * ```
+ * <sub>._sub.<sn>._tcp.<servicedomain>.<parentdomain>.
+ * ```
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc6763">RFC 6763</a>, Section 7.2.
+ */
+#define DNS_SD_MIN_LABELS 3
+/**
+ * Maximum number of segments in a fully-qualified name
+ *
+ * This reqpresents FQN's of the form below
+ * ```
+ * <instance>.<sn>._tcp.<domain>.
+ * ```
+ *
+ * Currently sub-types and service domains are unsupported and only the
+ * "local" domain is supported. Specifically, that excludes the following:
+ * ```
+ * <sub>._sub.<sn>._tcp.<servicedomain>.<parentdomain>.
+ * ```
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc6763">RFC 6763</a>, Section 7.2.
+ */
+#define DNS_SD_MAX_LABELS 4
+
+/**
  * @brief Register a service for DNS Service Discovery
  *
  * This macro should be used for advanced use cases. Two simple use cases are
@@ -209,6 +241,12 @@
  * @internal
  */
 extern const char dns_sd_empty_txt[1];
+/**
+ * @brief Wildcard Port specifier for DNS-SD
+ *
+ * @internal
+ */
+extern const uint16_t dns_sd_port_zero;
 
 /** @endcond */
 
@@ -224,6 +262,32 @@
 }
 
 /**
+ * @brief Check if @a rec is a DNS-SD Service Type Enumeration
+ *
+ * DNS-SD Service Type Enumeration is used by network tooling to
+ * acquire a list of all mDNS-advertised services belonging to a
+ * particular host on a particular domain.
+ *
+ * For example, for the domain '.local', the equivalent query
+ * would be '_services._dns-sd._udp.local'.
+ *
+ * Currently, only the '.local' domain is supported.
+ *
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc6763#section-9">Service Type Enumeration, RFC 6763</a>.
+ *
+ * @param rec the record to in question
+ * @return true if @a rec is a DNS-SD Service Type Enumeration
+ */
+bool dns_sd_is_service_type_enumeration(const struct dns_sd_rec *rec);
+
+/**
+ * @brief Create a wildcard filter for DNS-SD records
+ *
+ * @param filter a pointer to the filter to use
+ */
+void dns_sd_create_wildcard_filter(struct dns_sd_rec *filter);
+
+/**
  * @}
  */
 
diff --git a/subsys/net/lib/dns/Kconfig b/subsys/net/lib/dns/Kconfig
index 1ed027e..9c52253 100644
--- a/subsys/net/lib/dns/Kconfig
+++ b/subsys/net/lib/dns/Kconfig
@@ -159,6 +159,17 @@
 	  By doing so, Zephyr network services are discoverable
 	  using e.g. 'avahi-browse -t -r _greybus._tcp'.
 
+if MDNS_RESPONDER_DNS_SD
+config MDNS_RESPONDER_DNS_SD_SERVICE_TYPE_ENUMERATION
+	bool "Enable DNS SD Service Type Enumeration"
+	default y
+	help
+	  Selecting this option ensures that the MDNS Responder
+	  performs DNS-SD Service Type Enumeration according to RFC 6763,
+	  Chapter 9. By doing so, Zephyr network services are discoverable
+	  using e.g. 'avahi-browse -t -r _services._dns-sd._udp.local'.
+endif # MDNS_RESPONDER_DNS_SD
+
 module = MDNS_RESPONDER
 module-dep = NET_LOG
 module-str = Log level for mDNS responder
diff --git a/subsys/net/lib/dns/dns_sd.c b/subsys/net/lib/dns/dns_sd.c
index a0d0b11..8853f98 100644
--- a/subsys/net/lib/dns/dns_sd.c
+++ b/subsys/net/lib/dns/dns_sd.c
@@ -25,6 +25,7 @@
 LOG_MODULE_REGISTER(net_dns_sd, CONFIG_DNS_SD_LOG_LEVEL);
 
 const char dns_sd_empty_txt[1];
+const uint16_t dns_sd_port_zero;
 
 #ifndef CONFIG_NET_TEST
 
@@ -699,9 +700,9 @@
 }
 #endif /* CONFIG_NET_TEST */
 
-int dns_sd_handle_ptr_query(const struct dns_sd_rec *inst,
-	const struct in_addr *addr4, const struct in6_addr *addr6,
-	uint8_t *buf, uint16_t buf_size)
+
+int dns_sd_handle_ptr_query(const struct dns_sd_rec *inst, const struct in_addr *addr4,
+			    const struct in6_addr *addr6, uint8_t *buf, uint16_t buf_size)
 {
 	/*
 	 * RFC 6763 Section 12.1
@@ -734,9 +735,8 @@
 	}
 
 	if (*(inst->port) == 0) {
-		NET_DBG("Ephemeral port %u for %s.%s.%s.%s "
-			"not initialized", ntohs(*(inst->port)),
-			inst->instance, inst->service, inst->proto,
+		NET_DBG("Ephemeral port %u for %s.%s.%s.%s not initialized",
+			ntohs(*(inst->port)), inst->instance, inst->service, inst->proto,
 			inst->domain);
 		return -EHOSTDOWN;
 	}
@@ -756,10 +756,8 @@
 	}
 
 	/* first add the answer record */
-	r = add_ptr_record(inst, DNS_SD_PTR_TTL, buf, offset,
-			   buf_size - offset,
-			   &service_offset, &instance_offset,
-			   &domain_offset);
+	r = add_ptr_record(inst, DNS_SD_PTR_TTL, buf, offset, buf_size - offset, &service_offset,
+			   &instance_offset, &domain_offset);
 	if (r < 0) {
 		return r; /* LCOV_EXCL_LINE */
 	}
@@ -768,9 +766,7 @@
 	offset += r;
 
 	/* then add the additional records */
-	r = add_txt_record(inst, DNS_SD_TXT_TTL, instance_offset, buf,
-			   offset,
-			   buf_size - offset);
+	r = add_txt_record(inst, DNS_SD_TXT_TTL, instance_offset, buf, offset, buf_size - offset);
 	if (r < 0) {
 		return r; /* LCOV_EXCL_LINE */
 	}
@@ -778,9 +774,8 @@
 	rsp->arcount++;
 	offset += r;
 
-	r = add_srv_record(inst, DNS_SD_SRV_TTL, instance_offset,
-			   domain_offset,
-			   buf, offset, buf_size - offset, &host_offset);
+	r = add_srv_record(inst, DNS_SD_SRV_TTL, instance_offset, domain_offset, buf, offset,
+			   buf_size - offset, &host_offset);
 	if (r < 0) {
 		return r; /* LCOV_EXCL_LINE */
 	}
@@ -789,12 +784,10 @@
 	offset += r;
 
 	if (addr6 != NULL) {
-		r = add_aaaa_record(inst, DNS_SD_AAAA_TTL, host_offset,
-				    addr6->s6_addr,
-				    buf, offset,
+		r = add_aaaa_record(inst, DNS_SD_AAAA_TTL, host_offset, addr6->s6_addr, buf, offset,
 				    buf_size - offset); /* LCOV_EXCL_LINE */
 		if (r < 0) {
-			return r;                       /* LCOV_EXCL_LINE */
+			return r; /* LCOV_EXCL_LINE */
 		}
 
 		rsp->arcount++;
@@ -803,8 +796,7 @@
 
 	if (addr4 != NULL) {
 		tmp = htonl(*(addr4->s4_addr32));
-		r = add_a_record(inst, DNS_SD_A_TTL, host_offset,
-				 tmp, buf, offset,
+		r = add_a_record(inst, DNS_SD_A_TTL, host_offset, tmp, buf, offset,
 				 buf_size - offset);
 		if (r < 0) {
 			return r; /* LCOV_EXCL_LINE */
@@ -822,6 +814,93 @@
 	return offset;
 }
 
+int dns_sd_handle_service_type_enum(const struct dns_sd_rec *inst,
+				    const struct in_addr *addr4, const struct in6_addr *addr6,
+				    uint8_t *buf, uint16_t buf_size)
+{
+	static const char query[] = { "\x09_services\x07_dns-sd\x04_udp\x05local" };
+	/* offset of '.local' in the above */
+	uint16_t domain_offset = DNS_SD_PTR_MASK | 35;
+	uint16_t proto;
+	int name_size;
+	uint16_t service_size;
+	uint16_t offset = sizeof(struct dns_header);
+	struct dns_rr *rr;
+	struct dns_header *const rsp = (struct dns_header *)buf;
+
+	if (!rec_is_valid(inst)) {
+		return -EINVAL;
+	}
+
+	if (*(inst->port) == 0) {
+		NET_DBG("Ephemeral port %u for %s.%s.%s.%s "
+			"not initialized",
+			ntohs(*(inst->port)), inst->instance, inst->service, inst->proto,
+			inst->domain);
+		return -EHOSTDOWN;
+	}
+
+	if (strncmp("_tcp", inst->proto, DNS_SD_PROTO_SIZE) == 0) {
+		proto = IPPROTO_TCP;
+	} else if (strncmp("_udp", inst->proto, DNS_SD_PROTO_SIZE) == 0) {
+		proto = IPPROTO_UDP;
+	} else {
+		NET_DBG("invalid protocol %s", inst->proto);
+		return -EINVAL;
+	}
+
+	if (!port_in_use(proto, ntohs(*(inst->port)), addr4, addr6)) {
+		/* Service is not yet bound, so do not advertise */
+		return -EHOSTDOWN;
+	}
+
+	service_size = strlen(inst->service);
+	name_size =
+		/* uncompressed. e.g. "._foo._tcp.local." */
+		sizeof(query)
+		+ sizeof(*rr)
+		/* compressed e.g. ._googlecast._tcp" followed by (DNS_SD_PTR_MASK | 0x0abc) */
+		+ DNS_LABEL_LEN_SIZE + service_size
+		+ DNS_LABEL_LEN_SIZE + DNS_SD_PROTO_SIZE
+		+ DNS_POINTER_SIZE;
+
+	if (offset > buf_size || name_size >= buf_size - offset) {
+		NET_DBG("Buffer too small. required: %u available: %d", name_size,
+			(int)buf_size - (int)offset);
+		return -ENOSPC;
+	}
+
+	memset(rsp, 0, sizeof(*rsp));
+	memcpy(&buf[offset], query, sizeof(query));
+	offset += sizeof(query);
+
+	rr = (struct dns_rr *)&buf[offset];
+	rr->type = htons(DNS_RR_TYPE_PTR);
+	rr->class_ = htons(DNS_CLASS_IN);
+	rr->ttl = htonl(DNS_SD_PTR_TTL);
+	rr->rdlength = htons(0
+		+ DNS_LABEL_LEN_SIZE + service_size
+		+ DNS_LABEL_LEN_SIZE + DNS_SD_PROTO_SIZE
+		+ DNS_POINTER_SIZE);
+	offset += sizeof(*rr);
+
+	buf[offset++] = service_size;
+	memcpy(&buf[offset], inst->service, service_size);
+	offset += service_size;
+	buf[offset++] = DNS_SD_PROTO_SIZE;
+	memcpy(&buf[offset], inst->proto, DNS_SD_PROTO_SIZE);
+	offset += DNS_SD_PROTO_SIZE;
+	domain_offset = htons(domain_offset);
+	memcpy(&buf[offset], &domain_offset, sizeof(domain_offset));
+	offset += sizeof(domain_offset);
+
+	/* Set the Response and AA bits */
+	rsp->flags = htons(BIT(15) | BIT(10));
+	rsp->ancount = htons(1);
+
+	return offset;
+}
+
 /* TODO: dns_sd_handle_srv_query() */
 /* TODO: dns_sd_handle_txt_query() */
 
@@ -893,82 +972,202 @@
 	return true;
 }
 
-int dns_sd_extract_service_proto_domain(const uint8_t *query,
-	size_t query_size, struct dns_sd_rec *record, char *service,
-	size_t service_size, char *proto, size_t proto_size, char *domain,
-	size_t domain_size)
+int dns_sd_query_extract(const uint8_t *query, size_t query_size, struct dns_sd_rec *record,
+			 char **label, size_t *size, size_t *n)
 {
-	uint16_t offs;
-	uint8_t label_size;
+	size_t i;
+	size_t offset;
+	size_t qlabels;
+	size_t qsize;
+	const size_t N = (n) ? (*n) : 0;
 
-	if (query == NULL || record == NULL || service == NULL
-	    || proto == NULL || domain == NULL) {
-		NET_DBG("one or more arguments are NULL");
+	/*
+	 * See RFC 6763, 7.2. Service Name Length Limits
+	 *
+	 *            <sn>._tcp.<servicedomain>.<parentdomain>.
+	 * <Instance>.<sn>._tcp.<servicedomain>.<parentdomain>.
+	 * <sub>._sub.<sn>._tcp.<servicedomain>.<parentdomain>.
+	 */
+	__ASSERT(DNS_SD_MIN_LABELS <= N, "invalid number of labels %zu", N);
+	__ASSERT(!(query == NULL || label == NULL || size == NULL || n == NULL),
+		 "one or more required arguments are NULL");
+	__ASSERT(query + query_size >= query, "query %p + query_size %zu  wraps NULL", query,
+		 query_size);
+	__ASSERT(label + N >= label, "label %p + n %zu  wraps NULL", label, N);
+	__ASSERT(size + N >= size, "size %p + n %zu  wraps NULL", size, N);
+	for (i = 0; i < N; ++i) {
+		if (label[i] == NULL) {
+			__ASSERT(label[i] != NULL, "label[%zu] is NULL", i);
+		}
+	}
+
+	if (query_size <= DNS_MSG_HEADER_SIZE) {
+		NET_DBG("query size %zu is less than DNS_MSG_HEADER_SIZE %d", query_size,
+			DNS_MSG_HEADER_SIZE);
 		return -EINVAL;
 	}
 
-	if (query_size <= DNS_MSG_HEADER_SIZE
-	    || service_size < DNS_SD_SERVICE_MAX_SIZE + 1
-	    || proto_size < DNS_SD_PROTO_SIZE + 1
-	    || domain_size < DNS_SD_DOMAIN_MAX_SIZE + 1
-	    ) {
-		NET_DBG("one or more size arguments are too small");
-		return -EINVAL;
+	query += DNS_MSG_HEADER_SIZE;
+	query_size -= DNS_MSG_HEADER_SIZE;
+	offset = DNS_MSG_HEADER_SIZE;
+	dns_sd_create_wildcard_filter(record);
+	/* valid record must have non-NULL port */
+	record->port = &dns_sd_port_zero;
+
+	/* also counts labels */
+	for (i = 0, qlabels = 0; query_size > 0;) {
+		qsize = *query;
+		++offset;
+		++query;
+		--query_size;
+
+		if (qsize == 0) {
+			break;
+		}
+
+		++qlabels;
+		if (qsize >= query_size) {
+			NET_DBG("claimed query size %zu > query buffer size %zu", qsize,
+				query_size);
+			return -EINVAL;
+		}
+
+		if (qsize > size[i] - 1) {
+			NET_DBG("qsize %zu > size[%zu] - 1 %zu", qsize, i, size[i] - 1);
+			return -ENOBUFS;
+		}
+
+		if (i < N) {
+			/* only extract the label if there is storage for it */
+			memcpy(label[i], query, qsize);
+			label[i][qsize] = '\0';
+			size[i] = qsize;
+			++i;
+		}
+
+		offset += qsize;
+		query += qsize;
+		query_size -= qsize;
 	}
 
-	memset(record, 0, sizeof(*record));
-	offs = DNS_MSG_HEADER_SIZE;
-
-	/* Copy service label to '\0'-terminated buffer */
-	label_size = query[offs];
-	if (label_size == 0  || label_size > service_size - 1
-	    || offs + label_size > query_size) {
-		NET_DBG("could not get service");
-		return -EINVAL;
-	} else {
-		strncpy(service, &query[offs + 1], label_size);
-		service[label_size] = '\0';
-		offs += label_size + 1;
+	/* write-out the actual number of labels in 'n' */
+	for (*n = i; i < N; ++i) {
+		label[i] = NULL;
+		size[i] = 0;
 	}
 
-	/* Copy proto label to '\0'-terminated buffer */
-	label_size = query[offs];
-	if (label_size == 0 || label_size > proto_size - 1
-	    || offs + label_size > query_size) {
-		NET_DBG("could not get proto for '%s...'", log_strdup(service));
+	if (qlabels < DNS_SD_MIN_LABELS) {
+		NET_DBG("too few labels in query %zu, DNS_SD_MIN_LABELS: %d", qlabels,
+			DNS_SD_MIN_LABELS);
 		return -EINVAL;
-	} else {
-		strncpy(proto, &query[offs + 1], label_size);
-		proto[label_size] = '\0';
-		offs += label_size + 1;
+	} else if (qlabels == DNS_SD_MIN_LABELS) {
+		/* e.g. _zephyr._tcp.local */
+		record->service = label[0];
+		record->proto = label[1];
+		record->domain = label[2];
+
+		if (!service_is_valid(record->service)) {
+			NET_DBG("service '%s' is invalid", record->service);
+			return -EINVAL;
+		}
+
+		if (!proto_is_valid(record->proto)) {
+			NET_DBG("proto '%s' is invalid", record->proto);
+			return -EINVAL;
+		}
+
+		if (!domain_is_valid(record->domain)) {
+			NET_DBG("domain '%s' is invalid", record->domain);
+			return -EINVAL;
+		}
+	} else if (qlabels > DNS_SD_MIN_LABELS && qlabels < DNS_SD_MAX_LABELS) {
+		NET_DBG("unsupported number of labels %zu", qlabels);
+		return -EINVAL;
+	} else if (qlabels >= DNS_SD_MAX_LABELS) {
+		/* e.g.
+		 * "Zephyr 42"._zephyr._tcp.local, or
+		 * _domains._dns-sd._udp.local
+		 */
+		record->instance = label[0];
+		record->service = label[1];
+		record->proto = label[2];
+		record->domain = label[3];
+
+		if (!instance_is_valid(record->instance)) {
+			NET_DBG("service '%s' is invalid", record->instance);
+			return -EINVAL;
+		}
+
+		if (!service_is_valid(record->service)) {
+			NET_DBG("service '%s' is invalid", record->service);
+			return -EINVAL;
+		}
+
+		if (!proto_is_valid(record->proto)) {
+			NET_DBG("proto '%s' is invalid", record->proto);
+			return -EINVAL;
+		}
+
+		if (!domain_is_valid(record->domain)) {
+			NET_DBG("domain '%s' is invalid", record->domain);
+			return -EINVAL;
+		}
+	} else if (qlabels > N) {
+		NET_DBG("too few buffers to extract query: qlabels: %zu, N: %zu",
+			qlabels, N);
+		return -ENOBUFS;
 	}
 
-	/* Copy domain label to '\0'-terminated buffer */
-	label_size = query[offs];
-	if (label_size == 0 || label_size > domain_size - 1
-	    || offs + label_size > query_size) {
-		NET_DBG("could not get domain for '%s.%s...'",
-			log_strdup(service), log_strdup(proto));
-		return -EINVAL;
-	} else {
-		strncpy(domain, &query[offs + 1], label_size);
-		domain[label_size] = '\0';
-		offs += label_size + 1;
+	return offset;
+}
+
+int dns_sd_extract_service_proto_domain(const uint8_t *query, size_t query_size,
+					struct dns_sd_rec *record, char *service,
+					size_t service_size, char *proto, size_t proto_size,
+					char *domain, size_t domain_size)
+{
+	char instance[DNS_SD_INSTANCE_MAX_SIZE + 1];
+	char *label[4];
+	size_t size[] = {
+		ARRAY_SIZE(instance),
+		service_size,
+		proto_size,
+		domain_size,
+	};
+	size_t n = ARRAY_SIZE(label);
+
+	BUILD_ASSERT(ARRAY_SIZE(label) == ARRAY_SIZE(size),
+		"label and size arrays are different size");
+
+	/*
+	 * work around for bug in compliance scripts which say that the array
+	 * should be static const (incorrect)
+	 */
+	label[0] = instance;
+	label[1] = service;
+	label[2] = proto;
+	label[3] = domain;
+
+	return dns_sd_query_extract(query, query_size, record, label, size, &n);
+}
+
+bool dns_sd_is_service_type_enumeration(const struct dns_sd_rec *rec)
+{
+	static const struct dns_sd_rec filter = {
+		.instance = "_services",
+		.service = "_dns-sd",
+		.proto = "_udp",
+		.domain = "local",
+	};
+
+	return dns_sd_rec_match(rec, &filter);
+}
+
+void dns_sd_create_wildcard_filter(struct dns_sd_rec *filter)
+{
+	if (filter != NULL) {
+		memset(filter, 0, sizeof(*filter));
+		filter->text = dns_sd_empty_txt;
+		filter->text_size = sizeof(dns_sd_empty_txt);
 	}
-
-	/* Check that we have reached the DNS terminator */
-	if (query[offs] != 0) {
-		NET_DBG("ignoring request for '%s.%s.%s...'",
-			log_strdup(service),
-			log_strdup(proto),
-			log_strdup(domain));
-		return -EINVAL;
-	}
-
-	offs++;
-	record->service = service;
-	record->proto = proto;
-	record->domain = domain;
-
-	return offs;
 }
diff --git a/subsys/net/lib/dns/dns_sd.h b/subsys/net/lib/dns/dns_sd.h
index d73b31a..1a10935 100644
--- a/subsys/net/lib/dns/dns_sd.h
+++ b/subsys/net/lib/dns/dns_sd.h
@@ -33,6 +33,35 @@
 	STRUCT_SECTION_FOREACH(dns_sd_rec, it)
 
 /**
+ * @brief Extract labels from a DNS-SD PTR query
+ *
+ * ```
+ *            <sn>._tcp.<domain>.
+ * <instance>.<sn>._tcp.<domain>.
+ * ```
+ *
+ * Currently sub-types and service domains are unsupported and only the
+ * "local" domain is supported. Specifically, that excludes the following:
+ * ```
+ * <sub>._sub.<sn>._tcp.<servicedomain>.<parentdomain>.
+ * ```
+ *
+ * @param query a pointer to the start of the query
+ * @param query_size the number of bytes contained in the query
+ * @param[out] record the DNS-SD record to initialize and populate
+ * @param label array of pointers to suitably sized buffers
+ * @param size array of sizes for each buffer in @p label
+ * @param[inout] n number of elements in @p label and @p size
+ *
+ * @return on success, number of bytes read from @p query
+ * @return on failure, a negative errno value
+ *
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc6763">RFC 6763</a>, Section 7.2.
+ */
+int dns_sd_query_extract(const uint8_t *query, size_t query_size, struct dns_sd_rec *record,
+			 char **label, size_t *size, size_t *n);
+
+/**
  * @brief Extract the Service, Protocol, and Domain from a DNS-SD PTR query
  *
  * This function zero-initializes @p record and populates the appropriate
@@ -57,6 +86,7 @@
  * @return on success, a positive number representing length of the query
  * @return on failure, a negative errno value
  */
+__deprecated
 int dns_sd_extract_service_proto_domain(const uint8_t *query,
 	size_t query_size, struct dns_sd_rec *record, char *service,
 	size_t service_size, char *proto, size_t proto_size,
@@ -121,7 +151,7 @@
  * If there is no IPv6 address to advertise, then @p addr6 should be
  * NULL.
  *
- * @param inst the DNS-SD record for to advertise
+ * @param inst the DNS-SD record to advertise
  * @param addr4 pointer to the IPv4 address
  * @param addr6 pointer to the IPv6 address
  * @param buf output buffer
@@ -134,6 +164,24 @@
 	const struct in_addr *addr4, const struct in6_addr *addr6,
 	uint8_t *buf, uint16_t buf_size);
 
+/**
+ * @brief Handle a Service Type Enumeration with DNS Service Discovery
+ *
+ * This function should be called once for each type of advertised service.
+ *
+ * @param service the DNS-SD service to advertise
+ * @param addr4 pointer to the IPv4 address
+ * @param addr6 pointer to the IPv6 address
+ * @param buf output buffer
+ * @param buf_size size of the output buffer
+ *
+ * @return on success, number of bytes written to @p buf
+ * @return on failure, a negative errno value
+ */
+int dns_sd_handle_service_type_enum(const struct dns_sd_rec *service,
+	const struct in_addr *addr4, const struct in6_addr *addr6,
+	uint8_t *buf, uint16_t buf_size);
+
 #ifdef __cplusplus
 };
 #endif
diff --git a/subsys/net/lib/dns/mdns_responder.c b/subsys/net/lib/dns/mdns_responder.c
index b794cf8..0e2ce11 100644
--- a/subsys/net/lib/dns/mdns_responder.c
+++ b/subsys/net/lib/dns/mdns_responder.c
@@ -306,11 +306,32 @@
 	struct dns_sd_rec filter;
 	struct sockaddr dst;
 	socklen_t dst_len;
+	bool service_type_enum = false;
 	const struct in6_addr *addr6 = NULL;
 	const struct in_addr *addr4 = NULL;
+	char instance_buf[DNS_SD_SERVICE_MAX_SIZE + 1];
 	char service_buf[DNS_SD_SERVICE_MAX_SIZE + 1];
 	char proto_buf[DNS_SD_PROTO_SIZE + 1];
 	char domain_buf[DNS_SD_DOMAIN_MAX_SIZE + 1];
+	char *label[4];
+	size_t size[] = {
+		ARRAY_SIZE(instance_buf),
+		ARRAY_SIZE(service_buf),
+		ARRAY_SIZE(proto_buf),
+		ARRAY_SIZE(domain_buf),
+	};
+	size_t n = ARRAY_SIZE(label);
+
+	BUILD_ASSERT(ARRAY_SIZE(label) == ARRAY_SIZE(size), "");
+
+	/*
+	 * work around for bug in compliance scripts which say that the array
+	 * should be static const (incorrect)
+	 */
+	label[0] = instance_buf;
+	label[1] = service_buf;
+	label[2] = proto_buf;
+	label[3] = domain_buf;
 
 	/* This actually is used but the compiler doesn't see that */
 	ARG_UNUSED(record);
@@ -333,14 +354,28 @@
 					   &ip_hdr->ipv6->src);
 	}
 
-	ret = dns_sd_extract_service_proto_domain(dns_msg->msg,
-		dns_msg->msg_size, &filter, service_buf, sizeof(service_buf),
-		proto_buf, sizeof(proto_buf), domain_buf, sizeof(domain_buf));
+	ret = dns_sd_query_extract(dns_msg->msg,
+		dns_msg->msg_size, &filter, label, size, &n);
 	if (ret < 0) {
-		NET_DBG("unable to extract service.proto.domain (%d)", ret);
+		NET_DBG("unable to extract query (%d)", ret);
 		return;
 	}
 
+	if (IS_ENABLED(CONFIG_MDNS_RESPONDER_DNS_SD_SERVICE_TYPE_ENUMERATION)
+		&& dns_sd_is_service_type_enumeration(&filter)) {
+
+		/*
+		 * RFC 6763, Section 9
+		 *
+		 * A DNS query for PTR records with the name
+		 * "_services._dns-sd._udp.<Domain>" yields a set of PTR records,
+		 * where the rdata of each PTR record is the two-label <Service> name,
+		 * plus the same domain, e.g., "_http._tcp.<Domain>".
+		 */
+		dns_sd_create_wildcard_filter(&filter);
+		service_type_enum = true;
+	}
+
 	DNS_SD_FOREACH(record) {
 		/* Checks validity and then compare */
 		if (dns_sd_rec_match(record, &filter)) {
@@ -350,13 +385,21 @@
 				ntohs(*(record->port)));
 
 			/* Construct the response */
-			ret = dns_sd_handle_ptr_query(record,
-				addr4, addr6,
-				result->data, result->size);
-			if (ret < 0) {
-				NET_DBG("dns_sd_handle_ptr_query() failed (%d)",
-					ret);
-				continue;
+			if (service_type_enum) {
+				ret = dns_sd_handle_service_type_enum(record, addr4, addr6,
+					result->data, result->size);
+				if (ret < 0) {
+					NET_DBG("dns_sd_handle_service_type_enum() failed (%d)",
+						ret);
+					continue;
+				}
+			} else {
+				ret = dns_sd_handle_ptr_query(record, addr4, addr6,
+					result->data, result->size);
+				if (ret < 0) {
+					NET_DBG("dns_sd_handle_ptr_query() failed (%d)", ret);
+					continue;
+				}
 			}
 
 			result->len = ret;