From 730b9ce5eea6acad1b0e3f5034c97390c29c3c0d Mon Sep 17 00:00:00 2001 From: kolkman Date: Fri, 30 May 2025 09:31:22 +0200 Subject: [PATCH 1/6] Proper EDNS handling and cleaner NOERROR response --- libraries/DNSServer/library.properties | 2 +- libraries/DNSServer/src/DNSServer.cpp | 89 ++++++++++++++++++++++++-- libraries/DNSServer/src/DNSServer.h | 21 +++++- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/libraries/DNSServer/library.properties b/libraries/DNSServer/library.properties index 5e70a6ec03a..c193b919d02 100644 --- a/libraries/DNSServer/library.properties +++ b/libraries/DNSServer/library.properties @@ -1,5 +1,5 @@ name=DNSServer -version=3.2.0 +version=3.3.0 author=Kristijan Novoselić maintainer=Kristijan Novoselić, sentence=A simple DNS server for ESP32. diff --git a/libraries/DNSServer/src/DNSServer.cpp b/libraries/DNSServer/src/DNSServer.cpp index 28cf89d6ede..07f93e3ef44 100644 --- a/libraries/DNSServer/src/DNSServer.cpp +++ b/libraries/DNSServer/src/DNSServer.cpp @@ -15,7 +15,7 @@ DNSServer::DNSServer() : _port(DNS_DEFAULT_PORT), _ttl(htonl(DNS_DEFAULT_TTL)), _errorReplyCode(DNSReplyCode::NonExistentDomain) {} DNSServer::DNSServer(const String &domainName) - : _port(DNS_DEFAULT_PORT), _ttl(htonl(DNS_DEFAULT_TTL)), _errorReplyCode(DNSReplyCode::NonExistentDomain), _domainName(domainName){}; + : _port(DNS_DEFAULT_PORT), _ttl(htonl(DNS_DEFAULT_TTL)), _errorReplyCode(DNSReplyCode::NonExistentDomain), _domainName(domainName) {}; bool DNSServer::start() { if (_resolvedIP.operator uint32_t() == 0) { // no address is set, try to obtain AP interface's IP @@ -111,16 +111,22 @@ void DNSServer::_handleUDP(AsyncUDPPacket &pkt) { // will reply with IP only to "*" or if domain matches without www. subdomain if (dnsHeader.OPCode == DNS_OPCODE_QUERY && requestIncludesOnlyOneQuestion(dnsHeader) && (_domainName.isEmpty() || getDomainNameWithoutWwwPrefix(static_cast(dnsQuestion.QName), dnsQuestion.QNameLength) == _domainName)) { - replyWithIP(pkt, dnsHeader, dnsQuestion); + + // Qtype = A (1) or ANY (255): send an A record otherwise an empty response + if (ntohs(dnsQuestion.QType) == 1 || ntohs(dnsQuestion.QType) == 255) { + replyWithIP(pkt, dnsHeader, dnsQuestion); + } else { + replyWithNoAnsw(pkt, dnsHeader, dnsQuestion); + } return; } - // otherwise reply with custom code replyWithCustomCode(pkt, dnsHeader); } bool DNSServer::requestIncludesOnlyOneQuestion(DNSHeader &dnsHeader) { - return ntohs(dnsHeader.QDCount) == 1 && dnsHeader.ANCount == 0 && dnsHeader.NSCount == 0 && dnsHeader.ARCount == 0; + dnsHeader.ARCount = 0; // We assume that if ARCount !=0 there is a EDNS OPT packet, just ignore + return ntohs(dnsHeader.QDCount) == 1 && dnsHeader.ANCount == 0 && dnsHeader.NSCount == 0; // && dnsHeader.ARCount == 0; } String DNSServer::getDomainNameWithoutWwwPrefix(const unsigned char *start, size_t len) { @@ -139,7 +145,6 @@ String DNSServer::getDomainNameWithoutWwwPrefix(const unsigned char *start, size void DNSServer::replyWithIP(AsyncUDPPacket &req, DNSHeader &dnsHeader, DNSQuestion &dnsQuestion) { AsyncUDPMessage rpl; - // Change the type of message to a response and set the number of answers equal to // the number of questions in the header dnsHeader.QR = DNS_QR_RESPONSE; @@ -187,3 +192,77 @@ void DNSServer::replyWithCustomCode(AsyncUDPPacket &req, DNSHeader &dnsHeader) { rpl.write(reinterpret_cast(&dnsHeader), sizeof(DNSHeader)); _udp.sendTo(rpl, req.remoteIP(), req.remotePort()); } + +void DNSServer::replyWithNoAnsw(AsyncUDPPacket &req, DNSHeader &dnsHeader, DNSQuestion &dnsQuestion) { + + dnsHeader.QR = DNS_QR_RESPONSE; + // dnsHeader.QDCount = 1; + dnsHeader.ANCount = 0; + dnsHeader.NSCount = htons(1); + + AsyncUDPMessage rpl; + rpl.write(reinterpret_cast(&dnsHeader), sizeof(DNSHeader)); + + // Write the question + rpl.write(dnsQuestion.QName, dnsQuestion.QNameLength); + rpl.write((uint8_t *)&dnsQuestion.QType, 2); + rpl.write((uint8_t *)&dnsQuestion.QClass, 2); + + // An empty answer contains an authority section with a SOA, + // We take the name of the query as the root of the zone for which the SOA is generated + // and use a value of DNS_MINIMAL_TTL seconds in order to minimize negative caching + // Write the authority section: + // The SOA RR's ownername is set equal to the query name, and we use made up names for + // the MNAME and RNAME - it doesn't really matter from a protocol perspective - as for + // a no such QTYPE answer only the timing fields are used. + // a protocol perspective - it + // Use DNS name compression : instead of repeating the name in this RNAME occurrence, + // set the two MSB of the byte corresponding normally to the length to 1. The following + // 14 bits must be used to specify the offset of the domain name in the message + // (<255 here so the first byte has the 6 LSB at 0) + rpl.write((uint8_t)0xC0); + rpl.write((uint8_t)DNS_OFFSET_DOMAIN_NAME); + + // DNS type A : host address, DNS class IN for INternet, returning an IPv4 address + uint16_t answerType = htons(DNS_TYPE_SOA), answerClass = htons(DNS_CLASS_IN); + uint32_t Serial = htonl(DNS_SOA_SERIAL); // Date type serial based on the date this piece of code was written + uint32_t Refresh = htonl(DNS_SOA_REFRESH); // These timers don't matter, we don't serve zone transfers + uint32_t Retry = htonl(DNS_SOA_RETRY); + uint32_t Expire = htonl(DNS_SOA_EXPIRE); + uint32_t MinTTL = htonl(DNS_MINIMAL_TTL); // See RFC2308 section 5 + char MLabel[] = DNS_SOA_MNAME_LABEL; + char RLabel[] = DNS_SOA_RNAME_LABEL; + char PostFixLabel[] = DNS_SOA_POSTFIX_LABEL; + + // 4 accounts for len fields and for both rname + // and lname and their postfix labels and there are 5 32 bit fields + + uint16_t RdataLength = htons((uint16_t)(strlen(MLabel) + strlen(RLabel) + 2 * strlen(PostFixLabel) + 4 + 5 * sizeof(Serial))); + + rpl.write((unsigned char *)&answerType, 2); + rpl.write((unsigned char *)&answerClass, 2); + rpl.write((unsigned char *)&MinTTL, 4); // DNS Time To Live + + rpl.write((unsigned char *)&RdataLength, 2); + + rpl.write((uint8_t)strlen(MLabel)); + rpl.write((unsigned char *)&MLabel, strlen(MLabel)); + + rpl.write((unsigned char *)&PostFixLabel, strlen(PostFixLabel)); + rpl.write((uint8_t)0); + // rpl.write((uint8_t)0xC0); + // rpl.write((uint8_t)DNS_OFFSET_DOMAIN_NAME); + + rpl.write((uint8_t)strlen(RLabel)); + rpl.write((unsigned char *)&RLabel, strlen(RLabel)); + rpl.write((unsigned char *)&PostFixLabel, strlen(PostFixLabel)); + rpl.write((uint8_t)0); + + rpl.write((unsigned char *)&Serial, 4); + rpl.write((unsigned char *)&Refresh, 4); + rpl.write((unsigned char *)&Retry, 4); + rpl.write((unsigned char *)&Expire, 4); + rpl.write((unsigned char *)&MinTTL, 4); + + _udp.sendTo(rpl, req.remoteIP(), req.remotePort()); +} diff --git a/libraries/DNSServer/src/DNSServer.h b/libraries/DNSServer/src/DNSServer.h index dfd9a45604d..cab065578e3 100644 --- a/libraries/DNSServer/src/DNSServer.h +++ b/libraries/DNSServer/src/DNSServer.h @@ -9,6 +9,23 @@ #define DNS_OFFSET_DOMAIN_NAME DNS_HEADER_SIZE // Offset in bytes to reach the domain name labels in the DNS message #define DNS_DEFAULT_PORT 53 +#define DNS_SOA_MNAME_LABEL "ns" +#define DNS_SOA_RNAME_LABEL "esp32" +// The POSTFIX_LABEL will be concatinated to the RName and MName Label label +// do not use a multilabel name here. "local" is a good choice as it is reserved for +// local use by IANA +// The postfix label is defined as an array of characters that follows the +// definition of RFC1035 3.1 +// for instance, a postfix of example.com would be defined as: +// #define DNS_SOA_POSTFIX_LABEL {'\7', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '\3', 'c', 'o', 'm', '\0'} +#define DNS_SOA_POSTFIX_LABEL {'\5', 'l', 'o', 'c', 'a', 'l', '\0'} +// From the following values only the MINIMAL_TTL has relevance +// in the context of client-server protocol interactions. +#define DNS_SOA_SERIAL 2025052900 // Arbitrary +#define DNS_SOA_REFRESH 100000 // Arbitrary +#define DNS_SOA_RETRY 10000 // Arbitrary +#define DNS_SOA_EXPIRE 1000000 // Arbitrary +#define DNS_MINIMAL_TTL 5 // Time to live for negative answers RFC2308 enum class DNSReplyCode : uint16_t { NoError = 0, FormError = 1, @@ -82,7 +99,7 @@ class DNSServer { * @param domainName - domain name to serve */ DNSServer(const String &domainName); - ~DNSServer(){}; // default d-tor + ~DNSServer() {}; // default d-tor // Copy semantics not implemented (won't run on same UDP port anyway) DNSServer(const DNSServer &) = delete; @@ -179,5 +196,7 @@ class DNSServer { inline bool requestIncludesOnlyOneQuestion(DNSHeader &dnsHeader); void replyWithIP(AsyncUDPPacket &req, DNSHeader &dnsHeader, DNSQuestion &dnsQuestion); inline void replyWithCustomCode(AsyncUDPPacket &req, DNSHeader &dnsHeader); + inline void replyWithNoAnsw(AsyncUDPPacket &req, DNSHeader &dnsHeader, DNSQuestion &dnsQuestion); + void _handleUDP(AsyncUDPPacket &pkt); }; From 5323ee29f92c9d25372138de20f89841e45faa21 Mon Sep 17 00:00:00 2001 From: Olaf Date: Sat, 31 May 2025 09:59:42 +0200 Subject: [PATCH 2/6] fix: library.properties reverting version number update - as it is done automatically --- libraries/DNSServer/library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/DNSServer/library.properties b/libraries/DNSServer/library.properties index c193b919d02..5e70a6ec03a 100644 --- a/libraries/DNSServer/library.properties +++ b/libraries/DNSServer/library.properties @@ -1,5 +1,5 @@ name=DNSServer -version=3.3.0 +version=3.2.0 author=Kristijan Novoselić maintainer=Kristijan Novoselić, sentence=A simple DNS server for ESP32. From 557d9c73cc5d71cb0acca774dafbfdd4465ad835 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:22:56 +0000 Subject: [PATCH 3/6] ci(pre-commit): Apply automatic fixes --- libraries/DNSServer/src/DNSServer.cpp | 2 +- libraries/DNSServer/src/DNSServer.h | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/DNSServer/src/DNSServer.cpp b/libraries/DNSServer/src/DNSServer.cpp index 07f93e3ef44..36fc8089410 100644 --- a/libraries/DNSServer/src/DNSServer.cpp +++ b/libraries/DNSServer/src/DNSServer.cpp @@ -15,7 +15,7 @@ DNSServer::DNSServer() : _port(DNS_DEFAULT_PORT), _ttl(htonl(DNS_DEFAULT_TTL)), _errorReplyCode(DNSReplyCode::NonExistentDomain) {} DNSServer::DNSServer(const String &domainName) - : _port(DNS_DEFAULT_PORT), _ttl(htonl(DNS_DEFAULT_TTL)), _errorReplyCode(DNSReplyCode::NonExistentDomain), _domainName(domainName) {}; + : _port(DNS_DEFAULT_PORT), _ttl(htonl(DNS_DEFAULT_TTL)), _errorReplyCode(DNSReplyCode::NonExistentDomain), _domainName(domainName){}; bool DNSServer::start() { if (_resolvedIP.operator uint32_t() == 0) { // no address is set, try to obtain AP interface's IP diff --git a/libraries/DNSServer/src/DNSServer.h b/libraries/DNSServer/src/DNSServer.h index cab065578e3..9ef551375ad 100644 --- a/libraries/DNSServer/src/DNSServer.h +++ b/libraries/DNSServer/src/DNSServer.h @@ -18,7 +18,8 @@ // definition of RFC1035 3.1 // for instance, a postfix of example.com would be defined as: // #define DNS_SOA_POSTFIX_LABEL {'\7', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '\3', 'c', 'o', 'm', '\0'} -#define DNS_SOA_POSTFIX_LABEL {'\5', 'l', 'o', 'c', 'a', 'l', '\0'} +#define DNS_SOA_POSTFIX_LABEL \ + { '\5', 'l', 'o', 'c', 'a', 'l', '\0' } // From the following values only the MINIMAL_TTL has relevance // in the context of client-server protocol interactions. #define DNS_SOA_SERIAL 2025052900 // Arbitrary @@ -99,7 +100,7 @@ class DNSServer { * @param domainName - domain name to serve */ DNSServer(const String &domainName); - ~DNSServer() {}; // default d-tor + ~DNSServer(){}; // default d-tor // Copy semantics not implemented (won't run on same UDP port anyway) DNSServer(const DNSServer &) = delete; From c400f34355e9912c730983a3610d0340f98a3f65 Mon Sep 17 00:00:00 2001 From: Olaf Kolkman Date: Thu, 5 Jun 2025 08:54:42 +0200 Subject: [PATCH 4/6] Spelling Corrected and minor clarification in comments --- libraries/DNSServer/src/DNSServer.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/DNSServer/src/DNSServer.h b/libraries/DNSServer/src/DNSServer.h index 9ef551375ad..ac86bb330ee 100644 --- a/libraries/DNSServer/src/DNSServer.h +++ b/libraries/DNSServer/src/DNSServer.h @@ -11,7 +11,7 @@ #define DNS_SOA_MNAME_LABEL "ns" #define DNS_SOA_RNAME_LABEL "esp32" -// The POSTFIX_LABEL will be concatinated to the RName and MName Label label +// The POSTFIX_LABEL will be concatenated to the RName and MName Label label // do not use a multilabel name here. "local" is a good choice as it is reserved for // local use by IANA // The postfix label is defined as an array of characters that follows the @@ -22,10 +22,12 @@ { '\5', 'l', 'o', 'c', 'a', 'l', '\0' } // From the following values only the MINIMAL_TTL has relevance // in the context of client-server protocol interactions. -#define DNS_SOA_SERIAL 2025052900 // Arbitrary -#define DNS_SOA_REFRESH 100000 // Arbitrary -#define DNS_SOA_RETRY 10000 // Arbitrary -#define DNS_SOA_EXPIRE 1000000 // Arbitrary +// The other vallues are arbitrary chosen as they are only relevant for +// in a zone-transfer scenario. +#define DNS_SOA_SERIAL 2025052900 // Arbitrary serial (format: YYYYMMDDnn) +#define DNS_SOA_REFRESH 100000 // Arbitrary (seconds) +#define DNS_SOA_RETRY 10000 // Arbitrary (seconds) +#define DNS_SOA_EXPIRE 1000000 // Arbitrary (seconds) #define DNS_MINIMAL_TTL 5 // Time to live for negative answers RFC2308 enum class DNSReplyCode : uint16_t { NoError = 0, From e64e96ac4e42f3e341247e55bb9f29832efbb537 Mon Sep 17 00:00:00 2001 From: Olaf Kolkman Date: Thu, 5 Jun 2025 08:57:34 +0200 Subject: [PATCH 5/6] Removing commented out code fragments --- libraries/DNSServer/src/DNSServer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/DNSServer/src/DNSServer.cpp b/libraries/DNSServer/src/DNSServer.cpp index 36fc8089410..640690f7de4 100644 --- a/libraries/DNSServer/src/DNSServer.cpp +++ b/libraries/DNSServer/src/DNSServer.cpp @@ -126,7 +126,7 @@ void DNSServer::_handleUDP(AsyncUDPPacket &pkt) { bool DNSServer::requestIncludesOnlyOneQuestion(DNSHeader &dnsHeader) { dnsHeader.ARCount = 0; // We assume that if ARCount !=0 there is a EDNS OPT packet, just ignore - return ntohs(dnsHeader.QDCount) == 1 && dnsHeader.ANCount == 0 && dnsHeader.NSCount == 0; // && dnsHeader.ARCount == 0; + return ntohs(dnsHeader.QDCount) == 1 && dnsHeader.ANCount == 0 && dnsHeader.NSCount == 0; } String DNSServer::getDomainNameWithoutWwwPrefix(const unsigned char *start, size_t len) { @@ -196,7 +196,6 @@ void DNSServer::replyWithCustomCode(AsyncUDPPacket &req, DNSHeader &dnsHeader) { void DNSServer::replyWithNoAnsw(AsyncUDPPacket &req, DNSHeader &dnsHeader, DNSQuestion &dnsQuestion) { dnsHeader.QR = DNS_QR_RESPONSE; - // dnsHeader.QDCount = 1; dnsHeader.ANCount = 0; dnsHeader.NSCount = htons(1); From 28fc7e40fddf26147d2664311567b154c52f1a95 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 07:55:03 +0000 Subject: [PATCH 6/6] ci(pre-commit): Apply automatic fixes --- libraries/DNSServer/src/DNSServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/DNSServer/src/DNSServer.cpp b/libraries/DNSServer/src/DNSServer.cpp index 640690f7de4..1f74c96c733 100644 --- a/libraries/DNSServer/src/DNSServer.cpp +++ b/libraries/DNSServer/src/DNSServer.cpp @@ -126,7 +126,7 @@ void DNSServer::_handleUDP(AsyncUDPPacket &pkt) { bool DNSServer::requestIncludesOnlyOneQuestion(DNSHeader &dnsHeader) { dnsHeader.ARCount = 0; // We assume that if ARCount !=0 there is a EDNS OPT packet, just ignore - return ntohs(dnsHeader.QDCount) == 1 && dnsHeader.ANCount == 0 && dnsHeader.NSCount == 0; + return ntohs(dnsHeader.QDCount) == 1 && dnsHeader.ANCount == 0 && dnsHeader.NSCount == 0; } String DNSServer::getDomainNameWithoutWwwPrefix(const unsigned char *start, size_t len) {