1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-04-21 10:26:06 +03:00

Add DNS forwarder to DNSServer (#7237)

The key functions added are:

`bool enableForwarder(const String &domainName=emptyString, const IPAddress &dns=uint32_t)0)`

If specified, `enableForwarder` will update the `domainName` that is used to match DNS request to this AP's IP Address. A non-matching request will be forwarded to the DNS server specified by `dns`. 

Returns `true` on success.

Returns `false`, 
 * when forwarding `dns` is not set, or 
 * unable to allocate resources for managing the DNS forward function.

`void disableForwarder(const String &domainName=emptyString, bool freeResources=false)`

`disableForwarder` will stop forwarding DNS requests. If specified, updates the `domainName` that is matched for returning this AP's IP Address.
Optionally, resources used for the DNS forward function can be freed.
This commit is contained in:
M Hightower 2022-05-08 04:04:34 -07:00 committed by GitHub
parent 1a49a0449b
commit bcb5464167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1432 additions and 47 deletions

View File

@ -0,0 +1,391 @@
/*
This example shows the use of the 'DNS forwarder' feature in the DNSServer.
It does so by combining two examples CaptivePortalAdvanced and
RangeExtender-NAPT. Additionally the CaptivePortalAdvanced part has a few
upgrades to the HTML presentation to improve readability and ease of use on
mobile devices.
Also for an example of using HTML chunked response, see handleWifi() in
handleHttp.ino.
This example starts up in Captive Portal mode by default.
It starts the SoftAP and NAPT w/o connecting the WLAN side.
You connect your computer or mobile device to the WiFi Network 'MagicPortal'
password 'ShowTime'. Your device should shortly notify you of a Captive
Portal and the need to login. If it fails to do so in a timely maner,
navigate to http://172.217.28.1/wifi and configure it there.
Note, until a successful WLAN connection is made all DNS lookups will point
back to the SoftAP at 172.217.28.1. This is the Captive Portal element of
this example.
Once the WLAN is connected, your device should notify you that you are
connected. This, of course, assumes your WLAN connection has a path to the
Internet.
At this stage we are no longer running as a Captive Portal, but a regular
NAPT. The DNSServer will be running with the DNS forwarder enabled. The
DNSServer will resolve lookups for 'margicportal' to point to 172.217.28.1
and all other lookup request will be forwarded to the 1st DNS server that was
in the DHCP response for the WLAN interface.
You should now be able to access things on the Internet. The ease of access
to devices on your home Network may vary. By IP address it should work.
Access by a hostname - maybe. Some home routers will use the hostname
supplied during DHCP to support a local DNS table; some do not.
There is an additional possible complication for using the local DNS, the DNS
suffix list, this subject is seldom discussed. It is normally handled
automaticly by the host computers DNS lookup code. For the DHCP case, the
DHCP server will supply a suffix list, if there is one. Then when a name
lookup fails and the name does not have a trailing (.)dot the host computer
will append a suffix from the list and try again, until successful or the
list is exhaused. This topic I fear can become a TL;DR. A quick wrapup by way
of an example. On an Ubuntu system run `nmcli dev show eth0 | grep
IP4\.DOMAIN` that may show you a suffix list. (replace eth0 with your wlan
interface name) Try adding them to the local name you are failing to connect
to. For example, assume 'myhost' fails. You see that 'lan' is in the suffix
list. Try connecting to 'myhost.lan'.
mDNS names also will not work. We do not have a way to pass those request
back and forth through the NAPT.
Note if hostnames are going to work for an ESP8266 device on your home
Network, you have to have the call to WiFi.hostname(...) before you call
WiFi.begin().
In this example the SoftAP in 'Captive Portal' uses the same public address
that was used in the CaptivePortalAdvanced example. Depending on your devices
you may or may not be successful in using a private address. A previous
PR-author discovered a fix that made the CaptivePortalAdvanced example work
better with Android devices. That fix was to use that public address. At this
time, this PR-author with a different Android device running the latest
version of Android has seen no problems in using either. At least not yet :)
FWIW: My device also works with the original CaptivePortalAdvanced example
when using a private address. I would suggest keeping the public address
for a while. At lest until you are confident everything is working well
before experimenting with a private address.
*/
#if LWIP_FEATURES && !LWIP_IPV6
#include <ESP8266WiFi.h>
#include <lwip/napt.h>
#include <lwip/dns.h>
#include <dhcpserver.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#include <ESP8266mDNS.h>
#include <EEPROM.h>
#define NAPT 1000
#define NAPT_PORT 10
/*
Some defines for debugging
*/
#ifdef DEBUG_ESP_PORT
#define CONSOLE DEBUG_ESP_PORT
#else
#define CONSOLE Serial
#endif
#define _PRINTF(a, ...) printf_P(PSTR(a), ##__VA_ARGS__)
#define _PRINT(a) print(String(F(a)))
#define _PRINTLN(a) println(String(F(a)))
#define _PRINTLN2(a, b) println(String(F(a)) + b)
#define CONSOLE_PRINTF CONSOLE._PRINTF
#define CONSOLE_PRINT CONSOLE._PRINT
#define CONSOLE_PRINTLN CONSOLE._PRINTLN
#define CONSOLE_PRINTLN2 CONSOLE._PRINTLN2
#ifdef DEBUG_SKETCH
#define DEBUG_PRINTF CONSOLE_PRINTF
#define DEBUG_PRINT CONSOLE_PRINT
#define DEBUG_PRINTLN CONSOLE_PRINTLN
#define DEBUG_PRINTLN2 CONSOLE_PRINTLN2
#else
#define DEBUG_PRINTF(...) \
do { \
} while (false)
#define DEBUG_PRINT(...) \
do { \
} while (false)
#define DEBUG_PRINTLN(...) \
do { \
} while (false)
#define DEBUG_PRINTLN2(...) \
do { \
} while (false)
#endif
/* Set these to your desired softAP credentials. They are not configurable at runtime */
#ifndef APSSID
#define APSSID "MagicPortal"
#define APPSK "ShowTime"
#endif
const char *softAP_ssid = APSSID;
const char *softAP_password = APPSK;
/* hostname for mDNS. Should work at least on windows. Try http://esp8266.local */
const char *myHostname = "magicportal";
/* Don't set this wifi credentials. They are configurated at runtime and stored on EEPROM */
char ssid[33] = "";
char password[65] = "";
uint8_t bssid[6];
WiFiEventHandler staModeConnectedHandler;
WiFiEventHandler staModeDisconnectedHandler;
// DNS server
DNSServer dnsServer;
// Web server
ESP8266WebServer server(80);
/* Soft AP network parameters */
IPAddress apIP(172, 217, 28, 1);
IPAddress netMsk(255, 255, 255, 0);
/** Should I connect to WLAN asap? */
bool connect = false;
/** Set to true to start WiFi STA at setup time when credentials loaded successfuly from EEPROM */
/** Set to false to defer WiFi STA until configured through web interface. */
bool staReady = false; // Don't connect right away
/** Last time I tried to connect to WLAN */
unsigned long lastConnectTry = 0;
/** Current WLAN status */
unsigned int status = WL_IDLE_STATUS;
void setup() {
WiFi.persistent(false); // w/o this a flash write occurs at every boot
WiFi.mode(WIFI_OFF); // Prevent use of SDK stored credentials
CONSOLE.begin(115200);
CONSOLE_PRINTLN("\r\n\r\nNAPT with Configuration Portal ...");
staModeConnectedHandler = WiFi.onStationModeConnected(
[](const WiFiEventStationModeConnected &data) {
// Keep a copy of the BSSID for the AP that WLAN connects to.
// This is used in the WLAN report on WiFi Details page.
memcpy(bssid, data.bssid, sizeof(bssid));
});
staModeDisconnectedHandler = WiFi.onStationModeDisconnected(
[](const WiFiEventStationModeDisconnected &) {
if (dnsServer.isForwarding()) {
dnsServer.disableForwarder("*");
dnsServer.setTTL(0);
// Reminder, Serial.println() will not work from these callbacks.
// For debug printf use ets_uart_printf().
}
});
/*
While you can remove the password parameter to make the AP open.
You will be operating with less security and allowing snoopers to see
the credentials you use for your WiFi.
*/
WiFi.softAPConfig(apIP, apIP, netMsk);
WiFi.softAP(softAP_ssid, softAP_password);
// The following comment for delay(500) was committed Aug 19, 2015; is it
// still true? Commented out for verification. - APR 2020
// delay(500); // Without delay I've seen the IP address blank
CONSOLE_PRINTF("SoftAP '%s' started\r\n", softAP_ssid);
CONSOLE_PRINTLN2(" IP address: ", WiFi.softAPIP().toString());
/* Captive portals will usually use a TTL of 0 to avoid DNS cache poisoning. */
dnsServer.setTTL(0);
/* Setup the DNS server redirecting all the domains to the apIP */
dnsServer.start(IANA_DNS_PORT, "*", apIP);
CONSOLE_PRINTLN("DNSServer started:");
CONSOLE_PRINTF(" DNS Forwarding is %s\r\n", dnsServer.isForwarding() ? "on" : "off");
CONSOLE_PRINTF(" Resolve all domain lookups, '%s', to this AP's IP address, '%s' %s.\r\n",
dnsServer.getDomainName().c_str(),
softAP_ssid,
WiFi.softAPIP().toString().c_str());
CONSOLE_PRINTF(" TTL set to %u\r\n", dnsServer.getTTL());
/*
Do some NAPT startup stuff
*/
CONSOLE_PRINTLN("Begin NAPT initialization:");
CONSOLE_PRINTF(" Heap before NAPT init: %d\r\n", ESP.getFreeHeap());
err_t ret = ip_napt_init(NAPT, NAPT_PORT);
CONSOLE_PRINTF(" ip_napt_init(%d,%d): ret=%d (OK=%d)\r\n", NAPT, NAPT_PORT, (int)ret, (int)ERR_OK);
if (ret == ERR_OK) {
ret = ip_napt_enable_no(SOFTAP_IF, 1);
CONSOLE_PRINTF(" ip_napt_enable_no(SOFTAP_IF): ret=%d (OK=%d)\r\n", (int)ret, (int)ERR_OK);
if (ret == ERR_OK) {
CONSOLE_PRINTF(" NAPT AP '%s' started.\r\n", softAP_ssid);
if (WiFi.localIP().isSet()) {
CONSOLE_PRINTF(" It is an extension of '%s' made through WLAN interface.\r\n", ssid);
CONSOLE_PRINTF(" Remote WLAN IP Address: %s.\r\n", WiFi.localIP().toString().c_str());
}
}
}
CONSOLE_PRINTF(" Heap after NAPT init: %d\r\n", ESP.getFreeHeap());
if (ret != ERR_OK) {
CONSOLE_PRINTF(" NAPT initialization failed!!!\r\n");
}
/* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */
server.on("/", handleRoot);
server.on("/wifi", handleWifi);
server.on("/wifisave", handleWifiSave);
server.on("/generate_204", handleRoot); // Android captive portal. Maybe not needed. Might be handled by notFound handler.
server.on("/fwlink", handleRoot); // Microsoft captive portal. Maybe not needed. Might be handled by notFound handler.
server.onNotFound(handleNotFound);
server.begin(); // Web server start
CONSOLE_PRINTLN("HTTP server started");
loadCredentials(); // Load WLAN credentials from network
connect = (strlen(ssid) > 0 && staReady); // Request WLAN connect if there is a SSID and we want to connect at startup
}
void connectWifi() {
CONSOLE_PRINTF("Connecting as wifi client, WLAN, to '%s' ...\r\n", ssid);
WiFi.disconnect();
/*
A call to set hostname, must be set before the call to WiFi.begin, otherwise
the name may be missing from the routers DNS lookup results. Note, not all
routers will import registered DHCP host names from clients into the active
local DNS resolver. For those that do, it is best to set hostname before
calling WiFi.begin().
*/
WiFi.hostname(myHostname);
WiFi.begin(ssid, password);
int connRes = WiFi.waitForConnectResult();
if (-1 == connRes) {
CONSOLE_PRINTLN(" WiFi.waitForConnectResult() timed out.");
} else {
CONSOLE_PRINTF(" Connection status: %s, %d\r\n", getWiFiStatusString(connRes).c_str(), connRes);
}
}
void loop() {
if (connect) {
connect = false;
connectWifi();
lastConnectTry = millis();
}
{
unsigned int s = WiFi.status();
if (s == 0 && millis() > (lastConnectTry + 60000) && ssid[0] && staReady) {
/* When all of the following conditions are true, try to connect */
/* 1) If WLAN disconnected */
/* 2) Required idle time between connect attempts has passed. */
/* 3) We have an ssid configured */
/* 4) We are ready for the STA to come up */
/* Don't set retry time too low as retry interfere the softAP operation */
connect = true;
}
if (status != s) { // WLAN status change
CONSOLE_PRINTF("WLAN Status changed:\r\n");
CONSOLE_PRINTF(" new status: %s, %d\r\n", getWiFiStatusString(s).c_str(), s);
CONSOLE_PRINTF(" previous status: %s, %d\r\n", getWiFiStatusString(status).c_str(), status);
status = s;
if (s == WL_CONNECTED) {
/* Just connected to WLAN */
CONSOLE.println();
if (WiFi.localIP().isSet() && WiFi.softAPIP().isSet()) {
CONSOLE_PRINTF("NAPT AP '%s' status:\r\n", softAP_ssid);
if (WiFi.localIP().isSet()) {
CONSOLE_PRINTF(" It is an extension of '%s' made through WLAN interface.\r\n", ssid);
CONSOLE_PRINTF(" WLAN connected with IP Address: %s.\r\n", WiFi.localIP().toString().c_str());
}
} else {
CONSOLE_PRINT("WLAN connected to ");
CONSOLE.println(ssid);
CONSOLE_PRINT(" IP address: ");
CONSOLE.println(WiFi.localIP());
}
// Setup MDNS responder
if (!MDNS.begin(myHostname, WiFi.localIP())) {
CONSOLE_PRINTLN(" Error setting up MDNS responder!");
} else {
CONSOLE_PRINTLN(" mDNS responder started");
// Add service to MDNS-SD
MDNS.addService("http", "tcp", 80);
}
/*
Setup the DNSServer to respond only to request for our hostname and
forward other name request to the DNS configured to the WLAN.
*/
dnsServer.setTTL(600); // 10 minutes
dnsServer.enableForwarder(myHostname, WiFi.dnsIP(0));
CONSOLE_PRINTF("DNSServer changes/status:\r\n");
CONSOLE_PRINTF(" DNS Forwarding is %s\r\n", dnsServer.isForwarding() ? "on" : "off");
CONSOLE_PRINTF(" Resolve '%s' to this AP's IP address, '%s' %s.\r\n",
dnsServer.getDomainName().c_str(),
softAP_ssid,
WiFi.softAPIP().toString().c_str());
if (dnsServer.isDNSSet()) {
CONSOLE_PRINTF(" Forward other lookups to DNS: %s\r\n", dnsServer.getDNS().toString().c_str());
}
CONSOLE_PRINTF(" TTL set to %u\r\n", dnsServer.getTTL());
} else {
/* Captive portals will usually use a TTL of 0 to avoid DNS cache poisoning. */
dnsServer.setTTL(0);
/* Setup the DNSServer to redirect all the domain lookups to the apIP */
dnsServer.disableForwarder("*");
CONSOLE_PRINTF("DNSServer changes/status:\r\n");
CONSOLE_PRINTF(" DNS Forwarding is %s\r\n", dnsServer.isForwarding() ? "on" : "off");
CONSOLE_PRINTF(" Resolve all domain lookups, '%s', to this AP's IP address, '%s' %s.\r\n",
dnsServer.getDomainName().c_str(),
softAP_ssid,
WiFi.softAPIP().toString().c_str());
CONSOLE_PRINTF(" TTL set to %u\r\n", dnsServer.getTTL());
// Note, it is not necessary to clear the DNS forwarder address. This
// is being done here, to test that methods isDNSSet() and setDNS() work.
dnsServer.setDNS(0U);
if (dnsServer.isDNSSet()) {
CONSOLE_PRINTF(" DNS forwarder address: %s\r\n", dnsServer.getDNS().toString().c_str());
} else {
CONSOLE_PRINTF(" DNS forwarder address not set.\r\n");
}
if (s == WL_NO_SSID_AVAIL) {
WiFi.disconnect();
}
}
}
if (s == WL_CONNECTED) {
MDNS.update();
}
}
// Do work:
// DNS
dnsServer.processNextRequest();
// HTTP
server.handleClient();
}
#else // LWIP_FEATURES && !LWIP_IPV6
#include <ESP8266WiFi.h>
void setup() {
WiFi.persistent(false);
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
Serial.printf("\n\nNAPT not supported in this configuration\n");
}
void loop() {
}
#endif // LWIP_FEATURES && !LWIP_IPV6

View File

@ -0,0 +1,48 @@
#if LWIP_FEATURES && !LWIP_IPV6
// Substitution list:
// {t} - target name
// {1} - The target to redirect to, in absolute URL form.
#ifdef DEBUG_VIEW
static const char portalRedirectHTML[] PROGMEM = R"EOF(
<!DOCTYPE html>
<html lang='en'>
<head>
<meta name='viewport' content='width=device-width'>
<meta http-equiv='Refresh' content='5; url={1}'>
<title>Redirecting</title>
</head>
<body>
<h1>Captive Portal Redirect</h1>
<p>Redirecting to <a href='{1}'>{t}</a></p>
<p>If you do not see the menu in 5 seconds, please click on the above link!</p>
</body>
</html>
)EOF";
#else
static const char portalRedirectHTML[] PROGMEM = R"EOF(
<!DOCTYPE html><html lang='en'><head><meta name='viewport' content='width=device-width'><meta http-equiv='Refresh' content='5; url={1}'><title>Redirecting</title></head><body><h1>Captive Portal Redirect</h1><p>Redirecting to <a href='{1}'>{t}</a></p><p>If you do not see the menu in 5 seconds, please click on the above link!</p></body></html>
)EOF";
#endif
void sendPortalRedirect(String path, String targetName) {
CONSOLE_PRINTLN2("Request redirected to captive portal, original request was for ", server.hostHeader());
/* There are 3 points of redirection here:
1) "Location" element in the header
2) HTML meta element to redirect
3) Click on link to redirect
If the "Location" header element works the HTML stuff is never seen.
*/
// https://tools.ietf.org/html/rfc7231#section-6.4.3
server.sendHeader("Location", path, true);
addNoCacheHeader();
String reply = FPSTR(portalRedirectHTML);
reply.reserve(reply.length() + 2 * path.length() + 80);
reply.replace("{t}", targetName);
reply.replace("{1}", path);
server.send(302, "text/html", reply);
}
#endif // LWIP_FEATURES && !LWIP_IPV6

View File

@ -0,0 +1,325 @@
// #define DEBUG_VIEW
// The idea here is to debug HTML with DEBUG_VIEW defined then, when finished,
// use one of the minify web applications to strip the comments and nice
// formating spaces. Then update the minified version below.
//
// Also there are comment sections at the top and bottom of each block of HTML
// code. The purpose is to move the lines of C code near by into the blocked
// comment sections. Then you have a large block of continguious HTML that can
// be copy/pasted into one of the online web HTML checkers. You can adjust the
// code there till it is correct then copy/paste back here. Then, you can move
// comment boarders around until the C code is back in place.
#ifdef DEBUG_VIEW
static const char configHead[] PROGMEM = R"EOF(
<!--
Captive Portal html for WiFi Config
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta name='viewport' content='width=device-width'>
<title>WiFi</title>
<style>
html, body {
width: 100%;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 16px;
}
h1, h2, h3, h4 {
vertical-align:middle;
text-align:center;
}
input[type=submit] {
width: 98%;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
height: 3em;
}
a {
text-decoration: none;
color: blue;
}
.mono, td, input {
font-family: 'Source Code Pro', monospace;
height: 1.5em;
font-size: 1em;
vertical-align: middle;
}
.lg {
font-size: 1em;
height: 1.8em;
}
.left {
text-align: left;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
#outer {
width:100%
overflow-y: auto;
}
#inner {
margin: .75em auto;
max-width: 500px;
min-width: 428px;
text-align: left;
}
</style>
</head>
<body onload='ol()'>
<script>
// short hand
function ge(i) {
return document.getElementById(i);
}
// Toggle password view
function pv() {
let e = ge('p');
if (e.type === 'password') {
e.type = 'text';
} else {
e.type = 'password';
}
e.focus();
}
// Copy SSID value to input element for SSID
// Move focus to password input element and clear
function cs(i){
ge('s').value = i.innerText;
let e = ge('p');
e.type = 'password';
e.value = '';
e.focus();
e.scrollIntoView();
}
</script>
<!--
-->
)EOF";
#else
static const char configHead[] PROGMEM = R"EOF(<!DOCTYPE html><html lang="en"><head><meta name='viewport' content='width=device-width'><title>WiFi</title><style>html,body{width:100%;font-family:'Open Sans',Helvetica,Arial,sans-serif;font-size:16px}h1,h2,h3,h4{vertical-align:middle;text-align:center}input[type=submit]{width:98%;font-family:'Open Sans',Helvetica,Arial,sans-serif;height:3em}a{text-decoration:none;color:blue}.mono,td,input{font-family:'Source Code Pro',monospace;height:1.5em;font-size:1em;vertical-align:middle}.lg{font-size:1em;height:1.8em}.left{text-align:left}.center{text-align:center}.right{text-align:right}#outer{width:100% overflow-y: auto}#inner{margin: .75em auto;max-width:500px;min-width:428px;text-align:left}</style></head><body onload='ol()'> <script>function ge(i){return document.getElementById(i);} function pv(){let e=ge('p');if(e.type==='password'){e.type='text';}else{e.type='password';} e.focus();} function cs(i){ge('s').value=i.innerText;let e=ge('p');e.type='password';e.value='';e.focus();e.scrollIntoView();}</script> )EOF";
#endif
#ifdef DEBUG_VIEW
static const char configPresetInput[] PROGMEM = R"EOF(
<!--
Prefill input fields with the last credentials used.
{s} - Network Name/SSID
{p} - Password
-->
<script>
// After web page load init SSID/PSK field
// onload copy credentials
function ol() {
ge('s').value='{s}';
ge('p').value='{p}';
}
</script>
<!--
-->
)EOF";
#else
static const char configPresetInput[] PROGMEM = R"EOF(<script>function ol(){ge('s').value='{s}';ge('p').value='{p}';}</script>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configConnection[] PROGMEM = R"EOF(
<!--
{w} - SoftAP info "SoftAP: softAP_ssid"
or - WiFi Network connection info "WiFi Network: ssid"
-->
<div id='outer'>
<div id='inner'>
<h1>WiFi Details and Config</h1>
<p>You are connected through the {w}</p>
<!--
-->
)EOF";
#else
static const char configConnection[] PROGMEM = R"EOF(<div id='outer'><div id='inner'><h1>WiFi Details and Config</h1><p>You are connected through the {w}</p>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configAPInfo[] PROGMEM = R"EOF(
<!--
{s} - SSDI of Network
{i} - IP address on that Network, WiFi.softAPIP or WiFi.localIP
{a} - Number of Stations connected to AP
-->
<br />
<h2>SoftAP Details</h2>
<table>
<tr><td>SSI</td><td>{s}</td></tr>
<tr><td>BSSID&nbsp;</td><td>{b}</td></tr>
<tr><td>IP</td><td>{i}</td></tr>
<tr><td>STA</td><td>Count: {a}</td></tr>
</table>
<!--
-->
)EOF";
#else
static const char configAPInfo[] PROGMEM = R"EOF(<br /><h2>SoftAP Details</h2><table><tr><td>SSI</td><td>{s}</td></tr><tr><td>BSSID&nbsp;</td><td>{b}</td></tr><tr><td>IP</td><td>{i}</td></tr><tr><td>STA</td><td>Count: {a}</td></tr></table>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configWLANInfo[] PROGMEM = R"EOF(
<!--
{s} - SSDI of Network
{b} - BSSID
{c} - Channel
{p} - PHY Mode
{r} - RSSI
{i} - IP address on that Network, WiFi.softAPIP or WiFi.localIP
{m} - Subnet Mask
{g} - Gateway
{1} - DNS1
{2} - DNS2
-->
<br />
<h2>WLAN Details</h2>
<table>
<tr><td>SSID</td><td>{s}</td></tr>
<tr><td>BSSID&nbsp;</td><td>{b}</td></tr>
<tr><td>CH</td><td>{c}</td></tr>
<tr><td>PHY</td><td>{p}</td></tr>
<tr><td>RSSI</td><td>{r}</td></tr>
<tr><td>IP</td><td>{i}</td></tr>
<tr><td>GW</td><td>{g}</td></tr>
<tr><td>Mask</td><td>{m}</td></tr>
<tr><td>DNS1</td><td>{1}</td></tr>
<tr><td>DNS2</td><td>{2}</td></tr>
</table>
<!--
-->
)EOF";
#else
static const char configWLANInfo[] PROGMEM = R"EOF(<br /><h2>WLAN Details</h2><table><tr><td>SSID</td><td>{s}</td></tr><tr><td>BSSID&nbsp;</td><td>{b}</td></tr><tr><td>CH</td><td>{c}</td></tr><tr><td>PHY</td><td>{p}</td></tr><tr><td>RSSI</td><td>{r}</td></tr><tr><td>IP</td><td>{i}</td></tr><tr><td>GW</td><td>{g}</td></tr><tr><td>Mask</td><td>{m}</td></tr><tr><td>DNS1</td><td>{1}</td></tr><tr><td>DNS2</td><td>{2}</td></tr></table>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configWLANOffline[] PROGMEM = R"EOF(
<!--
-->
<br />
<h2>WLAN - offline</h2>
<!--
-->
)EOF";
#else
static const char configWLANOffline[] PROGMEM = R"EOF(<br /><h2>WLAN - offline</h2>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configList[] PROGMEM = R"EOF(
<!--
-->
<br />
<h2>WLAN Network List</h2>
<h4>(refresh if any are missing)</h4>
<table>
<tr><th class='left'>Network Name/SSID</th>
<th style='width: 2em;' class='center'>CH</th>
<th style='width: 1em;' class='center'></th>
<th class='right'>RSSI</th></tr>
<!--
-->
)EOF";
#else
static const char configList[] PROGMEM = R"EOF(<br /><h2>WLAN Network List</h2><h4>(refresh if any are missing)</h4><table><tr><th class='left'>Network Name/SSID</th><th style='width: 2em;' class='center'>CH</th><th style='width: 1em;' class='center'></th><th class='right'>RSSI</th></tr>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configItem[] PROGMEM = R"EOF(
<!--
use this with each AP when APs are found in a scan
{s} - WiFi.SSID
{c} - channel
{l} - For a secure Network replace with &#x1f512; (lock symbol)
{r} - WiFi.RSSI
-->
<tr><td><a class='mono' onclick='cs(this)' title='{t}'>{s}</a></td>
<td class='center'>{c}</td>
<td class='center'>{l}</td>
<td class='right'>{r}</td></tr>
<!--
-->
)EOF";
#else
static const char configItem[] PROGMEM = R"EOF(<tr><td><a class='mono' onclick='cs(this)' title='{t}'>{s}</a></td><td class='center'>{c}</td><td class='center'>{l}</td><td class='right'>{r}</td></tr>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configNoAPs[] PROGMEM = R"EOF(
<!--
or this when no APs are found
-->
<tr><td>No WLAN found</td>
<td></td>
<td></td>
<td></td></tr>
<!--
-->
)EOF";
#else
static const char configNoAPs[] PROGMEM = R"EOF(<tr><td>No WLAN found</td><td></td><td></td><td></td></tr>)EOF";
#endif
#ifdef DEBUG_VIEW
static const char configEnd[] PROGMEM = R"EOF(
<!--
-->
</table>
<br /><form method='POST' action='wifisave'><h4>Connect to Network:</h4>
<input id='s' class='lg' type='text' size=32 maxlength=32 placeholder='Network Name/SSID' name='n' spellcheck='false' data-gramm_editor='false'/>
<br /><br />
<input id='p' class='lg' type='password' size=32 maxlength=64 placeholder='password' name='p' spellcheck='false' data-gramm_editor='false'/>
<!--
-->
)EOF";
#else
#endif
#ifdef DEBUG_VIEW
static const char configEnd2[] PROGMEM = R"EOF(
<!--
-->
&nbsp;&nbsp;<a class='lg' onclick='pv();'>&#x1f441;</a>
<br /><br /><input type='submit' value='Connect/Disconnect'/>
</form>
<br />
<p>You may want to <a href='/'>return to the home page</a>.</p>
<p></p>
</div>
</div>
</body>
</html>
<!--
-->
)EOF";
#else
static const char configEnd[] PROGMEM = R"EOF(</table> <br /><form method='POST' action='wifisave'><h4>Connect to Network:</h4> <input id='s' class='lg' type='text' size=32 maxlength=32 placeholder='Network Name/SSID' name='n' spellcheck='false' data-gramm_editor='false'/> <br /><br /> <input id='p' class='lg' type='password' size=32 maxlength=64 placeholder='password' name='p' spellcheck='false' data-gramm_editor='false'/> &nbsp;&nbsp;<a class='lg' onclick='pv();'>&#x1f441;</a> <br /><br /><input type='submit' value='Connect/Disconnect'/></form> <br /><p>You may want to <a href='/'>return to the home page</a>.</p><p></p></div></div></body></html>)EOF";
#endif

View File

@ -0,0 +1,33 @@
#if LWIP_FEATURES && !LWIP_IPV6
/** Load WLAN credentials from EEPROM */
void loadCredentials() {
EEPROM.begin(512);
EEPROM.get(0, ssid);
EEPROM.get(0 + sizeof(ssid), password);
char ok[2 + 1];
EEPROM.get(0 + sizeof(ssid) + sizeof(password), ok);
EEPROM.end();
if (String(ok) != String("OK")) {
ssid[0] = 0;
password[0] = 0;
}
CONSOLE_PRINTLN("Recovered credentials:");
CONSOLE_PRINTF(" %s\r\n", ssid);
CONSOLE_PRINTF(" %s\r\n", strlen(password) > 0 ? "********" : "<no password>");
}
/** Store WLAN credentials to EEPROM */
void saveCredentials() {
EEPROM.begin(512);
EEPROM.put(0, ssid);
EEPROM.put(0 + sizeof(ssid), password);
char ok[2 + 1] = "OK";
EEPROM.put(0 + sizeof(ssid) + sizeof(password), ok);
EEPROM.commit();
EEPROM.end();
}
#endif // LWIP_FEATURES && !LWIP_IPV6

View File

@ -0,0 +1,250 @@
#if LWIP_FEATURES && !LWIP_IPV6
#ifndef TCP_MSS
#define TCP_MSS 1460
#endif
/*
Use kMaxChunkSize to limit size of chuncks
*/
constexpr inline size_t kMaxChunkSize = TCP_MSS;
String& sendIfOver(String& str, size_t threshold = kMaxChunkSize / 2);
size_t sendAsChunks_P(PGM_P content, size_t chunkSize = kMaxChunkSize);
size_t maxPage = 0;
void addNoCacheHeader() {
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
}
String& sendIfOver(String& str, size_t threshold) {
size_t len = str.length();
if (len > threshold) {
// Use later to determine if we reserved enough room in page to avoid realloc
maxPage = std::max(maxPage, len);
server.sendContent(str);
str = "";
}
return str;
}
/*
The idea here is to avoid a large allocation by sendContent_P to copy a
big PROGMEM string. Slice PROGMEM string into chuncks and send.
*/
size_t sendAsChunks_P(PGM_P content, size_t chunkSize) {
size_t len = strlen_P(content);
for (size_t pos = 0; pos < len; pos += chunkSize) {
server.sendContent_P(&content[pos], ((len - pos) >= chunkSize) ? chunkSize : len - pos);
}
return len;
}
/** Handle root or redirect to captive portal */
void handleRoot() {
if (captivePortal()) {
// If captive portal is needed, redirect instead of displaying the page.
return;
}
addNoCacheHeader();
String Page;
Page += F(
"<!DOCTYPE html>"
"<html lang='en'><head><meta name='viewport' content='width=device-width'>"
"<title>ADV CAP Portal Example</title>"
"</head><body>"
"<h1>HELLO WORLD!!</h1>");
if (server.client().localIP() == apIP) {
Page += String(F("<p>You are connected through the soft AP: ")) + softAP_ssid + F("</p>");
} else {
Page += String(F("<p>You are connected through the wifi network: ")) + ssid + F("</p>");
}
Page += F(
"<p>You may want to <a href='/wifi'>config the wifi connection</a>.</p>"
"</body></html>");
server.send(200, F("text/html"), Page);
}
/*
Redirect to the captive portal if we got a request for another domain.
Return true in that case, so the page handler does not try to handle
the request again.
*/
boolean captivePortal() {
IPAddress hAddr, cAddr;
cAddr = server.client().localIP();
if (!cAddr.isSet()) {
// The connection closed prematurely on us.
// Return true, so no further action is taken.
return true;
}
if (hAddr.fromString(server.hostHeader()) && hAddr == cAddr) {
return false;
}
if (hAddr.isSet() || (server.hostHeader() != (String(myHostname) + ".local") && // arrived here by mDNS
server.hostHeader() != String(myHostname))) { // arrived here by local router DNS
String whereTo = String("http://") + server.client().localIP().toString();
sendPortalRedirect(whereTo, F("Captive Portal Example"));
return true;
}
return false;
}
/** Wifi Details and Config page handler */
void handleWifi() {
addNoCacheHeader();
// use HTTP/1.1 Chunked response to avoid building a huge temporary string
if (!server.chunkedResponseModeStart(200, F("text/html"))) {
server.send(505, F("text/plain"), F("HTTP1.1 required"));
return;
}
// Send a few chunks of the HTML that don't need to change.
sendAsChunks_P(configHead);
String page;
CONSOLE_PRINTLN2("sizeof(configHead): ", (sizeof(configHead)));
CONSOLE_PRINTLN2("sizeof(configWLANInfo): ", (sizeof(configWLANInfo)));
CONSOLE_PRINTLN2("sizeof(configList): ", (sizeof(configList)));
CONSOLE_PRINTLN2("sizeof(configEnd): ", (sizeof(configEnd)));
// Just do max on some of the visually larger HTML chunks that will be loaded
// into page and add a little for growth when substituting values in.
size_t thisMany = std::max(sizeof(configWLANInfo), sizeof(configList)) + 200;
CONSOLE_PRINTLN2("Estimate Minimum page reserve size: ", (thisMany));
page.reserve(std::max(kMaxChunkSize, thisMany));
page = FPSTR(configPresetInput);
/*
Set previously used/entered credentials as a default entries.
This allows an opportunity to correct them and try again.
*/
page.replace("{s}", String(ssid));
page.replace("{p}", String(password));
sendIfOver(page);
page += FPSTR(configConnection);
if (server.client().localIP() == apIP) {
page.replace("{w}", String(F("SoftAP: ")) + softAP_ssid);
} else {
page.replace("{w}", String(F("WiFi Network: ")) + ssid);
}
/*
To avoid sending lots of small packets. We call this function frequently,
to check if the 'page' has gone over 512 bytes and if so send.
*/
sendIfOver(page);
page += FPSTR(configAPInfo);
{
uint8_t sta_cnt = wifi_softap_get_station_num();
page.replace("{s}", String(softAP_ssid));
page.replace("{b}", String(WiFi.softAPmacAddress()));
page.replace("{i}", WiFi.softAPIP().toString());
page.replace("{a}", String(sta_cnt));
sendIfOver(page);
if (sta_cnt) {
page += String(F("\r\n<PRE>\r\n"));
struct station_info* info = wifi_softap_get_station_info();
IPAddress addr;
while (info != NULL) {
addr = info->ip;
page += macToString(info->bssid) + F(" ") + addr.toString() + F("\r\n");
info = STAILQ_NEXT(info, next);
sendIfOver(page);
}
page += F("</PRE>\r\n");
}
}
/*
Before we prepare a large block for sending, we call 'sendIfOver' with a
threshold of 0 to force the sending of the current 'page' content.
*/
if (WiFi.localIP().isSet()) {
sendIfOver(page, 0);
page += FPSTR(configWLANInfo);
page.replace("{s}", String(ssid));
page.replace("{b}", macToString(bssid));
page.replace("{c}", String(WiFi.channel()));
page.replace("{p}", String(F("802.11")) + (getPhyModeChar(WiFi.getPhyMode())));
page.replace("{r}", String(WiFi.RSSI()));
page.replace("{i}", WiFi.localIP().toString());
page.replace("{g}", WiFi.gatewayIP().toString());
page.replace("{m}", WiFi.subnetMask().toString());
page.replace("{1}", WiFi.dnsIP(0).toString());
page.replace("{2}", WiFi.dnsIP(1).toString());
} else {
page += FPSTR(configWLANOffline);
}
sendIfOver(page, 0);
sendAsChunks_P(configList);
CONSOLE_PRINTLN("scan start");
int n = WiFi.scanNetworks();
CONSOLE_PRINTLN("scan done");
if (n > 0) {
for (size_t i = 0; i < (size_t)n; i++) {
page += FPSTR(configItem);
page.replace("{s}", WiFi.SSID(i));
page.replace("{t}", WiFi.BSSIDstr(i));
page.replace("{c}", String(WiFi.channel(i)));
page.replace("{l}", (WiFi.encryptionType(i) == ENC_TYPE_NONE) ? F("") : F("&#x1f512;"));
page.replace("{r}", String(WiFi.RSSI(i)));
sendIfOver(page);
}
} else {
page += FPSTR(configNoAPs);
}
sendIfOver(page, 0); // send what we have buffered before next direct send.
sendAsChunks_P(configEnd);
CONSOLE_PRINTLN2("MAX String memory used: ", (maxPage));
server.chunkedResponseFinalize();
}
/** Handle the WLAN save form and redirect to WLAN config page again */
void handleWifiSave() {
CONSOLE_PRINTLN("wifi save");
server.arg("n").toCharArray(ssid, sizeof(ssid) - 1);
server.arg("p").toCharArray(password, sizeof(password) - 1);
sendPortalRedirect(F("wifi"), F("Wifi Config"));
saveCredentials();
connect = strlen(ssid) > 0; // Request WLAN connect with new credentials if there is a SSID
}
void handleNotFound() {
if (captivePortal()) { // If captive portal redirect instead of displaying the error page.
return;
}
String message = F("File Not Found\r\n\r\n");
message += F("URI: ");
message += server.uri();
message += F("\r\nMethod: ");
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += F("\r\nArguments: ");
message += server.args();
message += F("\r\n");
for (uint8_t i = 0; i < server.args(); i++) {
message += String(F(" ")) + server.argName(i) + F(": ") + server.arg(i) + F("\r\n");
}
addNoCacheHeader();
server.send(404, F("text/plain"), message);
}
#endif // LWIP_FEATURES && !LWIP_IPV6

View File

@ -0,0 +1,84 @@
/*
These functions may exist in other projects
*/
#if LWIP_FEATURES && !LWIP_IPV6
/*
Returns a descriptive string for WiFi.status() value
*/
String getWiFiStatusString(uint32_t status) {
const __FlashStringHelper* r;
switch (status) {
case WL_IDLE_STATUS:
r = F("WL_IDLE_STATUS");
break;
case WL_NO_SSID_AVAIL:
r = F("WL_NO_SSID_AVAIL");
break;
case WL_SCAN_COMPLETED:
r = F("WL_SCAN_COMPLETED");
break;
case WL_CONNECTED:
r = F("WL_CONNECTED");
break;
case WL_CONNECT_FAILED:
r = F("WL_CONNECT_FAILED");
break;
case WL_CONNECTION_LOST:
r = F("WL_CONNECTION_LOST");
break;
case WL_DISCONNECTED:
r = F("WL_DISCONNECTED");
break;
case WL_NO_SHIELD:
r = F("WL_NO_SHIELD");
break;
default:
return String(F("Unknown: 0x")) + String(status, HEX);
}
return String(r);
}
/*
Returns a single charcter to append to a "802.11" string to describe the PHY
mode of a WiFi device. Can be used with the value returned by WiFi.getPhyMode().
*/
char getPhyModeChar(WiFiPhyMode_t i) {
switch (i) {
case WIFI_PHY_MODE_11B:
return 'b'; // = 1
case WIFI_PHY_MODE_11G:
return 'g'; // = 2,
case WIFI_PHY_MODE_11N:
return 'n'; // = 3,
default:
break;
}
return '?';
}
/*
Return a String of 6 colon separated hex bytes.
This format is commonly used when printing 6 byte MAC addresses.
*/
String macToString(const unsigned char* mac) {
char buf[20];
int rc = snprintf(buf, sizeof(buf), PSTR("%02X:%02X:%02X:%02X:%02X:%02X"),
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
if (rc < 0 || rc >= (int)sizeof(buf)) {
return emptyString;
}
return String(buf);
}
#endif // LWIP_FEATURES && !LWIP_IPV6

View File

@ -1,33 +1,139 @@
#include <ESP8266WiFi.h>
#include "DNSServer.h" #include "DNSServer.h"
#include <lwip/def.h> #include <lwip/def.h>
#include <Arduino.h> #include <Arduino.h>
#include <memory> #include <memory>
extern struct rst_info resetInfo;
#ifdef DEBUG_ESP_PORT #ifdef DEBUG_ESP_PORT
#define DEBUG_OUTPUT DEBUG_ESP_PORT #define CONSOLE DEBUG_ESP_PORT
#else #else
#define DEBUG_OUTPUT Serial #define CONSOLE Serial
#endif
#define _ETS_PRINTF(a, ...) ets_uart_printf(a, ##__VA_ARGS__)
#define _ETS_PRINTFNL(a, ...) ets_uart_printf(a "\n", ##__VA_ARGS__)
#define _PRINTF(a, ...) printf_P(PSTR(a), ##__VA_ARGS__)
#define _PRINT(a) print(String(F(a)))
#define _PRINTLN(a) println(String(F(a)))
#define _PRINTLN2(a, b) println(String(F(a)) + b )
#define ETS_PRINTF _ETS_PRINTF
#define ETS_PRINTFNL _ETS_PRINTFNL
#define CONSOLE_PRINTF CONSOLE._PRINTF
#define CONSOLE_PRINT CONSOLE._PRINT
#define CONSOLE_PRINTLN CONSOLE._PRINTLN
#define CONSOLE_PRINTLN2 CONSOLE._PRINTLN2
#ifdef DEBUG_DNSSERVER
#define DEBUG_PRINTF CONSOLE_PRINTF
#define DEBUG_PRINT CONSOLE_PRINT
#define DEBUG_PRINTLN CONSOLE_PRINTLN
#define DEBUG_PRINTLN2 CONSOLE_PRINTLN2
#define DBGLOG_FAIL LOG_FAIL
#define DEBUG_(...) do { (__VA_ARGS__); } while(false)
#define DEBUG__(...) __VA_ARGS__
#define LOG_FAIL(a, fmt, ...) do { if (!(a)) { CONSOLE.printf_P( PSTR(fmt " line: %d, function: %s\r\n"), ##__VA_ARGS__, __LINE__, __FUNCTION__ ); } } while(false);
#else
#define DEBUG_PRINTF(...) do { } while(false)
#define DEBUG_PRINT(...) do { } while(false)
#define DEBUG_PRINTLN(...) do { } while(false)
#define DEBUG_PRINTLN2(...) do { } while(false)
#define DEBUG_(...) do { } while(false)
#define DEBUG__(...) do { } while(false)
#define LOG_FAIL(a, ...) do { a; } while(false)
#define DBGLOG_FAIL(...) do { } while(false)
#endif #endif
#define DNS_HEADER_SIZE sizeof(DNSHeader) #define DNS_HEADER_SIZE sizeof(DNSHeader)
// Want to keep IDs unique across restarts and continquious
static uint32_t _ids __attribute__((section(".noinit")));
DNSServer::DNSServer() DNSServer::DNSServer()
{ {
// I have observed that using 0 for captive and non-zero (600) when
// forwarding, will help Android devices recognize the change in connectivity.
// They will then report connected.
_ttl = lwip_htonl(60); _ttl = lwip_htonl(60);
if (REASON_DEFAULT_RST == resetInfo.reason ||
REASON_DEEP_SLEEP_AWAKE <= resetInfo.reason) {
_ids = random(0, BIT(16) - 1);
}
_ids += kDNSSQueSize; // for the case of restart, ignore any inflight responses
_errorReplyCode = DNSReplyCode::NonExistentDomain; _errorReplyCode = DNSReplyCode::NonExistentDomain;
} }
bool DNSServer::start(const uint16_t &port, const String &domainName, void DNSServer::disableForwarder(const String &domainName, bool freeResources)
const IPAddress &resolvedIP)
{ {
_port = port; _forwarder = false;
if (!domainName.isEmpty()) {
_domainName = domainName;
downcaseAndRemoveWwwPrefix(_domainName);
}
if (freeResources) {
_dns = (uint32_t)0;
if (_que) {
_que = nullptr;
DEBUG_PRINTF("from stop, deleted _que\r\n");
DEBUG_(({
if (_que_ov) {
DEBUG_PRINTLN2("DNS forwarder que overflow or no reply to request: ", (_que_ov));
}
if (_que_drop) {
DEBUG_PRINTLN2("DNS forwarder que wrapped, reply dropped: ", (_que_drop));
}
}));
}
}
}
bool DNSServer::enableForwarder(const String &domainName, const IPAddress &dns)
{
disableForwarder(domainName, false); // Just happens to have the same logic needed here.
if (dns.isSet()) {
_dns = dns;
}
if (_dns.isSet()) {
if (!_que) {
_que = std::unique_ptr<DNSS_REQUESTER[]> (new (std::nothrow) DNSS_REQUESTER[kDNSSQueSize]);
DEBUG_PRINTF("Created new _que\r\n");
if (_que) {
for (size_t i = 0; i < kDNSSQueSize; i++) {
_que[i].ip = 0;
}
DEBUG_((_que_ov = 0));
DEBUG_((_que_drop = 0));
}
}
if (_que) {
_forwarder = true;
}
}
return _forwarder;
}
bool DNSServer::start(const uint16_t &port, const String &domainName,
const IPAddress &resolvedIP, const IPAddress &dns)
{
_port = (port) ? port : IANA_DNS_PORT;
_domainName = domainName;
_resolvedIP[0] = resolvedIP[0]; _resolvedIP[0] = resolvedIP[0];
_resolvedIP[1] = resolvedIP[1]; _resolvedIP[1] = resolvedIP[1];
_resolvedIP[2] = resolvedIP[2]; _resolvedIP[2] = resolvedIP[2];
_resolvedIP[3] = resolvedIP[3]; _resolvedIP[3] = resolvedIP[3];
downcaseAndRemoveWwwPrefix(_domainName);
if (!enableForwarder(domainName, dns) && (dns.isSet() || _dns.isSet())) {
return false;
}
return _udp.begin(_port) == 1; return _udp.begin(_port) == 1;
} }
@ -41,9 +147,15 @@ void DNSServer::setTTL(const uint32_t &ttl)
_ttl = lwip_htonl(ttl); _ttl = lwip_htonl(ttl);
} }
uint32_t DNSServer::getTTL()
{
return lwip_ntohl(_ttl);
}
void DNSServer::stop() void DNSServer::stop()
{ {
_udp.stop(); _udp.stop();
disableForwarder(emptyString, true);
} }
void DNSServer::downcaseAndRemoveWwwPrefix(String &domainName) void DNSServer::downcaseAndRemoveWwwPrefix(String &domainName)
@ -53,7 +165,58 @@ void DNSServer::downcaseAndRemoveWwwPrefix(String &domainName)
domainName.remove(0, 4); domainName.remove(0, 4);
} }
void DNSServer::respondToRequest(uint8_t *buffer, size_t length) void DNSServer::forwardReply(uint8_t *buffer, size_t length)
{
if (!_forwarder || !_que) {
return;
}
DNSHeader *dnsHeader = (DNSHeader *)buffer;
uint16_t id = dnsHeader->ID;
// if (kDNSSQueSize <= (uint16_t)((uint16_t)_ids - id)) {
if ((uint16_t)kDNSSQueSize <= (uint16_t)_ids - id) {
DEBUG_((++_que_drop));
DEBUG_PRINTLN2("Forward reply ID: 0x", (String(id, HEX) + F(" dropped!")));
return;
}
size_t i = id & (kDNSSQueSize - 1);
// Drop duplicate packets
if (0 == _que[i].ip) {
DEBUG_PRINTLN2("Duplicate reply dropped ID: 0x", String(id, HEX));
return;
}
dnsHeader->ID = _que[i].id;
_udp.beginPacket(_que[i].ip, _que[i].port);
_udp.write(buffer, length);
_udp.endPacket();
DEBUG_PRINTLN2("Forward reply ID: 0x", (String(id, HEX) + F(" to ") + IPAddress(_que[i].ip).toString()));
_que[i].ip = 0; // This gets used to detect duplicate packets and overflow
}
void DNSServer::forwardRequest(uint8_t *buffer, size_t length)
{
if (!_forwarder || !_dns.isSet() || !_que) {
return;
}
DNSHeader *dnsHeader = (DNSHeader *)buffer;
++_ids;
size_t i = _ids & (kDNSSQueSize - 1);
DEBUG_(({
if (0 != _que[i].ip) {
++_que_ov;
}
}));
_que[i].ip = _udp.remoteIP();
_que[i].port = _udp.remotePort();
_que[i].id = dnsHeader->ID;
dnsHeader->ID = (uint16_t)_ids;
_udp.beginPacket(_dns, IANA_DNS_PORT);
_udp.write(buffer, length);
_udp.endPacket();
DEBUG_PRINTLN2("Forward request ID: 0x", (String(dnsHeader->ID, HEX) + F(" to ") + _dns.toString()));
}
bool DNSServer::respondToRequest(uint8_t *buffer, size_t length)
{ {
DNSHeader *dnsHeader; DNSHeader *dnsHeader;
uint8_t *query, *start; uint8_t *query, *start;
@ -64,23 +227,30 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length)
dnsHeader = (DNSHeader *)buffer; dnsHeader = (DNSHeader *)buffer;
// Must be a query for us to do anything with it // Must be a query for us to do anything with it
if (dnsHeader->QR != DNS_QR_QUERY) if (dnsHeader->QR != DNS_QR_QUERY) {
return; return false;
}
// If operation is anything other than query, we don't do it // If operation is anything other than query, we don't do it
if (dnsHeader->OPCode != DNS_OPCODE_QUERY) if (dnsHeader->OPCode != DNS_OPCODE_QUERY) {
return replyWithError(dnsHeader, DNSReplyCode::NotImplemented); replyWithError(dnsHeader, DNSReplyCode::NotImplemented);
return false;
}
// Only support requests containing single queries - everything else // Only support requests containing single queries - everything else
// is badly defined // is badly defined
if (dnsHeader->QDCount != lwip_htons(1)) if (dnsHeader->QDCount != lwip_htons(1)) {
return replyWithError(dnsHeader, DNSReplyCode::FormError); replyWithError(dnsHeader, DNSReplyCode::FormError);
return false;
}
// We must return a FormError in the case of a non-zero ARCount to // We must return a FormError in the case of a non-zero ARCount to
// be minimally compatible with EDNS resolvers // be minimally compatible with EDNS resolvers
if (dnsHeader->ANCount != 0 || dnsHeader->NSCount != 0 if (dnsHeader->ANCount != 0 || dnsHeader->NSCount != 0
|| dnsHeader->ARCount != 0) || dnsHeader->ARCount != 0) {
return replyWithError(dnsHeader, DNSReplyCode::FormError); replyWithError(dnsHeader, DNSReplyCode::FormError);
return false;
}
// Even if we're not going to use the query, we need to parse it // Even if we're not going to use the query, we need to parse it
// so we can check the address type that's being queried // so we can check the address type that's being queried
@ -89,15 +259,19 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length)
remaining = length - DNS_HEADER_SIZE; remaining = length - DNS_HEADER_SIZE;
while (remaining != 0 && *start != 0) { while (remaining != 0 && *start != 0) {
labelLength = *start; labelLength = *start;
if (labelLength + 1 > remaining) if (labelLength + 1 > remaining) {
return replyWithError(dnsHeader, DNSReplyCode::FormError); replyWithError(dnsHeader, DNSReplyCode::FormError);
return false;
}
remaining -= (labelLength + 1); remaining -= (labelLength + 1);
start += (labelLength + 1); start += (labelLength + 1);
} }
// 1 octet labelLength, 2 octet qtype, 2 octet qclass // 1 octet labelLength, 2 octet qtype, 2 octet qclass
if (remaining < 5) if (remaining < 5) {
return replyWithError(dnsHeader, DNSReplyCode::FormError); replyWithError(dnsHeader, DNSReplyCode::FormError);
return false;
}
start += 1; // Skip the 0 length label that we found above start += 1; // Skip the 0 length label that we found above
@ -109,23 +283,33 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length)
queryLength = start - query; queryLength = start - query;
if (qclass != lwip_htons(DNS_QCLASS_ANY) if (qclass != lwip_htons(DNS_QCLASS_ANY)
&& qclass != lwip_htons(DNS_QCLASS_IN)) && qclass != lwip_htons(DNS_QCLASS_IN)) {
return replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, query, queryLength);
query, queryLength); return false;
}
if (qtype != lwip_htons(DNS_QTYPE_A) if (qtype != lwip_htons(DNS_QTYPE_A)
&& qtype != lwip_htons(DNS_QTYPE_ANY)) && qtype != lwip_htons(DNS_QTYPE_ANY)) {
return replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, query, queryLength);
query, queryLength); return false;
}
// If we have no domain name configured, just return an error // If we have no domain name configured, just return an error
if (_domainName.isEmpty()) if (_domainName.isEmpty()) {
return replyWithError(dnsHeader, _errorReplyCode, if (_forwarder) {
query, queryLength); return true;
} else {
replyWithError(dnsHeader, _errorReplyCode, query, queryLength);
return false;
}
}
// If we're running with a wildcard we can just return a result now // If we're running with a wildcard we can just return a result now
if (_domainName == "*") if (_domainName == "*") {
return replyWithIP(dnsHeader, query, queryLength); DEBUG_PRINTF("dnsServer - replyWithIP\r\n");
replyWithIP(dnsHeader, query, queryLength);
return false;
}
matchString = _domainName.c_str(); matchString = _domainName.c_str();
@ -139,24 +323,32 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length)
labelLength = *start; labelLength = *start;
start += 1; start += 1;
while (labelLength > 0) { while (labelLength > 0) {
if (tolower(*start) != *matchString) if (tolower(*start) != *matchString) {
return replyWithError(dnsHeader, _errorReplyCode, if (_forwarder) {
query, queryLength); return true;
} else {
replyWithError(dnsHeader, _errorReplyCode, query, queryLength);
return false;
}
}
++start; ++start;
++matchString; ++matchString;
--labelLength; --labelLength;
} }
if (*start == 0 && *matchString == '\0') if (*start == 0 && *matchString == '\0') {
return replyWithIP(dnsHeader, query, queryLength); replyWithIP(dnsHeader, query, queryLength);
return false;
}
if (*matchString != '.') if (*matchString != '.') {
return replyWithError(dnsHeader, _errorReplyCode, replyWithError(dnsHeader, _errorReplyCode, query, queryLength);
query, queryLength); return false;
}
++matchString; ++matchString;
} }
return replyWithError(dnsHeader, _errorReplyCode, replyWithError(dnsHeader, _errorReplyCode, query, queryLength);
query, queryLength); return false;
} }
void DNSServer::processNextRequest() void DNSServer::processNextRequest()
@ -182,7 +374,14 @@ void DNSServer::processNextRequest()
return; return;
_udp.read(buffer.get(), currentPacketSize); _udp.read(buffer.get(), currentPacketSize);
respondToRequest(buffer.get(), currentPacketSize); if (_dns.isSet() && _udp.remoteIP() == _dns) {
// _forwarder may have been set to false; however, for now allow inflight
// replys to finish. //??
forwardReply(buffer.get(), currentPacketSize);
} else
if (respondToRequest(buffer.get(), currentPacketSize)) {
forwardRequest(buffer.get(), currentPacketSize);
}
} }
void DNSServer::writeNBOShort(uint16_t value) void DNSServer::writeNBOShort(uint16_t value)

View File

@ -2,6 +2,14 @@
#define DNSServer_h #define DNSServer_h
#include <WiFiUdp.h> #include <WiFiUdp.h>
// #define DEBUG_DNSSERVER
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
#ifndef IANA_DNS_PORT
#define IANA_DNS_PORT 53 // AKA domain
constexpr inline uint16_t kIanaDnsPort = IANA_DNS_PORT;
#endif
#define DNS_QR_QUERY 0 #define DNS_QR_QUERY 0
#define DNS_QR_RESPONSE 1 #define DNS_QR_RESPONSE 1
#define DNS_OPCODE_QUERY 0 #define DNS_OPCODE_QUERY 0
@ -45,6 +53,15 @@ struct DNSHeader
uint16_t ARCount; // number of resource entries uint16_t ARCount; // number of resource entries
}; };
constexpr inline size_t kDNSSQueSizeAddrBits = 3; // The number of bits used to address que entries
constexpr inline size_t kDNSSQueSize = BIT(kDNSSQueSizeAddrBits);
struct DNSS_REQUESTER {
uint32_t ip;
uint16_t port;
uint16_t id;
};
class DNSServer class DNSServer
{ {
public: public:
@ -52,24 +69,60 @@ class DNSServer
~DNSServer() { ~DNSServer() {
stop(); stop();
}; };
/*
If specified, `enableForwarder` will update the `domainName` that is used
to match DNS request to this AP's IP Address. A non-matching request will
be forwarded to the DNS server specified by `dns`.
Returns `true` on success.
Returns `false`,
* when forwarding `dns` is not set, or
* unable to allocate resources for managing the DNS forward function.
*/
bool enableForwarder(const String &domainName = emptyString, const IPAddress &dns = (uint32_t)0);
/*
`disableForwarder` will stop forwarding DNS requests. If specified,
updates the `domainName` that is matched for returning this AP's IP Address.
Optionally, resources used for the DNS forward function can be freed.
*/
void disableForwarder(const String &domainName = emptyString, bool freeResources = false);
bool isForwarding() { return _forwarder && _dns.isSet(); }
void setDNS(const IPAddress& dns) { _dns = dns; }
IPAddress getDNS() { return _dns; }
bool isDNSSet() { return _dns.isSet(); }
void processNextRequest(); void processNextRequest();
void setErrorReplyCode(const DNSReplyCode &replyCode); void setErrorReplyCode(const DNSReplyCode &replyCode);
void setTTL(const uint32_t &ttl); void setTTL(const uint32_t &ttl);
uint32_t getTTL();
String getDomainName() { return _domainName; }
// Returns true if successful, false if there are no sockets available // Returns true if successful, false if there are no sockets available
bool start(const uint16_t &port, bool start(const uint16_t &port,
const String &domainName, const String &domainName,
const IPAddress &resolvedIP); const IPAddress &resolvedIP,
const IPAddress &dns = (uint32_t)0);
// stops the DNS server // stops the DNS server
void stop(); void stop();
private: private:
WiFiUDP _udp; WiFiUDP _udp;
uint16_t _port;
String _domainName; String _domainName;
unsigned char _resolvedIP[4]; IPAddress _dns;
std::unique_ptr<DNSS_REQUESTER[]> _que;
uint32_t _ttl; uint32_t _ttl;
#ifdef DEBUG_DNSSERVER
// There are 2 possiblities for OverFlow:
// 1) we have more than kDNSSQueSize request already outstanding.
// 2) we have request that never received a reply.
uint32_t _que_ov;
uint32_t _que_drop;
#endif
DNSReplyCode _errorReplyCode; DNSReplyCode _errorReplyCode;
bool _forwarder;
unsigned char _resolvedIP[4];
uint16_t _port;
void downcaseAndRemoveWwwPrefix(String &domainName); void downcaseAndRemoveWwwPrefix(String &domainName);
void replyWithIP(DNSHeader *dnsHeader, void replyWithIP(DNSHeader *dnsHeader,
@ -81,7 +134,9 @@ class DNSServer
size_t queryLength); size_t queryLength);
void replyWithError(DNSHeader *dnsHeader, void replyWithError(DNSHeader *dnsHeader,
DNSReplyCode rcode); DNSReplyCode rcode);
void respondToRequest(uint8_t *buffer, size_t length); bool respondToRequest(uint8_t *buffer, size_t length);
void forwardRequest(uint8_t *buffer, size_t length);
void forwardReply(uint8_t *buffer, size_t length);
void writeNBOShort(uint16_t value); void writeNBOShort(uint16_t value);
}; };
#endif #endif