From 66eac28c200b42ba5a779b06660ea23b5cf523f2 Mon Sep 17 00:00:00 2001 From: "Sergio R. Caprile" Date: Mon, 20 Jul 2026 18:51:34 -0300 Subject: [PATCH] Add SD query and response to mDNS --- mongoose.c | 198 ++++++++++++++++++------ mongoose.h | 5 +- src/dns.c | 198 ++++++++++++++++++------ src/dns.h | 5 +- tutorials/udp/mdns-client/main.c | 3 +- tutorials/udp/mdns-sd-client/Makefile | 25 +++ tutorials/udp/mdns-sd-client/README.md | 43 +++++ tutorials/udp/mdns-sd-client/main.c | 58 +++++++ tutorials/udp/mdns-sd-client/mongoose.c | 1 + tutorials/udp/mdns-sd-client/mongoose.h | 1 + 10 files changed, 444 insertions(+), 93 deletions(-) create mode 100644 tutorials/udp/mdns-sd-client/Makefile create mode 100644 tutorials/udp/mdns-sd-client/README.md create mode 100644 tutorials/udp/mdns-sd-client/main.c create mode 120000 tutorials/udp/mdns-sd-client/mongoose.c create mode 120000 tutorials/udp/mdns-sd-client/mongoose.h diff --git a/mongoose.c b/mongoose.c index 3094a88a..c699c47b 100644 --- a/mongoose.c +++ b/mongoose.c @@ -1861,16 +1861,16 @@ static void dns_cb(struct mg_connection *c, int ev, void *ev_data) { } } -static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, - uint16_t txnid, bool ipv6) { - struct { +static bool dns_send(struct mg_connection *c, const struct mg_str *name, + unsigned int rtype, uint16_t txnid, uint16_t flags) { + struct { // RFC-1035 4.1.2 struct mg_dns_header header; uint8_t data[256]; } pkt; size_t i, n; memset(&pkt, 0, sizeof(pkt)); pkt.header.txnid = mg_htons(txnid); - pkt.header.flags = mg_htons(0x100); + pkt.header.flags = mg_htons(flags); pkt.header.num_questions = mg_htons(1); for (i = n = 0; i < sizeof(pkt.data) - 5; i++) { if (name->buf[i] == '.' || i >= name->len) { @@ -1880,14 +1880,19 @@ static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, } if (i >= name->len) break; } - memcpy(&pkt.data[n], "\x00\x00\x01\x00\x01", 5); // A query + memset(&pkt.data[n], 0, 5); // nul, QTYPE, QCLASS + pkt.data[n + 2] = (uint8_t) rtype; // QTYPE = rtype, only 0-255 supported + pkt.data[n + 4] = 1; // QCLASS = IN n += 5; - if (ipv6) pkt.data[n - 3] = 0x1c; // AAAA query - // memcpy(&pkt.data[n], "\xc0\x0c\x00\x1c\x00\x01", 6); // AAAA query - // n += 6; return mg_send(c, &pkt, sizeof(pkt.header) + n); } +static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, + uint16_t txnid, bool ipv6) { + return dns_send(c, name, ipv6 ? MG_DNS_RTYPE_AAAA : MG_DNS_RTYPE_A, txnid, + 0x100); // RD +} + bool mg_dnsc_init(struct mg_mgr *mgr, struct mg_dns *dnsc); bool mg_dnsc_init(struct mg_mgr *mgr, struct mg_dns *dnsc) { if (dnsc->url == NULL) { @@ -2207,43 +2212,149 @@ static void handle_mdns_query(struct mg_connection *c) { } } + +static size_t srvtoname(char *svc) { + char *dot = strchr(svc, '.'); + if (dot == NULL) return 0; + *dot = '\0'; + return (size_t) (dot - svc); +} + +#define MG_MAX_MDNS_RECORDS 8 +// Condense one useful mDNS/DNS-SD chain from a response datagram into a single +// MG_EV_MDNS_RESP event. A/AAAA as the first supported record are terminal +// address answers (AAAA currently ignored). For PTR/SRV/TXT, subsequent +// records are attached only when their label matches the service instance, or +// the SRV target for address records. +// Some devices send SRV/TXT as extra Answer RRs instead of Additional RRs, so +// both sections are parsed as one chain. static void handle_mdns_response(struct mg_connection *c) { struct mg_dns_header *rh = (struct mg_dns_header *) c->recv.buf; - struct mg_dns_rr rr; - size_t n; - // Parse first response, offset 12 is header size - n = mg_dns_parse_rr(c->recv.buf, c->recv.len, 12, false, &rr); - MG_VERBOSE(("mDNS response parsed, result=%d", (int) n)); - if (n > 0) { - // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 - char name[256]; - uint8_t name_len; - struct mg_mdns_resp resp; - memset(&resp, 0, sizeof(resp)); - if (rh->num_answers > mg_htons(1)) MG_DEBUG(("ignoring > 1 answers")); - mg_dns_parse_name(c->recv.buf, c->recv.len, 12, name, sizeof(name)); - name_len = (uint8_t) strlen(name); - MG_VERBOSE(("RR %u %u %s", (unsigned int) rr.atype, - (unsigned int) rr.aclass, name)); - if (rr.alen == 4 && rr.atype == MG_DNS_RTYPE_A && - (rr.aclass & 0x7FFF) == 1) { - resp.addr.is_ip6 = false; - memcpy(resp.addr.addr.ip, (char *) (rh + 1) + n - 4, 4); - MG_DEBUG(("A response from %.*s = %M", name_len, name, mg_print_ip, - &resp.addr)); - // } else if (rr.alen == 16 && rr.atype == MG_DNS_RTYPE_AAAA && - // (rr.aclass & 0x7FFF) == 1) { - // resp.addr.is_ip6 = true; - // memcpy(resp.addr.addr.ip, (char *)(rh + 1) + n - 16], 16); - // MG_DEBUG(("AAAA response from %.*s = %M", name_len, name, - // mg_print_ip, &resp.addr)); - } else { - return; - } - resp.name = mg_str_n(name, name_len); - resp.rr = &rr; - mg_call(c, MG_EV_MDNS_RESP, &resp); + struct mg_dns_rr rr, rr_[5]; // A, PTR, SRV, TXT, AAAA, in that order + struct mg_mdns_resp resp; + size_t n, i, num, answers, roff_[5], roff = 12; // offset 12 is header size + // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 + char name[256], srvcproto[256], instance[256], host[256]; + uint16_t atype = 0; + answers = mg_ntohs(rh->num_answers); + num = answers + mg_ntohs(rh->num_other_prs); + if (answers == 0) return; + MG_VERBOSE(("mDNS response: %u answer, %u additional", (unsigned) answers, + (unsigned) (num - answers))); + if (num > MG_MAX_MDNS_RECORDS) { + num = MG_MAX_MDNS_RECORDS; + MG_DEBUG(("ignoring > %u records", MG_MAX_MDNS_RECORDS)); } + memset(roff_, 0, sizeof(roff_)); + memset(&rr_, 0, sizeof(rr_)); + memset(&resp, 0, sizeof(resp)); + instance[0] = host[0] = '\0'; + for (i = 0; i < num; i++) { // First Answer RR is primary; the rest must match it + n = mg_dns_parse_rr(c->recv.buf, c->recv.len, roff, false, &rr); + MG_VERBOSE(("mDNS record parsed, result=%u", n)); + if (n == 0) return; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff, name, sizeof(name)) == 0) return; + MG_VERBOSE(("RR %u %u %s", (unsigned int) rr.atype, (unsigned int) rr.aclass, name)); + if (rr.alen == 4 && rr.atype == MG_DNS_RTYPE_A && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (host[0] != '\0' && strcmp(name, host) == 0) || + (host[0] == '\0' && instance[0] != '\0' && strcmp(name, instance) == 0))) { + if (rr_[0].atype != 0) break; + if (i == 0) atype = rr.atype; + resp.name = mg_str(name); + resp.addr.is_ip6 = false; + memcpy(resp.addr.addr.ip, c->recv.buf + roff + n - 4, 4); + MG_VERBOSE(("A record")); + roff_[0] = roff, rr_[0] = rr; + if (atype == MG_DNS_RTYPE_A) break; + } else if (rr.alen == 16 && rr.atype == MG_DNS_RTYPE_AAAA && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (host[0] != '\0' && strcmp(name, host) == 0) || + (host[0] == '\0' && instance[0] != '\0' && strcmp(name, instance) == 0))) { +# if 0 + if (rr_[4].atype != 0) break; + resp.addr.is_ip6 = true; + if (i == 0) atype = rr.atype; + resp.name = mg_str(name); + memcpy(resp.addr.addr.ip, c->recv.buf + roff + n - 16, 16); + MG_VERBOSE(("AAAA record")); + roff_[4] = roff, rr_[4] = rr; + if (atype == MG_DNS_RTYPE_AAAA) break; +#else + MG_VERBOSE(("ignored AAAA record")); +#endif + } else if (rr.atype == MG_DNS_RTYPE_PTR && (rr.aclass & 0x7FFF) == 1 && i == 0) { + if (rr_[1].atype != 0) break; + atype = rr.atype; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff + rr.nlen + 10, + instance, sizeof(instance)) == 0) return; + MG_VERBOSE(("PTR record")); + roff_[1] = roff, rr_[1] = rr; + } else if (rr.atype == MG_DNS_RTYPE_SRV && rr.alen >= 6 && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (instance[0] != '\0' && strcmp(name, instance) == 0))) { + if (rr_[2].atype != 0) break; + if (i == 0) atype = rr.atype; + resp.sd.port = MG_LOAD_BE16(c->recv.buf + roff + rr.nlen + 14); + resp.addr.port = mg_htons(resp.sd.port); + if (instance[0] == '\0') mg_snprintf(instance, sizeof(instance), "%s", name); + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff + rr.nlen + 16, + host, sizeof(host)) == 0) return; + MG_VERBOSE(("SRV record")); + roff_[2] = roff, rr_[2] = rr; + } else if (rr.atype == MG_DNS_RTYPE_TXT && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (instance[0] != '\0' && strcmp(name, instance) == 0))) { + if (rr_[3].atype != 0) break; + if (i == 0) atype = rr.atype; + if (instance[0] == '\0') mg_snprintf(instance, sizeof(instance), "%s", name); + resp.sd.txt = mg_str_n((char *) c->recv.buf + roff + rr.nlen + 10, rr.alen); + MG_VERBOSE(("TXT record")); + roff_[3] = roff, rr_[3] = rr; + } + roff += n; + if (i == 0 && atype == 0) return; + } + if (atype == MG_DNS_RTYPE_A) { + resp.rr = &rr_[0]; + MG_DEBUG(("A response from %s = %M", name, mg_print_ip, &resp.addr)); +#if 0 + } else if (atype == MG_DNS_RTYPE_AAAA) { + resp.rr = &rr_[4]; + MG_DEBUG(("AAAA response from %s = %M", name, mg_print_ip6, resp.addr.addr.ip)); +#endif + } else if (atype == MG_DNS_RTYPE_PTR) { + size_t len; + resp.rr = &rr_[1]; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff_[1], srvcproto, + sizeof(srvcproto)) == 0) return; + len = strlen(srvcproto); + if (len <= 6) return; + resp.sd.srvcproto = mg_str_n(srvcproto, len - 6); // remove .local + if (host[0] != '\0') { + resp.name = mg_str(host); + } else if (rr_[0].atype == 0) { + mg_snprintf(name, sizeof(name), "%s", instance); + resp.name = mg_str_n(name, srvtoname(name)); + } else { + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff_[0], name, + sizeof(name)) == 0) return; + resp.name = mg_str(name); + } + MG_DEBUG(("PTR response for %s from %s", srvcproto, name)); + } else if (atype == MG_DNS_RTYPE_SRV || atype == MG_DNS_RTYPE_TXT) { + size_t len, name_len; + resp.rr = atype == MG_DNS_RTYPE_SRV ? &rr_[2] : &rr_[3]; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, + atype == MG_DNS_RTYPE_SRV ? roff_[2] : roff_[3], + srvcproto, sizeof(srvcproto)) == 0) + return; + len = strlen(srvcproto); + name_len = srvtoname(srvcproto); + if (name_len == 0 || len <= name_len + 7) return; // remove .local + resp.sd.srvcproto = mg_str_n(srvcproto + name_len + 1, len - name_len - 7); + resp.name = mg_str_n(srvcproto, name_len); + MG_DEBUG(("%s response for %s from %s", atype == MG_DNS_RTYPE_SRV ? "SRV" : "TXT", srvcproto, name)); + } else { + return; + } + mg_call(c, MG_EV_MDNS_RESP, &resp); } static void handle_mdns_record(struct mg_connection *c) { @@ -2317,8 +2428,7 @@ struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, mg_event_handler_t fn, static bool mdns_query(struct mg_connection *c, struct mg_str *name, unsigned int rtype) { mg_multicast_restore(c, (uint8_t *) &c->loc); - (void) rtype; - return mg_dns_send(c, name, 0, false); + return dns_send(c, name, rtype, 0, 0); // RFC-6762 18.1 id = 0, 18.6 RD = 0 } bool mg_mdns_query(struct mg_connection *c, const char *name, diff --git a/mongoose.h b/mongoose.h index b3dc01a1..4014d2bf 100644 --- a/mongoose.h +++ b/mongoose.h @@ -4020,11 +4020,12 @@ struct mg_mdns_req { bool is_unicast; // True if the client requested a unicast (QU) response }; -// ev_data for MG_EV_MDNS_RESP, carrying the resolved address from an mDNS response. +// ev_data for MG_EV_MDNS_RESP, carrying address/data from a mDNS response struct mg_mdns_resp { - struct mg_dns_rr *rr; // Resource record from the response + struct mg_dns_rr *rr; // Resource record from the response (1st in chain) struct mg_str name; // Resolved hostname, without the .local suffix struct mg_addr addr; // Resolved IP address + struct mg_dnssd_record sd; // Service Discovery data }; // Parses a DNS query or response from buf/len into dm. Returns true on success. diff --git a/src/dns.c b/src/dns.c index edbde720..46fe4311 100644 --- a/src/dns.c +++ b/src/dns.c @@ -228,16 +228,16 @@ static void dns_cb(struct mg_connection *c, int ev, void *ev_data) { } } -static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, - uint16_t txnid, bool ipv6) { - struct { +static bool dns_send(struct mg_connection *c, const struct mg_str *name, + unsigned int rtype, uint16_t txnid, uint16_t flags) { + struct { // RFC-1035 4.1.2 struct mg_dns_header header; uint8_t data[256]; } pkt; size_t i, n; memset(&pkt, 0, sizeof(pkt)); pkt.header.txnid = mg_htons(txnid); - pkt.header.flags = mg_htons(0x100); + pkt.header.flags = mg_htons(flags); pkt.header.num_questions = mg_htons(1); for (i = n = 0; i < sizeof(pkt.data) - 5; i++) { if (name->buf[i] == '.' || i >= name->len) { @@ -247,14 +247,19 @@ static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, } if (i >= name->len) break; } - memcpy(&pkt.data[n], "\x00\x00\x01\x00\x01", 5); // A query + memset(&pkt.data[n], 0, 5); // nul, QTYPE, QCLASS + pkt.data[n + 2] = (uint8_t) rtype; // QTYPE = rtype, only 0-255 supported + pkt.data[n + 4] = 1; // QCLASS = IN n += 5; - if (ipv6) pkt.data[n - 3] = 0x1c; // AAAA query - // memcpy(&pkt.data[n], "\xc0\x0c\x00\x1c\x00\x01", 6); // AAAA query - // n += 6; return mg_send(c, &pkt, sizeof(pkt.header) + n); } +static bool mg_dns_send(struct mg_connection *c, const struct mg_str *name, + uint16_t txnid, bool ipv6) { + return dns_send(c, name, ipv6 ? MG_DNS_RTYPE_AAAA : MG_DNS_RTYPE_A, txnid, + 0x100); // RD +} + bool mg_dnsc_init(struct mg_mgr *mgr, struct mg_dns *dnsc); bool mg_dnsc_init(struct mg_mgr *mgr, struct mg_dns *dnsc) { if (dnsc->url == NULL) { @@ -574,43 +579,149 @@ static void handle_mdns_query(struct mg_connection *c) { } } + +static size_t srvtoname(char *svc) { + char *dot = strchr(svc, '.'); + if (dot == NULL) return 0; + *dot = '\0'; + return (size_t) (dot - svc); +} + +#define MG_MAX_MDNS_RECORDS 8 +// Condense one useful mDNS/DNS-SD chain from a response datagram into a single +// MG_EV_MDNS_RESP event. A/AAAA as the first supported record are terminal +// address answers (AAAA currently ignored). For PTR/SRV/TXT, subsequent +// records are attached only when their label matches the service instance, or +// the SRV target for address records. +// Some devices send SRV/TXT as extra Answer RRs instead of Additional RRs, so +// both sections are parsed as one chain. static void handle_mdns_response(struct mg_connection *c) { struct mg_dns_header *rh = (struct mg_dns_header *) c->recv.buf; - struct mg_dns_rr rr; - size_t n; - // Parse first response, offset 12 is header size - n = mg_dns_parse_rr(c->recv.buf, c->recv.len, 12, false, &rr); - MG_VERBOSE(("mDNS response parsed, result=%d", (int) n)); - if (n > 0) { - // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 - char name[256]; - uint8_t name_len; - struct mg_mdns_resp resp; - memset(&resp, 0, sizeof(resp)); - if (rh->num_answers > mg_htons(1)) MG_DEBUG(("ignoring > 1 answers")); - mg_dns_parse_name(c->recv.buf, c->recv.len, 12, name, sizeof(name)); - name_len = (uint8_t) strlen(name); - MG_VERBOSE(("RR %u %u %s", (unsigned int) rr.atype, - (unsigned int) rr.aclass, name)); - if (rr.alen == 4 && rr.atype == MG_DNS_RTYPE_A && - (rr.aclass & 0x7FFF) == 1) { - resp.addr.is_ip6 = false; - memcpy(resp.addr.addr.ip, (char *) (rh + 1) + n - 4, 4); - MG_DEBUG(("A response from %.*s = %M", name_len, name, mg_print_ip, - &resp.addr)); - // } else if (rr.alen == 16 && rr.atype == MG_DNS_RTYPE_AAAA && - // (rr.aclass & 0x7FFF) == 1) { - // resp.addr.is_ip6 = true; - // memcpy(resp.addr.addr.ip, (char *)(rh + 1) + n - 16], 16); - // MG_DEBUG(("AAAA response from %.*s = %M", name_len, name, - // mg_print_ip, &resp.addr)); - } else { - return; - } - resp.name = mg_str_n(name, name_len); - resp.rr = &rr; - mg_call(c, MG_EV_MDNS_RESP, &resp); + struct mg_dns_rr rr, rr_[5]; // A, PTR, SRV, TXT, AAAA, in that order + struct mg_mdns_resp resp; + size_t n, i, num, answers, roff_[5], roff = 12; // offset 12 is header size + // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 + char name[256], srvcproto[256], instance[256], host[256]; + uint16_t atype = 0; + answers = mg_ntohs(rh->num_answers); + num = answers + mg_ntohs(rh->num_other_prs); + if (answers == 0) return; + MG_VERBOSE(("mDNS response: %u answer, %u additional", (unsigned) answers, + (unsigned) (num - answers))); + if (num > MG_MAX_MDNS_RECORDS) { + num = MG_MAX_MDNS_RECORDS; + MG_DEBUG(("ignoring > %u records", MG_MAX_MDNS_RECORDS)); } + memset(roff_, 0, sizeof(roff_)); + memset(&rr_, 0, sizeof(rr_)); + memset(&resp, 0, sizeof(resp)); + instance[0] = host[0] = '\0'; + for (i = 0; i < num; i++) { // First Answer RR is primary; the rest must match it + n = mg_dns_parse_rr(c->recv.buf, c->recv.len, roff, false, &rr); + MG_VERBOSE(("mDNS record parsed, result=%u", n)); + if (n == 0) return; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff, name, sizeof(name)) == 0) return; + MG_VERBOSE(("RR %u %u %s", (unsigned int) rr.atype, (unsigned int) rr.aclass, name)); + if (rr.alen == 4 && rr.atype == MG_DNS_RTYPE_A && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (host[0] != '\0' && strcmp(name, host) == 0) || + (host[0] == '\0' && instance[0] != '\0' && strcmp(name, instance) == 0))) { + if (rr_[0].atype != 0) break; + if (i == 0) atype = rr.atype; + resp.name = mg_str(name); + resp.addr.is_ip6 = false; + memcpy(resp.addr.addr.ip, c->recv.buf + roff + n - 4, 4); + MG_VERBOSE(("A record")); + roff_[0] = roff, rr_[0] = rr; + if (atype == MG_DNS_RTYPE_A) break; + } else if (rr.alen == 16 && rr.atype == MG_DNS_RTYPE_AAAA && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (host[0] != '\0' && strcmp(name, host) == 0) || + (host[0] == '\0' && instance[0] != '\0' && strcmp(name, instance) == 0))) { +# if 0 + if (rr_[4].atype != 0) break; + resp.addr.is_ip6 = true; + if (i == 0) atype = rr.atype; + resp.name = mg_str(name); + memcpy(resp.addr.addr.ip, c->recv.buf + roff + n - 16, 16); + MG_VERBOSE(("AAAA record")); + roff_[4] = roff, rr_[4] = rr; + if (atype == MG_DNS_RTYPE_AAAA) break; +#else + MG_VERBOSE(("ignored AAAA record")); +#endif + } else if (rr.atype == MG_DNS_RTYPE_PTR && (rr.aclass & 0x7FFF) == 1 && i == 0) { + if (rr_[1].atype != 0) break; + atype = rr.atype; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff + rr.nlen + 10, + instance, sizeof(instance)) == 0) return; + MG_VERBOSE(("PTR record")); + roff_[1] = roff, rr_[1] = rr; + } else if (rr.atype == MG_DNS_RTYPE_SRV && rr.alen >= 6 && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (instance[0] != '\0' && strcmp(name, instance) == 0))) { + if (rr_[2].atype != 0) break; + if (i == 0) atype = rr.atype; + resp.sd.port = MG_LOAD_BE16(c->recv.buf + roff + rr.nlen + 14); + resp.addr.port = mg_htons(resp.sd.port); + if (instance[0] == '\0') mg_snprintf(instance, sizeof(instance), "%s", name); + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff + rr.nlen + 16, + host, sizeof(host)) == 0) return; + MG_VERBOSE(("SRV record")); + roff_[2] = roff, rr_[2] = rr; + } else if (rr.atype == MG_DNS_RTYPE_TXT && (rr.aclass & 0x7FFF) == 1 && + (i == 0 || (instance[0] != '\0' && strcmp(name, instance) == 0))) { + if (rr_[3].atype != 0) break; + if (i == 0) atype = rr.atype; + if (instance[0] == '\0') mg_snprintf(instance, sizeof(instance), "%s", name); + resp.sd.txt = mg_str_n((char *) c->recv.buf + roff + rr.nlen + 10, rr.alen); + MG_VERBOSE(("TXT record")); + roff_[3] = roff, rr_[3] = rr; + } + roff += n; + if (i == 0 && atype == 0) return; + } + if (atype == MG_DNS_RTYPE_A) { + resp.rr = &rr_[0]; + MG_DEBUG(("A response from %s = %M", name, mg_print_ip, &resp.addr)); +#if 0 + } else if (atype == MG_DNS_RTYPE_AAAA) { + resp.rr = &rr_[4]; + MG_DEBUG(("AAAA response from %s = %M", name, mg_print_ip6, resp.addr.addr.ip)); +#endif + } else if (atype == MG_DNS_RTYPE_PTR) { + size_t len; + resp.rr = &rr_[1]; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff_[1], srvcproto, + sizeof(srvcproto)) == 0) return; + len = strlen(srvcproto); + if (len <= 6) return; + resp.sd.srvcproto = mg_str_n(srvcproto, len - 6); // remove .local + if (host[0] != '\0') { + resp.name = mg_str(host); + } else if (rr_[0].atype == 0) { + mg_snprintf(name, sizeof(name), "%s", instance); + resp.name = mg_str_n(name, srvtoname(name)); + } else { + if (mg_dns_parse_name(c->recv.buf, c->recv.len, roff_[0], name, + sizeof(name)) == 0) return; + resp.name = mg_str(name); + } + MG_DEBUG(("PTR response for %s from %s", srvcproto, name)); + } else if (atype == MG_DNS_RTYPE_SRV || atype == MG_DNS_RTYPE_TXT) { + size_t len, name_len; + resp.rr = atype == MG_DNS_RTYPE_SRV ? &rr_[2] : &rr_[3]; + if (mg_dns_parse_name(c->recv.buf, c->recv.len, + atype == MG_DNS_RTYPE_SRV ? roff_[2] : roff_[3], + srvcproto, sizeof(srvcproto)) == 0) + return; + len = strlen(srvcproto); + name_len = srvtoname(srvcproto); + if (name_len == 0 || len <= name_len + 7) return; // remove .local + resp.sd.srvcproto = mg_str_n(srvcproto + name_len + 1, len - name_len - 7); + resp.name = mg_str_n(srvcproto, name_len); + MG_DEBUG(("%s response for %s from %s", atype == MG_DNS_RTYPE_SRV ? "SRV" : "TXT", srvcproto, name)); + } else { + return; + } + mg_call(c, MG_EV_MDNS_RESP, &resp); } static void handle_mdns_record(struct mg_connection *c) { @@ -684,8 +795,7 @@ struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, mg_event_handler_t fn, static bool mdns_query(struct mg_connection *c, struct mg_str *name, unsigned int rtype) { mg_multicast_restore(c, (uint8_t *) &c->loc); - (void) rtype; - return mg_dns_send(c, name, 0, false); + return dns_send(c, name, rtype, 0, 0); // RFC-6762 18.1 id = 0, 18.6 RD = 0 } bool mg_mdns_query(struct mg_connection *c, const char *name, diff --git a/src/dns.h b/src/dns.h index 3387e899..d4f48db6 100644 --- a/src/dns.h +++ b/src/dns.h @@ -60,11 +60,12 @@ struct mg_mdns_req { bool is_unicast; // True if the client requested a unicast (QU) response }; -// ev_data for MG_EV_MDNS_RESP, carrying the resolved address from an mDNS response. +// ev_data for MG_EV_MDNS_RESP, carrying address/data from a mDNS response struct mg_mdns_resp { - struct mg_dns_rr *rr; // Resource record from the response + struct mg_dns_rr *rr; // Resource record from the response (1st in chain) struct mg_str name; // Resolved hostname, without the .local suffix struct mg_addr addr; // Resolved IP address + struct mg_dnssd_record sd; // Service Discovery data }; // Parses a DNS query or response from buf/len into dm. Returns true on success. diff --git a/tutorials/udp/mdns-client/main.c b/tutorials/udp/mdns-client/main.c index ab2540c2..525fc461 100644 --- a/tutorials/udp/mdns-client/main.c +++ b/tutorials/udp/mdns-client/main.c @@ -8,7 +8,8 @@ char s_theone[] = "Server.local"; static void mdns_ev_handler(struct mg_connection *c, int ev, void *ev_data) { if (ev == MG_EV_MDNS_RESP) { // is this the one ? struct mg_mdns_resp *resp = (struct mg_mdns_resp *) ev_data; - if (mg_strcasecmp(mg_str(s_theone), resp->name) == 0) + if (resp->rr->atype == MG_DNS_RTYPE_A && + mg_strcasecmp(mg_str(s_theone), resp->name) == 0) MG_INFO(("%.*s is: %M", resp->name.len, resp->name.buf, mg_print_ip, &resp->addr)); } (void) c; diff --git a/tutorials/udp/mdns-sd-client/Makefile b/tutorials/udp/mdns-sd-client/Makefile new file mode 100644 index 00000000..b231dd04 --- /dev/null +++ b/tutorials/udp/mdns-sd-client/Makefile @@ -0,0 +1,25 @@ +PROG ?= example # Program we are building +DELETE = rm -rf # Command to remove files +OUT ?= -o $(PROG) # Compiler argument for output file +SOURCES = main.c mongoose.c # Source code files +CFLAGS = -W -Wall -Wextra -g -I. # Build options + +# Mongoose build options. See https://mongoose.ws/documentation/#build-options +CFLAGS_MONGOOSE += -DMG_ENABLE_LINES + +ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe + PROG ?= example.exe # Use .exe suffix for the binary + CC = gcc # Use MinGW gcc compiler + CFLAGS += -lws2_32 # Link against Winsock library + DELETE = cmd /C del /Q /F /S # Command prompt command to delete files + OUT ?= -o $(PROG) # Build output +endif + +all: $(PROG) # Default target. Build and run program + $(RUN) ./$(PROG) $(ARGS) + +$(PROG): $(SOURCES) # Build program from sources + $(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT) + +clean: # Cleanup. Delete built program and all build artifacts + $(DELETE) $(PROG) *.o *.obj *.exe *.dSYM diff --git a/tutorials/udp/mdns-sd-client/README.md b/tutorials/udp/mdns-sd-client/README.md new file mode 100644 index 00000000..3a4164d7 --- /dev/null +++ b/tutorials/udp/mdns-sd-client/README.md @@ -0,0 +1,43 @@ + +Browses for `_http._tcp.local` services on the local network using mDNS/DNS-SD. + +```sh +make +cc main.c mongoose.c -W -Wall -Wextra -g -I. -DMG_ENABLE_LINES -o example +./example +1304aa594 3 net.c:311:mg_mgr_init MG_IO_SIZE: 16384, TLS: none +1304aa594 3 net.c:224:mg_listen 1 4 udp://224.0.0.251:5353 +1304aa594 3 sock.c:167:mg_send 1 4 0:0:0 34 err 0 +1304aa594 3 sock.c:361:read_conn 1 4 0:0:0 34 err 0 +1304aa594 3 dns.c:478:handle_mdns_query PTR request for _http._tcp +1304aa5d0 3 sock.c:361:read_conn 1 4 0:0:0 289 err 0 +1304aa5d1 3 dns.c:703:handle_mdns_respo PTR response for _http._tcp.local from 3D03F3CPAU00096.local +1304aa5d1 2 main.c:15:mdns_ev_handler Got a response + SERVICE -> _http._tcp + NAME -> 3D03F3CPAU00096.local + PORT -> 80 + TXT -> host=3D03F3CPAU00096mac=e0:50:8b:8d:2f:75ip=192.168.69.220 + URL -> http://3D03F3CPAU00096.local:80 + +1304aa622 3 sock.c:361:read_conn 1 4 0:0:0 226 err 0 +1304aa622 3 dns.c:703:handle_mdns_respo PTR response for _http._tcp.local from DIP-3TIR3020140717CCWR473261824.local +1304aa622 2 main.c:15:mdns_ev_handler Got a response + SERVICE -> _http._tcp + NAME -> DIP-3TIR3020140717CCWR473261824.local + PORT -> 80 + ADDRESS -> 192.168.69.225 + TXT -> path=/ + URL -> http://192.168.69.225:80 + +1304aa6f5 3 sock.c:361:read_conn 1 4 0:0:0 169 err 0 +1304aa6f5 3 dns.c:703:handle_mdns_respo PTR response for _http._tcp.local from PHILIPS_HD9255_3_6B7E5E.local +1304aa6f5 2 main.c:15:mdns_ev_handler Got a response + SERVICE -> _http._tcp + NAME -> PHILIPS_HD9255_3_6B7E5E.local + PORT -> 80 + ADDRESS -> 192.168.69.167 + TXT -> Device Type=Domestic Appliance + URL -> http://192.168.69.167:80 + +``` + diff --git a/tutorials/udp/mdns-sd-client/main.c b/tutorials/udp/mdns-sd-client/main.c new file mode 100644 index 00000000..ee5a9328 --- /dev/null +++ b/tutorials/udp/mdns-sd-client/main.c @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "mongoose.h" + +static char s_name[] = "Mongoose"; +static char s_service[] = "_http._tcp"; + +static void mdns_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_MDNS_RESP) { + struct mg_mdns_resp *resp = (struct mg_mdns_resp *) ev_data; + if (resp->rr->atype == MG_DNS_RTYPE_PTR && + mg_strcasecmp(mg_str(s_service), resp->sd.srvcproto) == 0) { + char addr[40]; + MG_INFO(("Got a response")); + printf("\tSERVICE -> %.*s\n", (int) resp->sd.srvcproto.len, + resp->sd.srvcproto.buf); + printf("\tNAME -> %.*s\n", (int) resp->name.len, resp->name.buf); + if (resp->sd.port != 0) + printf("\tPORT -> %u\n", resp->sd.port); + if (resp->addr.addr.ip4 != 0) { + mg_snprintf(addr, sizeof(addr), "%M", mg_print_ip, &resp->addr); + printf("\tADDRESS -> %s\n", addr); + } + if (resp->sd.txt.buf != NULL) + printf("\tTXT -> %.*s\n", (int) resp->sd.txt.len, resp->sd.txt.buf); + if (resp->addr.addr.ip4 != 0 && resp->addr.port != 0) { + mg_snprintf(addr, sizeof(addr), "http://%M:%u", mg_print_ip, &resp->addr, + mg_ntohs(resp->addr.port)); + printf("\tURL -> %s\n", addr); + } else if (resp->sd.port != 0 && resp->name.buf != NULL) { + printf("\tURL -> http://%.*s:%u\n", (int) resp->name.len, + resp->name.buf, resp->sd.port); + } + printf("\n"); + } + } + (void) c; +} + +int main(void) { + struct mg_mgr mgr; + char query[256]; + static struct mg_connection *c; + mg_log_set(MG_LL_DEBUG); // Set log level + mg_mgr_init(&mgr); // Initialise event manager + + c = mg_mdns_listen(&mgr, mdns_ev_handler, s_name); // Start mDNS server + if (c == NULL) return 1; + mg_snprintf(query, sizeof(query), "%s.local", s_service); + // Request service providers + if (!mg_mdns_query(c, query, MG_DNS_RTYPE_PTR)) return 1; + + for (;;) mg_mgr_poll(&mgr, 1000); // Event loop + + mg_mgr_free(&mgr); + return 0; +} diff --git a/tutorials/udp/mdns-sd-client/mongoose.c b/tutorials/udp/mdns-sd-client/mongoose.c new file mode 120000 index 00000000..5e522bbc --- /dev/null +++ b/tutorials/udp/mdns-sd-client/mongoose.c @@ -0,0 +1 @@ +../../../mongoose.c \ No newline at end of file diff --git a/tutorials/udp/mdns-sd-client/mongoose.h b/tutorials/udp/mdns-sd-client/mongoose.h new file mode 120000 index 00000000..ee4ac823 --- /dev/null +++ b/tutorials/udp/mdns-sd-client/mongoose.h @@ -0,0 +1 @@ +../../../mongoose.h \ No newline at end of file