Securing Internet of Things (IoT) devices is no longer optional; it is a fundamental requirement. When exposing an ESP32 or ESP8266 to a local network or the open internet, you inevitably face unauthorized access attempts, port scanners, and brute-force login bots. While traditional web frameworks like Node.js or Python's Flask offer built-in middleware for rate limiting and IP filtering, the asynchronous nature of embedded C++ frameworks requires a different approach. In this comprehensive tutorial, we will explore how to implement a robust ESP AsyncWebServer block IP address mechanism using advanced handler inheritance.
The Core Problem: Lack of Native Middleware
Unlike Express.js, which allows you to simply call app.use(ipFilter), the ESPAsyncWebServer library (specifically the modern mathieucarbou fork required for ESP32 Arduino Core 3.x) does not feature a global middleware pipeline. Every route is evaluated independently. If you attempt to check the client IP inside every single server.on() route callback, you will end up with bloated, unmaintainable code that wastes precious CPU cycles on every HTTP request.
To solve this, we must leverage the underlying architecture of the library. ESPAsyncWebServer processes requests by iterating through a linked list of AsyncWebHandler objects. By creating a custom handler that intercepts malicious IPs before they reach your primary route handlers, we can create a highly efficient, global IP blocking firewall.
Architecture of the AsyncWebHandler Interceptor
The secret to global filtering lies in overriding the canHandle() and handleRequest() virtual methods of the AsyncWebHandler base class. When a request arrives, the server asks each registered handler, "Can you handle this request?" If our custom IP blocker recognizes a banned IP, it claims the request and immediately returns a 403 Forbidden response, bypassing the rest of your application logic entirely.
Step 1: Defining the Blocklist Data Structure
First, we need a memory-efficient way to store blocked IPs. While you could use a simple array, C++ offers std::vector for dynamic sizing, or std::unordered_set for O(1) constant-time lookups. For most IoT applications blocking fewer than 50 IPs, a std::vector of IPAddress objects is perfectly adequate and avoids the heap fragmentation associated with hash maps on embedded systems.
#include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <vector>
// Create the AsyncWebServer instance on port 80
AsyncWebServer server(80);
// Define our blocklist
std::vector<IPAddress> blockedIPs = {
IPAddress(192, 168, 1, 105),
IPAddress(10, 0, 0, 50)
};
Step 2: Creating the Custom Interceptor Class
Next, we define our custom class inheriting from AsyncWebHandler. This is where the actual ESP AsyncWebServer block IP address logic resides. We extract the client's IP using the request->client()->remoteIP() method provided by the underlying AsyncTCP/lwIP stack.
class IPBlockerHandler : public AsyncWebHandler {
public:
// Method to dynamically add IPs to the blocklist at runtime
void blockIP(IPAddress ip) {
blockedIPs.push_back(ip);
Serial.printf("Blocked IP added: %s\n", ip.toString().c_str());
}
// The server calls this to see if we want to intercept the request
bool canHandle(AsyncWebServerRequest *request) override {
IPAddress clientIP = request->client()->remoteIP();
// Iterate through the blocklist
for (const auto& blockedIP : blockedIPs) {
if (clientIP == blockedIP) {
return true; // Claim the request!
}
}
return false; // Let normal routes handle it
}
// If canHandle() returned true, this method executes
void handleRequest(AsyncWebServerRequest *request) override {
Serial.printf("Blocked access attempt from: %s\n", request->client()->remoteIP().toString().c_str());
request->send(403, "text/plain", "Forbidden: Your IP has been blocked.");
}
};
// Instantiate the handler
IPBlockerHandler ipBlocker;
Step 3: Integrating with the Server Instance
Crucially, you must add the ipBlocker handler to the server before you define your standard routes. The server evaluates handlers in the order they were added. If you add your standard routes first, they will catch the request before the IP blocker gets a chance to evaluate it.
void setup() {
Serial.begin(115200);
WiFi.begin("YourSSID", "YourPassword");
while (WiFi.status() != WL_CONNECTED) { delay(500); }
Serial.println(WiFi.localIP());
// 1. Attach the IP Blocker FIRST
server.addHandler(&ipBlocker);
// 2. Attach standard routes AFTER
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", "Welcome to the secure IoT dashboard.");
});
server.begin();
}
void loop() {
// Async server runs on FreeRTOS cores, loop can remain empty
}
Performance Benchmarks: Storage Methods Compared
When scaling your security rules, where you store the blocklist matters. Reading from flash memory on every HTTP request will cause severe latency spikes and wear out the SPI flash chip. Below is a comparison of different storage strategies for maintaining an IP blocklist on an ESP32.
| Storage Method | Lookup Speed | RAM Overhead | Flash Wear | Best Use Case |
|---|---|---|---|---|
std::vector<IPAddress> (SRAM) |
Fast (O(N)) | ~4 bytes per IP | None | Hardcoded lists, < 100 IPs |
std::unordered_set (SRAM) |
Instant (O(1)) | ~32 bytes per IP | None | Massive lists, > 500 IPs |
| LittleFS CSV File | Very Slow | Low (if streamed) | High (if writing) | Persistent storage across reboots |
| Preferences (NVS) | Moderate | Low | Moderate | Saving a few dynamic admin bans |
Pro Tip: If you must persist blocked IPs across reboots, load them from LittleFS or NVS into a std::vector in SRAM during the setup() phase. Never read directly from the filesystem inside the canHandle() callback, as filesystem I/O blocks the FreeRTOS task and will starve the WiFi stack, leading to dropped packets.
Handling Edge Cases: NAT, Proxies, and Captive Portals
Implementing an ESP AsyncWebServer block IP address filter in a controlled lab environment is easy. Deploying it in the real world introduces complex networking edge cases that can accidentally lock you out of your own device.
The NAT (Network Address Translation) Trap
If your ESP32 is exposed to the internet via port forwarding on a home router, the request->client()->remoteIP() method will often return the local IP address of the router's gateway (e.g., 192.168.1.1) rather than the true external public IP of the attacker. This happens because the router performs NAT, terminating the external connection and opening a new internal one to the ESP32. If you block 192.168.1.1, you will instantly block all incoming internet traffic to your device.
Warning: To accurately filter external IPs behind a NAT router, you must configure your router to act as a reverse proxy that injects the
X-Forwarded-ForHTTP header. You can then parse this header in your ESP32 code usingrequest->getHeader("X-Forwarded-For")->value(). Consult the Espressif WiFi Architecture documentation for deeper insights into how the lwIP stack processes incoming NAT packets.
Captive Portals and SoftAP Mode
When running the ESP32 in SoftAP (Access Point) mode to host a Captive Portal for WiFi configuration, the device itself acts as the DHCP server and NAT router. In this scenario, all connected smartphones and laptops will appear to the web server as a single IP address (usually 192.168.4.1 or the gateway IP). IP blocking is effectively useless and dangerous in SoftAP captive portal scenarios, as banning one rogue device might ban the gateway itself. Always wrap your IP blocking logic in a conditional check that ensures the ESP32 is currently in WIFI_STA (Station) mode before enforcing the blocklist.
Summary and Next Steps
By leveraging the AsyncWebHandler inheritance model, you can create a highly efficient, non-blocking IP firewall for your ESP32 projects. This method ensures that malicious actors are dropped at the earliest possible stage of the HTTP request lifecycle, preserving CPU cycles and memory for your core application logic. Remember to account for NAT environments, avoid filesystem I/O during request evaluation, and always maintain a physical fallback (like a GPIO button) to clear the blocklist in case you accidentally ban your own workstation's IP address.






