Merge pull request #3643 from cesanta/mdns

Add SD query and response to mDNS
This commit is contained in:
Sergio R. Caprile authored and GitHub committed 2026-07-24 11:50:09 -03:00
commit 61638caeae
10 files changed
+444 -93

No files matched your search

+154 -44
View File
@@ -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,
+3 -2
View File
@@ -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.