From b85aebba11f3fe70336edfe17b150bb9e9070d49 Mon Sep 17 00:00:00 2001
From: David Girault <dgirault@sevenhugs.com>
Date: Fri, 21 Apr 2017 12:05:47 +0200
Subject: [PATCH] mdns: added support for searching services

Two new API:
err_t mdns_search_service(const char *name, const char *service, enum mdns_sd_proto proto,
                          struct netif *netif, search_result_fn_t result_fn, void *arg,
                          s8_t *request_id);
void mdns_search_stop(s8_t request_id);

One compilation flags:
LWIP_MDNS_SEARCH

One options flags:
MDNS_MAX_REQUESTS

Some structure declarations moved to allow use by callback result function.

Result domain names are early uncompress before calling application callback
because it cannot be made by application itself.

Allow search services with multiples labels included, like '_services._dns-sd'.

Search for `_services._dns-sd._udp.local.` is handled in a special way.
Only `PTR` answers are send back to the application.

The `mdns_search_service()` function won't assert if no more space in `mdns_request`
table, just return an error if too many simultanous requests.
---
 src/apps/mdns/mdns.c                | 229 +++++++++++++++++++++++++---
 src/apps/mdns/mdns_domain.c         |  88 +++++++++++
 src/apps/mdns/mdns_out.c            |  56 +++++++
 src/include/lwip/apps/mdns.h        |  45 ++++++
 src/include/lwip/apps/mdns_domain.h |   5 +
 src/include/lwip/apps/mdns_opts.h   |  12 ++
 src/include/lwip/apps/mdns_out.h    |   3 +
 src/include/lwip/apps/mdns_priv.h   |  31 ++--
 8 files changed, 438 insertions(+), 31 deletions(-)

diff --git a/src/apps/mdns/mdns.c b/src/apps/mdns/mdns.c
index 30bf4e67..577a8a29 100644
--- a/src/apps/mdns/mdns.c
+++ b/src/apps/mdns/mdns.c
@@ -99,6 +99,9 @@ static const ip_addr_t v6group = DNS_MQUERY_IPV6_GROUP_INIT;
 
 #define MDNS_IP_TTL  255
 
+#if LWIP_MDNS_SEARCH
+static struct mdns_request mdns_requests[MDNS_MAX_REQUESTS];
+#endif
 
 static u8_t mdns_netif_client_id;
 static struct udp_pcb *mdns_pcb;
@@ -171,32 +174,12 @@ struct mdns_packet {
   u16_t additional_left;
 };
 
-/** Domain, type and class.
- *  Shared between questions and answers */
-struct mdns_rr_info {
-  struct mdns_domain domain;
-  u16_t type;
-  u16_t klass;
-};
-
 struct mdns_question {
   struct mdns_rr_info info;
   /** unicast reply requested */
   u16_t unicast;
 };
 
-struct mdns_answer {
-  struct mdns_rr_info info;
-  /** cache flush command bit */
-  u16_t cache_flush;
-  /* Validity time in seconds */
-  u32_t ttl;
-  /** Length of variable answer */
-  u16_t rd_length;
-  /** Offset of start of variable answer in packet */
-  u16_t rd_offset;
-};
-
 struct mdns_answer_list {
   u16_t offset[MDNS_PROBE_TIEBREAK_MAX_ANSWERS];
   u16_t size;
@@ -349,6 +332,45 @@ check_service(struct mdns_service *service, struct mdns_rr_info *rr)
   return replies;
 }
 
+#if LWIP_MDNS_SEARCH
+/**
+ * Check if question belong to a specified request
+ * @param request A ongoing MDNS request
+ * @param rr Domain/type/class from an answer
+ * @return Bitmask of which matching replies
+ */
+static int
+check_request(struct mdns_request *request, struct mdns_rr_info *rr)
+{
+  err_t res;
+  int replies = 0;
+  struct mdns_domain mydomain;
+
+  if (rr->klass != DNS_RRCLASS_IN && rr->klass != DNS_RRCLASS_ANY) {
+    /* Invalid class */
+    return 0;
+  }
+
+  res = mdns_build_request_domain(&mydomain, request, 0);
+  if (res == ERR_OK && mdns_domain_eq(&rr->domain, &mydomain) &&
+      (rr->type == DNS_RRTYPE_PTR || rr->type == DNS_RRTYPE_ANY)) {
+    /* Request for the instance of my service */
+    replies |= REPLY_SERVICE_TYPE_PTR;
+  }
+  res = mdns_build_request_domain(&mydomain, request, 1);
+  if (res == ERR_OK && mdns_domain_eq(&rr->domain, &mydomain)) {
+    /* Request for info about my service */
+    if (rr->type == DNS_RRTYPE_SRV || rr->type == DNS_RRTYPE_ANY) {
+      replies |= REPLY_SERVICE_SRV;
+    }
+    if (rr->type == DNS_RRTYPE_TXT || rr->type == DNS_RRTYPE_ANY) {
+      replies |= REPLY_SERVICE_TXT;
+    }
+  }
+  return replies;
+}
+#endif
+
 /**
  * Helper function for mdns_read_question/mdns_read_answer
  * Reads a domain, type and class from the packet
@@ -1687,6 +1709,26 @@ mdns_probe_conflict(struct netif *netif)
   }
 }
 
+
+/**
+ * Loockup matching request for response MDNS packet
+ */
+#if LWIP_MDNS_SEARCH
+static struct mdns_request *
+mdns_lookup_request(struct mdns_rr_info *rr)
+{
+  int i;
+  /* search originating request */
+  for (i = 0; i < MDNS_MAX_REQUESTS; i++) {
+    if ((mdns_requests[i].result_fn != NULL) &&
+        (check_request(&mdns_requests[i], rr) != 0)) {
+      return &mdns_requests[i];
+    }
+  }
+  return NULL;
+}
+#endif
+
 /**
  * Handle response MDNS packet:
  *  - Handle responses on probe query
@@ -1700,6 +1742,10 @@ mdns_handle_response(struct mdns_packet *pkt, struct netif *netif)
 {
   struct mdns_host* mdns = NETIF_TO_HOST(netif);
   u16_t total_answers_left;
+#if LWIP_MDNS_SEARCH
+  struct mdns_request *req = NULL;
+  s8_t first = 1;
+#endif
 
   /* Ignore responses with a source port different from 5353
    * (LWIP_IANA_PORT_MDNS) -> RFC6762 section 6 */
@@ -1711,12 +1757,16 @@ mdns_handle_response(struct mdns_packet *pkt, struct netif *netif)
   while (pkt->questions_left) {
     struct mdns_question q;
     err_t res;
-
     res = mdns_read_question(pkt, &q);
     if (res != ERR_OK) {
       LWIP_DEBUGF(MDNS_DEBUG, ("MDNS: Failed to parse question, skipping response packet\n"));
       return;
     }
+#if LWIP_MDNS_SEARCH
+    else {
+      req = mdns_lookup_request(&q.info);
+    }
+#endif
   }
   /* We need to check all resource record sections: answers, authoritative and additional */
   total_answers_left = pkt->answers_left + pkt->authoritative_left + pkt->additional_left;
@@ -1739,6 +1789,64 @@ mdns_handle_response(struct mdns_packet *pkt, struct netif *netif)
       continue;
     }
 
+#if LWIP_MDNS_SEARCH
+    if (req && req->only_ptr) {
+      /* Need to recheck that this answer match request that match previous answer */
+      if (memcmp (req->service.name, ans.info.domain.name, req->service.length) != 0)
+        req = NULL;
+    }
+    if (!req) {
+      /* Try hard to search matching request */
+      req = mdns_lookup_request(&ans.info);
+    }
+    if (req && req->result_fn) {
+      int flags = (first ? MDNS_SEARCH_RESULT_FIRST : 0) |
+          (!total_answers_left ? MDNS_SEARCH_RESULT_LAST : 0);
+      if (req->only_ptr) {
+          if (ans.info.type != DNS_RRTYPE_PTR)
+              continue; /* Ignore non matching answer type */
+          flags = MDNS_SEARCH_RESULT_FIRST | MDNS_SEARCH_RESULT_LAST;
+      }
+      u16_t offset;
+      struct pbuf *p = pbuf_skip(pkt->pbuf, ans.rd_offset, &offset);
+      if (ans.info.type == DNS_RRTYPE_PTR || ans.info.type == DNS_RRTYPE_SRV)
+      {
+        /* Those RR types have compressed domain name. Must uncompress here,
+           since cannot be done without pbuf. */
+        struct {
+          u16_t values[3];        /* SRV: Prio, Weight, Port */
+          struct mdns_domain dom; /* PTR & SRV: Domain (uncompressed) */
+        } data;
+        u16_t off = (ans.info.type == DNS_RRTYPE_SRV ? 6 : 0);
+        u16_t len = mdns_readname(pkt->pbuf, ans.rd_offset + off, &data.dom);
+        if (len == MDNS_READNAME_ERROR) {
+          /* Ensure result_fn is called anyway, just copy failed domain as is */
+          data.dom.length = ans.rd_length - off;
+          memcpy(&data.dom, (const char *)p->payload + offset + off, data.dom.length);
+        }
+        /* Adjust len/off according RR type */
+        if (ans.info.type == DNS_RRTYPE_SRV)
+        {
+          memcpy(&data, (const char *)p->payload + offset, 6);
+          len = data.dom.length + 6;
+          off = 0;
+        }
+        else
+        {
+          len = data.dom.length;
+          off = 6;
+        }
+        req->result_fn(&ans, (const char *)&data + off, len, flags, req->arg);
+      }
+      else
+      {
+        /* Direct call result_fn with varpart pointing in pbuf payload */
+        req->result_fn(&ans, (const char *)p->payload + offset, ans.rd_length, flags, req->arg);
+      }
+      first = 0;
+    }
+#endif
+
     /* "Conflicting Multicast DNS responses received *before* the first probe
      * packet is sent MUST be silently ignored" so drop answer if we haven't
      * started probing yet. */
@@ -2396,6 +2504,82 @@ mdns_resp_add_service_txtitem(struct mdns_service *service, const char *txt, u8_
   return mdns_domain_add_label(&service->txtdata, txt, txt_len);
 }
 
+#if LWIP_MDNS_SEARCH
+/**
+ * @ingroup mdns
+ * Stop a search request.
+ * @param req The search request to stop
+ */
+void
+mdns_search_stop(s8_t request_id)
+{
+  LWIP_ASSERT("mdns_search_stop: bad request_id", (request_id >= 0) && (request_id < MDNS_MAX_REQUESTS));
+  struct mdns_request *req = &mdns_requests[request_id];
+  if (req && req->result_fn)
+    req->result_fn = NULL;
+}
+
+/**
+ * @ingroup mdns
+ * Search a specific service on the network.
+ * @param name The name of the service
+ * @param service The service type, like "_http"
+ * @param proto The service protocol, DNSSD_PROTO_TCP for TCP ("_tcp") and DNSSD_PROTO_UDP
+ *              for others ("_udp")
+ * @param netif The network interface where to send search request
+ * @param result_fn Callback to send answer received. Will be called for each answer of a
+ *                  responce frame matching request sent
+ * @param arg Userdata pointer for result_fn
+ * @param request_id Returned request identifier to allow stop it.
+ * @return ERR_OK if the search request was created and sent, an err_t otherwise
+ */
+err_t
+mdns_search_service(const char *name, const char *service, enum mdns_sd_proto proto,
+                    struct netif *netif, search_result_fn_t result_fn, void *arg,
+                    s8_t *request_id)
+{
+  int i;
+  s8_t slot = -1;
+  struct mdns_request *req;
+  if (name)
+    LWIP_ERROR("mdns_search_service: Name too long", (strlen(name) <= MDNS_LABEL_MAXLEN), return ERR_VAL);
+  LWIP_ERROR("mdns_search_service: Service too long", (strlen(service) < MDNS_DOMAIN_MAXLEN), return ERR_VAL);
+  LWIP_ERROR("mdns_search_service: Bad reqid pointer", request_id, return ERR_VAL);
+  LWIP_ERROR("mdns_search_service: Bad proto (need TCP or UDP)", (proto == DNSSD_PROTO_TCP || proto == DNSSD_PROTO_UDP), return ERR_VAL);
+  for (i = 0; i < MDNS_MAX_REQUESTS; i++) {
+    if (mdns_requests[i].result_fn == NULL) {
+      slot = i;
+      break;
+    }
+  }
+  if (slot < 0)
+    /* Don't assert if no more space in mdns_request table. Just return an error. */
+    return ERR_MEM;
+
+  req = &mdns_requests[slot];
+  memset(req, 0, sizeof(struct mdns_request));
+  req->result_fn = result_fn;
+  req->arg = arg;
+  req->proto = (u16_t)proto;
+  req->qtype = DNS_RRTYPE_PTR;
+  if (proto == DNSSD_PROTO_UDP && strcmp(service, "_services._dns-sd") == 0)
+      req->only_ptr = 1; /* don't check other answers */
+  mdns_domain_add_string(&req->service, service);
+  if (name)
+    MEMCPY(&req->name, name, LWIP_MIN(MDNS_LABEL_MAXLEN, strlen(name)));
+  /* save request id (slot) in pointer provided by caller */
+  *request_id = slot;
+  /* now prepare a MDNS request and send it (on specified interface) */
+#if LWIP_IPV6
+  mdns_send_request(req, netif, IP6_ADDR_ANY);
+#endif
+#if LWIP_IPV4
+  mdns_send_request(req, netif, IP4_ADDR_ANY);
+#endif
+  return ERR_OK;
+}
+#endif
+
 /**
  * @ingroup mdns
  * Send unsolicited answer containing all our known data
@@ -2487,6 +2671,9 @@ mdns_resp_init(void)
 
   /* LWIP_ASSERT_CORE_LOCKED(); is checked by udp_new() */
 
+#if LWIP_MDNS_SEARCH
+  memset(mdns_requests, 0, sizeof(mdns_requests));
+#endif
   mdns_pcb = udp_new_ip_type(IPADDR_TYPE_ANY);
   LWIP_ASSERT("Failed to allocate pcb", mdns_pcb != NULL);
 #if LWIP_MULTICAST_TX_OPTIONS
diff --git a/src/apps/mdns/mdns_domain.c b/src/apps/mdns/mdns_domain.c
index 6d48939e..1893c739 100644
--- a/src/apps/mdns/mdns_domain.c
+++ b/src/apps/mdns/mdns_domain.c
@@ -136,6 +136,67 @@ mdns_domain_add_label_pbuf(struct mdns_domain *domain, const struct pbuf *p, u16
   return ERR_OK;
 }
 
+/**
+ * Add a partial domain to a domain
+ * @param domain The domain to add a label to
+ * @param source The domain to add, like &lt;\x09_services\007_dns-sd\000&gt;
+ * @return ERR_OK on success, an err_t otherwise if label too long
+ */
+err_t
+mdns_domain_add_domain(struct mdns_domain *domain, struct mdns_domain *source)
+{
+  u8_t len = source->length;
+  if (len > 0 && (1 + len + domain->length >= MDNS_DOMAIN_MAXLEN)) {
+    return ERR_VAL;
+  }
+  /* Allow only zero marker on last byte */
+  if (len == 0 && (1 + domain->length > MDNS_DOMAIN_MAXLEN)) {
+    return ERR_VAL;
+  }
+  if (len) {
+    /* Copy partial domain */
+    MEMCPY(&domain->name[domain->length], source->name, len);
+    domain->length += len;
+  }
+  else {
+    /* Add zero marker */
+    domain->name[domain->length] = len;
+    domain->length++;
+  }
+  return ERR_OK;
+}
+
+/**
+ * Add a string domain to a domain
+ * @param domain The domain to add a label to
+ * @param source The string to add, like &lt;_services._dns-sd&gt;
+ * @return ERR_OK on success, an err_t otherwise if label too long
+ */
+err_t
+mdns_domain_add_string(struct mdns_domain *domain, const char *source)
+{
+  u8_t *len = &domain->name[domain->length];
+  u8_t *end = &domain->name[MDNS_DOMAIN_MAXLEN];
+  u8_t *start = len + 1;
+  *len = 0;
+  while (*source && start < end) {
+      if (*source == '.') {
+        len = start++;
+        *len = 0;
+        source++;
+      }
+      else {
+        *start++ = *source++;
+        *len = *len + 1;
+      }
+  }
+  if (start == end)
+      return ERR_VAL;
+  domain->length = start - &domain->name[0];
+  return ERR_OK;
+}
+
+
 /**
  * Internal readname function with max 6 levels of recursion following jumps
  * while decompressing name
@@ -433,6 +494,33 @@ mdns_build_service_domain(struct mdns_domain *domain, struct mdns_service *servi
   return mdns_add_dotlocal(domain);
 }
 
+#if LWIP_MDNS_SEARCH
+/**
+ * Build domain name for a request
+ * @param domain Where to write the domain name
+ * @param request The request struct, containing service name, type and protocol
+ * @param include_name Whether to include the service name in the domain
+ * @return ERR_OK if domain was written. If service name is included,
+ *         <name>.<type>.<proto>.local. will be written, otherwise <type>.<proto>.local.
+ *         An err_t is returned on error.
+ */
+err_t
+mdns_build_request_domain(struct mdns_domain *domain, struct mdns_request *request, int include_name)
+{
+  err_t res;
+  memset(domain, 0, sizeof(struct mdns_domain));
+  if (include_name) {
+    res = mdns_domain_add_label(domain, request->name, (u8_t)strlen(request->name));
+    LWIP_ERROR("mdns_build_request_domain: Failed to add label", (res == ERR_OK), return res);
+  }
+  res = mdns_domain_add_domain(domain, &request->service);
+  LWIP_ERROR("mdns_build_request_domain: Failed to add domain", (res == ERR_OK), return res);
+  res = mdns_domain_add_label(domain, dnssd_protos[request->proto], (u8_t)strlen(dnssd_protos[request->proto]));
+  LWIP_ERROR("mdns_build_request_domain: Failed to add label", (res == ERR_OK), return res);
+  return mdns_add_dotlocal(domain);
+}
+#endif
+
 /**
  * Return bytes needed to write before jump for best result of compressing supplied domain
  * against domain in outpacket starting at specified offset.
diff --git a/src/apps/mdns/mdns_out.c b/src/apps/mdns/mdns_out.c
index 8d12d27b..10a61929 100644
--- a/src/apps/mdns/mdns_out.c
+++ b/src/apps/mdns/mdns_out.c
@@ -549,6 +549,28 @@ mdns_add_probe_questions_to_outpacket(struct mdns_outpacket *outpkt, struct mdns
   return ERR_OK;
 }
 
+#if LWIP_MDNS_SEARCH
+static err_t
+mdns_add_query_question_to_outpacket(struct mdns_outpacket *outpkt, struct mdns_outmsg *msg)
+{
+  err_t res;
+  /* Write legacy query question */
+  if(msg->query) {
+    struct mdns_request *req = msg->query;
+    struct mdns_domain dom;
+    /* Build question domain */
+    mdns_build_request_domain(&dom, req, req->name[0]);
+    /* Add query question to output packet */
+    res = mdns_add_question(outpkt, &dom, req->qtype, DNS_RRCLASS_IN, 0);
+    if (res != ERR_OK) {
+      return res;
+    }
+    outpkt->questions++;
+  }
+  return ERR_OK;
+}
+#endif
+
 /**
  * Create packet with chosen answers as a reply
  *
@@ -565,6 +587,12 @@ mdns_create_outpacket(struct netif *netif, struct mdns_outmsg *msg,
   int i;
   u16_t answers = 0;
 
+#if LWIP_MDNS_SEARCH
+  res = mdns_add_query_question_to_outpacket(outpkt, msg);
+  if (res != ERR_OK) {
+    return res;
+  }
+#endif
 
   res = mdns_add_probe_questions_to_outpacket(outpkt, msg, netif);
   if (res != ERR_OK) {
@@ -1107,4 +1135,32 @@ mdns_set_timeout(struct netif *netif, u32_t msecs, sys_timeout_handler handler,
   *busy_flag = 1;
 }
 
+#ifdef LWIP_MDNS_SEARCH
+/**
+ * Send search request containing all our known data
+ * @param req The request to send
+ * @param netif The network interface to send on
+ * @param destination The target address to send to (usually multicast address)
+ */
+err_t
+mdns_send_request(struct mdns_request *req, struct netif *netif, const ip_addr_t *destination)
+{
+  struct mdns_outmsg outmsg;
+  err_t res;
+
+  memset(&outmsg, 0, sizeof(outmsg));
+  outmsg.query = req;
+  outmsg.dest_port = LWIP_IANA_PORT_MDNS;
+  SMEMCPY(&outmsg.dest_addr, destination, sizeof(outmsg.dest_addr));
+  res = mdns_send_outpacket(&outmsg, netif);
+  if(res != ERR_OK) {
+    LWIP_DEBUGF(MDNS_DEBUG, ("MDNS: Multicast query request send failed\n"));
+  }
+  else {
+    LWIP_DEBUGF(MDNS_DEBUG, ("MDNS: Multicast query request send successful\n"));
+  }
+  return res;
+}
+#endif
+
 #endif /* LWIP_MDNS_RESPONDER */
diff --git a/src/include/lwip/apps/mdns.h b/src/include/lwip/apps/mdns.h
index df3bf197..a4f52c1a 100644
--- a/src/include/lwip/apps/mdns.h
+++ b/src/include/lwip/apps/mdns.h
@@ -57,10 +57,42 @@ enum mdns_sd_proto {
 #define MDNS_PROBING_SUCCESSFUL 1
 
 #define MDNS_LABEL_MAXLEN  63
+#define MDNS_DOMAIN_MAXLEN 256
 
 struct mdns_host;
 struct mdns_service;
 
+/* Domain structs - also visible for unit tests */
+
+struct mdns_domain {
+  /* Encoded domain name */
+  u8_t name[MDNS_DOMAIN_MAXLEN];
+  /* Total length of domain name, including zero */
+  u16_t length;
+  /* Set if compression of this domain is not allowed */
+  u8_t skip_compression;
+};
+
+/** Domain, type and class.
+ *  Shared between questions and answers */
+struct mdns_rr_info {
+  struct mdns_domain domain;
+  u16_t type;
+  u16_t klass;
+};
+
+struct mdns_answer {
+  struct mdns_rr_info info;
+  /** cache flush command bit */
+  u16_t cache_flush;
+  /* Validity time in seconds */
+  u32_t ttl;
+  /** Length of variable answer */
+  u16_t rd_length;
+  /** Offset of start of variable answer in packet */
+  u16_t rd_offset;
+};
+
 /** Callback function to add text to a reply, called when generating the reply */
 typedef void (*service_get_txt_fn_t)(struct mdns_service *service, void *txt_userdata);
 
@@ -97,6 +129,19 @@ void mdns_resp_announce(struct netif *netif);
  */
 #define mdns_resp_netif_settings_changed(netif) mdns_resp_announce(netif)
 
+#if LWIP_MDNS_SEARCH
+typedef void (*search_result_fn_t)(struct mdns_answer *answer, const char *varpart, int varlen, int flags, void *arg);
+/* flags bits, both can be set! */
+#define MDNS_SEARCH_RESULT_FIRST    1 /* First answer in received frame. */
+#define MDNS_SEARCH_RESULT_LAST     2 /* Last answer. */
+
+err_t mdns_search_service(const char *name, const char *service, enum mdns_sd_proto proto,
+                          struct netif *netif, search_result_fn_t result_fn, void *arg,
+                          s8_t *request_id);
+void mdns_search_stop(s8_t request_id);
+
+#endif /* LWIP_MDNS_SEARCH */
+
 #endif /* LWIP_MDNS_RESPONDER */
 
 #ifdef __cplusplus
diff --git a/src/include/lwip/apps/mdns_domain.h b/src/include/lwip/apps/mdns_domain.h
index 941ce63c..9fa804e6 100644
--- a/src/include/lwip/apps/mdns_domain.h
+++ b/src/include/lwip/apps/mdns_domain.h
@@ -51,6 +51,8 @@ extern "C" {
 /* Domain methods - also visible for unit tests */
 
 err_t mdns_domain_add_label(struct mdns_domain *domain, const char *label, u8_t len);
+err_t mdns_domain_add_domain(struct mdns_domain *domain, struct mdns_domain *source);
+err_t mdns_domain_add_string(struct mdns_domain *domain, const char *source);
 u16_t mdns_readname(struct pbuf *p, u16_t offset, struct mdns_domain *domain);
 void mdns_domain_debug_print(struct mdns_domain *domain);
 int mdns_domain_eq(struct mdns_domain *a, struct mdns_domain *b);
@@ -63,6 +65,9 @@ err_t mdns_build_reverse_v6_domain(struct mdns_domain *domain, const ip6_addr_t
 err_t mdns_build_host_domain(struct mdns_domain *domain, struct mdns_host *mdns);
 err_t mdns_build_dnssd_domain(struct mdns_domain *domain);
 err_t mdns_build_service_domain(struct mdns_domain *domain, struct mdns_service *service, int include_name);
+#if LWIP_MDNS_SEARCH
+err_t mdns_build_request_domain(struct mdns_domain *domain, struct mdns_request *request, int include_name);
+#endif
 u16_t mdns_compress_domain(struct pbuf *pbuf, u16_t *offset, struct mdns_domain *domain);
 err_t mdns_write_domain(struct mdns_outpacket *outpkt, struct mdns_domain *domain);
 
diff --git a/src/include/lwip/apps/mdns_opts.h b/src/include/lwip/apps/mdns_opts.h
index d805e8c7..684d631e 100644
--- a/src/include/lwip/apps/mdns_opts.h
+++ b/src/include/lwip/apps/mdns_opts.h
@@ -67,6 +67,18 @@
 #define MDNS_RESP_USENETIF_EXTCALLBACK  LWIP_NETIF_EXT_STATUS_CALLBACK
 #endif
 
+/**
+ * LWIP_MDNS_SEARCH==1: Turn on search over multicast DNS module.
+ */
+#ifndef LWIP_MDNS_SEARCH
+#define LWIP_MDNS_SEARCH                1
+#endif
+
+/** The maximum number of running requests */
+#ifndef MDNS_MAX_REQUESTS
+#define MDNS_MAX_REQUESTS               2
+#endif
+
 /**
  * MDNS_DEBUG: Enable debugging for multicast DNS.
  */
diff --git a/src/include/lwip/apps/mdns_out.h b/src/include/lwip/apps/mdns_out.h
index a6e33234..e6a7e388 100644
--- a/src/include/lwip/apps/mdns_out.h
+++ b/src/include/lwip/apps/mdns_out.h
@@ -125,6 +125,9 @@ void mdns_send_unicast_msg_delayed_ipv6(void *arg);
 void mdns_start_multicast_timeouts_ipv6(struct netif *netif);
 #endif
 void mdns_prepare_txtdata(struct mdns_service *service);
+#ifdef LWIP_MDNS_SEARCH
+err_t mdns_send_request(struct mdns_request *req, struct netif *netif, const ip_addr_t *destination);
+#endif
 
 #endif /* LWIP_MDNS_RESPONDER */
 
diff --git a/src/include/lwip/apps/mdns_priv.h b/src/include/lwip/apps/mdns_priv.h
index 778ef38a..2bc9ef0d 100644
--- a/src/include/lwip/apps/mdns_priv.h
+++ b/src/include/lwip/apps/mdns_priv.h
@@ -48,7 +48,6 @@ extern "C" {
 
 #if LWIP_MDNS_RESPONDER
 
-#define MDNS_DOMAIN_MAXLEN 256
 #define MDNS_READNAME_ERROR 0xFFFF
 #define NUM_DOMAIN_OFFSETS 10
 
@@ -72,16 +71,24 @@ extern "C" {
 #define MDNS_PROBE_MAX_CONFLICTS_TIME_WINDOW        10000
 #define MDNS_PROBE_MAX_CONFLICTS_TIMEOUT            5000
 
-/* Domain structs - also visible for unit tests */
-
-struct mdns_domain {
-  /* Encoded domain name */
-  u8_t name[MDNS_DOMAIN_MAXLEN];
-  /* Total length of domain name, including zero */
-  u16_t length;
-  /* Set if compression of this domain is not allowed */
-  u8_t skip_compression;
+#if LWIP_MDNS_SEARCH
+/** Description of a search request */
+struct mdns_request {
+  /** Name of service, like 'myweb' */
+  char name[MDNS_LABEL_MAXLEN + 1];
+  /** Type of service, like '_http' or '_services._dns-sd' */
+  struct mdns_domain service;
+  /** Callback function called for each response */
+  search_result_fn_t result_fn;
+  void *arg;
+  /** Protocol, TCP or UDP */
+  u16_t proto;
+  /** Query type (PTR, SRV, ...) */
+  u8_t qtype;
+  /** PTR only request. */
+  u16_t only_ptr;
 };
+#endif
 
 /** Description of a service */
 struct mdns_service {
@@ -149,6 +156,10 @@ struct mdns_outmsg {
   u8_t host_reverse_v6_replies;
   /* Reply bitmask per service */
   u8_t serv_replies[MDNS_MAX_SERVICES];
+#ifdef LWIP_MDNS_SEARCH
+  /** Search query to send */
+  struct mdns_request *query;
+#endif
 };
 
 /** Delayed msg info */
-- 
2.17.1

