It’s been a while since I’ve written any advance C/C++ code, so I though this would be a nice challenge. I wanted a multiple domain DNS server running on my ESP32. There are lots of examples of single domain DNS resolution and multicast DNS. In fact, both of these are packages that can be easily imported, just include .h file and implement the required methods. The problem was single domain DNS didn’t work quiet well for my situation, I wanted multiple domains resolution. The ESPmDNS package seems to append “.local” at the end of any domain name and this made it very restrictive in terms of which domain names I could use.
My scenario is, I have 2 websites hosted in IIS on my new laptops and 1 website hosted in IIS on my old laptop and I wanted to browse to all 3 sites using my mobile phone. The solution I was looking for was my ESP32 to behave like a DHCP / DNS server. When my laptops and my mobile phone are connected to it, I should be able to browse to all 3 sites without having to do any additional domain to IP mapping. I also wanted it to be configurable, so my ESP32 could be updated with new domains and IPs without having to recompile the code every time. Eventually, I would like my Arduino Uno and additional ESP32s to connect to this DHCP / DNS ESP32 and access the .netCore WebAPIs running in my home network.
For this we need our ESP32 to take on 3 server roles: an Access Point / DHCP server, a web server and DNS server. Let’s look a little more into each role.
ESP32 Access Point / DHCP Server
This is quiet easy to achieve. There are a lot of examples on the internet around turning an ESP32 into an Access Point, but lets go over it really quick. We need to include WiFi.h, set the wifi mode to WIFI_AP, call softAP to set the Access Point name and call softAPConfig to set the router IP address and network mask. Any device that connects to our ESP32 Access Point should get an IP in the configured range using the subnet mask.
ESP32 Web Server
The next thing we want to do is have a Web Server running on port 80. This is so we can dynamically set our DNS settings. The GET request is going to give us a textbox to input our comma separated domains and IPs. Then we will have a POST action that will take the DNS settings and update the DNS mappings. And finally we need to start our UDP DNS server on port 53.
ESP32 DNS Server
The last piece of the puzzle. For this I’ve managed to located the single domain DNS server code in Espressif’s own Git hub repo and used it as the basis of my multi-domain DNS server code. This can be found here: https://github.com/espressif/arduino-esp32/tree/master/libraries/DNSServer/src
The Code
There are 3 files, MainServer.ino, DNSServer2.cpp and DNSServer2.h. MainServer.ino is where it all comes together. Lines 30-32 are where we setup our access point details and DHCP. Line 11 is where we initalise our Web server. Line 34-42 is our GET handler, this is where we render the textbox HTML to edit the DNS settings and also show any current DNS settings. Line 44-67 is our POST handler, this is where we process the textbox and update the DNS mappings. And at the bottom we have a few helper methods.
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "./DNSServer2.h"
const byte DNS_PORT = 53;
IPAddress mainServerIP(192,168,4,1);
IPAddress mainServerGatewayIP(192,168,4,1);
IPAddress mainServerIPMask(255,255,255,0);
DNSServer dnsServer;
AsyncWebServer httpServer(80);
const byte MAX_DNS_DOMAINS = 10;
String currentDnsSettings;
String dnsDomains[MAX_DNS_DOMAINS];
unsigned char dnsResolvedIP[MAX_DNS_DOMAINS][4];
const String responseHTMLStart = ""
"<!DOCTYPE html><html><head><title>My DNS Server</title></head><body>"
"<h1>My DNS Server</h1>"
"<p>Enter your DNS settings below, a comma separated list of domain names and resolve IPs:</p>"
"<p>e.g. www.mysecuresite.org,192.168.4.240,www.hello.com,192.168.4.240</p>"
"<form action=\"/update\" method=\"post\" id=\"dnsSettingsForm\" ><textarea id=\"dnsSettings\" name=\"dnsSettings\"></textarea><br/><input type=\"submit\"></form>";
const String responseHTMLEnd = "</body></html>";
const String responseHTMLDNSSettingTitle = "<br/><br/><h2>Current DNS settings are:</h2><br/>";
void setup() {
Serial.begin(9600);
Serial.println("Starting DHCP / DNS server");
WiFi.mode(WIFI_AP);
WiFi.softAP("Schubert.Codes.AP");
WiFi.softAPConfig(mainServerIP, mainServerGatewayIP, mainServerIPMask);
httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
if(currentDnsSettings == "") {
String response = responseHTMLStart + responseHTMLEnd;
request->send(200, "text/html", response);
} else {
String response = responseHTMLStart + responseHTMLDNSSettingTitle + currentDnsSettings + responseHTMLEnd;
request->send(200, "text/html", response);
}
});
httpServer.on("/update", HTTP_POST, [] (AsyncWebServerRequest *request) {
String dnsSettings;
if (request->hasParam("dnsSettings",true)) {
dnsSettings = request->getParam("dnsSettings", true)->value();
}
int separatorCount = GetCharacterCount(dnsSettings, ',');
if(separatorCount > 0 && separatorCount % 2 == 1)
{
clearCurrentDnsSettings();
String result[separatorCount + 1];
splitString(dnsSettings, ',', result, separatorCount + 1);
int index = 0;
for (int i = 0; i < separatorCount; i = i + 2) {
dnsDomains[index] = result[i];
splitIP(result[i+1], dnsResolvedIP[index]);
index++;
currentDnsSettings = currentDnsSettings + "<br/>" + result[i] + " mapped to " + result[i+1];
}
dnsServer.start(DNS_PORT, dnsDomains, dnsResolvedIP, separatorCount);
}
request->redirect("/");
});
httpServer.begin();
}
void loop() {
dnsServer.processNextRequest();
}
void splitString(String value, char separator, String* result, int arraySize) {
int startIndex = 0, resultCount = 0;
for (int i = 0; i < value.length(); i++)
{
if(value.charAt(i) == separator)
{
result[resultCount] = value.substring(startIndex, i);
startIndex = i + 1;
resultCount++;
if(resultCount > arraySize)
{
return;
}
}
}
result[resultCount] = value.substring(startIndex, value.length());
}
void splitIP(String value, unsigned char* resolvedIP) {
int startIndex = 0, resultCount = 0;
for (int i = 0; i < value.length(); i++)
{
if(value.charAt(i) == '.')
{
resolvedIP[resultCount] = value.substring(startIndex, i).toInt();
startIndex = i + 1;
resultCount++;
if(resultCount >= 4)
{
return;
}
}
}
resolvedIP[resultCount] = value.substring(startIndex, value.length()).toInt();
}
int GetCharacterCount(String value, char charToCount) {
int count = 0;
for (int i = 0; i < value.length(); i++)
{
if (value.charAt(i) == charToCount)
{
count++;
}
}
return count;
}
void clearCurrentDnsSettings()
{
currentDnsSettings = "";
for (int i = 0; i < MAX_DNS_DOMAINS; i++){
dnsDomains[i].clear();
}
}
Next lets look at DNSServer2.h file. This is a copy of Espressif’s DNSServer.h file with a few updates. Line 85 the start DNS server function now takes a list of domains as its second parameter, a list of IPs as its third parameter and the count i.e. number of domains as the forth parameter. And the other changes are on line 95, 96 and 98. 95 is now a pointer to String array called _domainNames. 96 is an array of resolution IPs. 98 is used to save the count. The replyWithIP method has also been updated to take an index on line 109.
I’ve put a limit of 10 domains, it can be easily updated by change this array size and updating the const MAX_DNS_DOMAINS in MainServer.ino. The rest of the code stays the same.
#ifndef DNSServer_h
#define DNSServer_h
#include <WiFiUdp.h>
#define DNS_QR_QUERY 0
#define DNS_QR_RESPONSE 1
#define DNS_OPCODE_QUERY 0
#define DNS_DEFAULT_TTL 60 // Default Time To Live : time interval in seconds that the resource record should be cached before being discarded
#define DNS_OFFSET_DOMAIN_NAME 12 // Offset in bytes to reach the domain name in the DNS message
#define DNS_HEADER_SIZE 12
enum class DNSReplyCode
{
NoError = 0,
FormError = 1,
ServerFailure = 2,
NonExistentDomain = 3,
NotImplemented = 4,
Refused = 5,
YXDomain = 6,
YXRRSet = 7,
NXRRSet = 8
};
enum DNSType
{
DNS_TYPE_A = 1, // Host Address
DNS_TYPE_AAAA = 28, // IPv6 Address
DNS_TYPE_SOA = 6, // Start Of a zone of Authority
DNS_TYPE_PTR = 12, // Domain name PoinTeR
DNS_TYPE_DNAME = 39 // Delegation Name
} ;
enum DNSClass
{
DNS_CLASS_IN = 1, // INternet
DNS_CLASS_CH = 3 // CHaos
} ;
enum DNSRDLength
{
DNS_RDLENGTH_IPV4 = 4 // 4 bytes for an IPv4 address
} ;
struct DNSHeader
{
uint16_t ID; // identification number
union {
struct {
uint16_t RD : 1; // recursion desired
uint16_t TC : 1; // truncated message
uint16_t AA : 1; // authoritive answer
uint16_t OPCode : 4; // message_type
uint16_t QR : 1; // query/response flag
uint16_t RCode : 4; // response code
uint16_t Z : 3; // its z! reserved
uint16_t RA : 1; // recursion available
};
uint16_t Flags;
};
uint16_t QDCount; // number of question entries
uint16_t ANCount; // number of answer entries
uint16_t NSCount; // number of authority entries
uint16_t ARCount; // number of resource entries
};
struct DNSQuestion
{
uint8_t QName[256] ; //need 1 Byte for zero termination!
uint16_t QNameLength ;
uint16_t QType ;
uint16_t QClass ;
} ;
class DNSServer
{
public:
DNSServer();
~DNSServer();
void processNextRequest();
void setErrorReplyCode(const DNSReplyCode &replyCode);
void setTTL(const uint32_t &ttl);
// Returns true if successful, false if there are no sockets available
bool start(const uint16_t &port,
String domainNames[],
unsigned char resolvedIPs[][4],
int &count);
// stops the DNS server
void stop();
private:
WiFiUDP _udp;
uint16_t _port;
String* _domainNames;
unsigned char _resolvedIPs[10][4];
int _currentPacketSize;
int _count;
unsigned char* _buffer;
DNSHeader* _dnsHeader;
uint32_t _ttl;
DNSReplyCode _errorReplyCode;
DNSQuestion* _dnsQuestion ;
void downcaseAndRemoveWwwPrefix(String &domainNames);
String getDomainNameWithoutWwwPrefix();
bool requestIncludesOnlyOneQuestion();
void replyWithIP(int index);
void replyWithCustomCode();
};
#endif
And finally the DNSServer2.cpp file. This is a copy of Espressif’s DNSServer.cpp file with a few updates. The 1st change is line 39, the start parameters are updated to match the definition in the DNSServer2.h file. Line 44, we are saving the domain names, 45 the count, and 48 to 50 the resolution IPs. Line 46 is now a loop to lowercase and remove the prefix from all the domain names.
In processNextRequest method, this actually gets called from the loop method in MainServer.ino, on line 118 to 124, this is now a loop. We loop over all the domain names to find a match and if we do, we pass the index to replyWithIP method. On line 108, we get the index as a parameter. And the final update is on line 211, this is where we are returning the corresponding IP and the DNS resolution is complete. The rest of the code stays the same
#include "./DNSServer2.h"
#include <lwip/def.h>
#include <Arduino.h>
// #define DEBUG_ESP_DNS
#ifdef DEBUG_ESP_PORT
#define DEBUG_OUTPUT DEBUG_ESP_PORT
#else
#define DEBUG_OUTPUT Serial
#endif
DNSServer::DNSServer()
{
_ttl = htonl(DNS_DEFAULT_TTL);
_errorReplyCode = DNSReplyCode::NonExistentDomain;
_dnsHeader = (DNSHeader*) malloc( sizeof(DNSHeader) ) ;
_dnsQuestion = (DNSQuestion*) malloc( sizeof(DNSQuestion) ) ;
_buffer = NULL;
_currentPacketSize = 0;
_port = 0;
}
DNSServer::~DNSServer()
{
if (_dnsHeader) {
free(_dnsHeader);
_dnsHeader = NULL;
}
if (_dnsQuestion) {
free(_dnsQuestion);
_dnsQuestion = NULL;
}
if (_buffer) {
free(_buffer);
_buffer = NULL;
}
}
bool DNSServer::start(const uint16_t &port, String domainNames[],
unsigned char resolvedIPs[][4], int &count)
{
_port = port;
_buffer = NULL;
_domainNames = domainNames;
_count = count;
for(int i =0; i< count; i++){
downcaseAndRemoveWwwPrefix(_domainNames[i]);
for(int j=0; j< 4;j++)
_resolvedIPs[i][j] = resolvedIPs[i][j];
}
return _udp.begin(_port) == 1;
}
void DNSServer::setErrorReplyCode(const DNSReplyCode &replyCode)
{
_errorReplyCode = replyCode;
}
void DNSServer::setTTL(const uint32_t &ttl)
{
_ttl = htonl(ttl);
}
void DNSServer::stop()
{
_udp.stop();
free(_buffer);
_buffer = NULL;
}
void DNSServer::downcaseAndRemoveWwwPrefix(String &domainName)
{
domainName.toLowerCase();
domainName.replace("www.", "");
}
void DNSServer::processNextRequest()
{
_currentPacketSize = _udp.parsePacket();
if (_currentPacketSize)
{
// Allocate buffer for the DNS query
if (_buffer != NULL)
free(_buffer);
_buffer = (unsigned char*)malloc(_currentPacketSize * sizeof(char));
if (_buffer == NULL)
return;
// Put the packet received in the buffer and get DNS header (beginning of message)
// and the question
_udp.read(_buffer, _currentPacketSize);
memcpy( _dnsHeader, _buffer, DNS_HEADER_SIZE ) ;
if ( requestIncludesOnlyOneQuestion() )
{
// The QName has a variable length, maximum 255 bytes and is comprised of multiple labels.
// Each label contains a byte to describe its length and the label itself. The list of
// labels terminates with a zero-valued byte. In "github.com", we have two labels "github" & "com"
// Iterate through the labels and copy them as they come into a single buffer (for simplicity's sake)
_dnsQuestion->QNameLength = 0 ;
while ( _buffer[ DNS_HEADER_SIZE + _dnsQuestion->QNameLength ] != 0 )
{
memcpy( (void*) &_dnsQuestion->QName[_dnsQuestion->QNameLength], (void*) &_buffer[DNS_HEADER_SIZE + _dnsQuestion->QNameLength], _buffer[DNS_HEADER_SIZE + _dnsQuestion->QNameLength] + 1 ) ;
_dnsQuestion->QNameLength += _buffer[DNS_HEADER_SIZE + _dnsQuestion->QNameLength] + 1 ;
}
_dnsQuestion->QName[_dnsQuestion->QNameLength] = 0 ;
_dnsQuestion->QNameLength++ ;
// Copy the QType and QClass
memcpy( &_dnsQuestion->QType, (void*) &_buffer[DNS_HEADER_SIZE + _dnsQuestion->QNameLength], sizeof(_dnsQuestion->QType) ) ;
memcpy( &_dnsQuestion->QClass, (void*) &_buffer[DNS_HEADER_SIZE + _dnsQuestion->QNameLength + sizeof(_dnsQuestion->QType)], sizeof(_dnsQuestion->QClass) ) ;
}
if (_dnsHeader->QR == DNS_QR_QUERY &&
_dnsHeader->OPCode == DNS_OPCODE_QUERY &&
requestIncludesOnlyOneQuestion())
{
for(int i = 0; i < _count; i++)
{
if(getDomainNameWithoutWwwPrefix() == _domainNames[i]){
replyWithIP(i);
}
}
}
else if (_dnsHeader->QR == DNS_QR_QUERY)
{
replyWithCustomCode();
}
free(_buffer);
_buffer = NULL;
}
}
bool DNSServer::requestIncludesOnlyOneQuestion()
{
return ntohs(_dnsHeader->QDCount) == 1 &&
_dnsHeader->ANCount == 0 &&
_dnsHeader->NSCount == 0 &&
_dnsHeader->ARCount == 0;
}
String DNSServer::getDomainNameWithoutWwwPrefix()
{
// Error checking : if the buffer containing the DNS request is a null pointer, return an empty domain
String parsedDomainName = "";
if (_buffer == NULL)
return parsedDomainName;
// Set the start of the domain just after the header (12 bytes). If equal to null character, return an empty domain
unsigned char *start = _buffer + DNS_OFFSET_DOMAIN_NAME;
if (*start == 0)
{
return parsedDomainName;
}
int pos = 0;
while(true)
{
unsigned char labelLength = *(start + pos);
for(int i = 0; i < labelLength; i++)
{
pos++;
parsedDomainName += (char)*(start + pos);
}
pos++;
if (*(start + pos) == 0)
{
downcaseAndRemoveWwwPrefix(parsedDomainName);
return parsedDomainName;
}
else
{
parsedDomainName += ".";
}
}
}
void DNSServer::replyWithIP(int index)
{
if (_buffer == NULL) return;
_udp.beginPacket(_udp.remoteIP(), _udp.remotePort());
// 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;
_dnsHeader->ANCount = _dnsHeader->QDCount;
_udp.write( (unsigned char*) _dnsHeader, DNS_HEADER_SIZE ) ;
// Write the question
_udp.write(_dnsQuestion->QName, _dnsQuestion->QNameLength) ;
_udp.write( (unsigned char*) &_dnsQuestion->QType, 2 ) ;
_udp.write( (unsigned char*) &_dnsQuestion->QClass, 2 ) ;
// Write the answer
// Use DNS name compression : instead of repeating the name in this RNAME occurence,
// 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)
_udp.write((uint8_t) 0xC0);
_udp.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_A), answerClass = htons(DNS_CLASS_IN), answerIPv4 = htons(DNS_RDLENGTH_IPV4) ;
_udp.write((unsigned char*) &answerType, 2 );
_udp.write((unsigned char*) &answerClass, 2 );
_udp.write((unsigned char*) &_ttl, 4); // DNS Time To Live
_udp.write((unsigned char*) &answerIPv4, 2 );
_udp.write(_resolvedIPs[index], sizeof(_resolvedIPs[index])); // The IP address to return
_udp.endPacket();
#ifdef DEBUG_ESP_DNS
DEBUG_OUTPUT.printf("DNS responds: %s for %s\n",
IPAddress(_resolvedIP).toString().c_str(), getDomainNameWithoutWwwPrefix().c_str() );
#endif
}
void DNSServer::replyWithCustomCode()
{
if (_buffer == NULL) return;
_dnsHeader->QR = DNS_QR_RESPONSE;
_dnsHeader->RCode = (unsigned char)_errorReplyCode;
_dnsHeader->QDCount = 0;
_udp.beginPacket(_udp.remoteIP(), _udp.remotePort());
_udp.write(_buffer, sizeof(DNSHeader));
_udp.endPacket();
}
The DNS server in Action
When the code is compiled and deployed on our ESP32 router, we should be able to connect to the Access Point. Then browse to the IP of the ESP32, it should be 192.168.4.1 and you should see something like this
Next find the IP of your machine hosting the Websites. Mine were hosted in IIS and these were the site names:
And I managed to get my IP of this machine, it was 192.168.1.2. So my DNS setup looked like this:
And after clicking submit, the page redirect back to the home page, but this time the mappings are listed below. We can browse to the home page at any time and see the current setting or update them
And finally, from my mobile device, with no extra effort I can now browse to www.hello.com and www.mysecuresite.org. My hello.com just render Hello.com in a H1 tag and as you might know from my previous blog post, www.mysecuresite.org ask for a client certificate. First the DNS mappings are visible here too
and Hello.com comes up as
and mysecuresite.org comes up as
Final Thoughts
I’ve not secure the Access Point or locked down the DNS dynamic setting page. This can be achieved by setting a password on the Access Point and requesting a username and password for the settings page. I’ll leave this as a to do for anyone interested in locking down their DNS settings
There is a constant, MAX_DNS_DOMAINS. This limits the maximum number to DNS and IP mappings to 10. Malloc can be used to make this value dynamic. In my case, 10 fits the number of domains I’ll be working with for now.
Happy Coding!