/ ESP32S2_TFT_AdBlocker / ESP32S2_TFT_AdBlocker.ino
ESP32S2_TFT_AdBlocker.ino
1 // SPDX-FileCopyrightText: 2022 s60sc with changes by ladyada 2 // SPDX-License-Identifier: GPL-3.0-or-later 3 4 /* 5 ESP32_AdBlocker acts as a DNS Sinkhole by returning 0.0.0.0 for any domain names in its blocked list, 6 else forwards to an external DNS server to resolve IP addresses. This prevents content being retrieved 7 from or sent to blocked domains. Searches generally take <200us. 8 9 To use ESP32_AdBlocker, enter its IP address in place of the DNS server IPs in your router/devices. 10 Currently it does have an IPv6 address and some devices use IPv6 by default, so disable IPv6 DNS on 11 your device / router to force it to use IPv4 DNS. 12 13 Blocklist files can downloaded from hosting sites and should either be in HOSTS format 14 or Adblock format (only domain name entries processed) 15 16 arduino-esp32 library DNSServer.cpp modified as custom AdBlockerDNSServer.cpp so that DNSServer::processNextRequest() 17 calls checkBlocklist() in ESP32_AdBlocker to check if domain blocked, which returns the relevant IP. 18 Based on idea from https://github.com/rubfi/esphole 19 20 Compile with partition scheme 'No OTA (2M APP/2M SPIFFS)' 21 22 s60sc 2021 23 */ 24 25 #include <WiFi.h> 26 #include <string> 27 #include <algorithm> 28 #include <bits/stdc++.h> 29 //#include <unordered_map> 30 #include <WiFiClientSecure.h> 31 #include <HTTPClient.h> 32 //#include <ESPAsyncWebServer.h> // https://github.com/me-no-dev/ESPAsyncWebServer & https://github.com/me-no-dev/AsyncTCP 33 #include <Preferences.h> 34 35 #include <Adafruit_SPIFlash.h> 36 #include <Adafruit_TinyUSB.h> 37 #include <Adafruit_ST7789.h> 38 #include <Fonts/FreeSans9pt7b.h> 39 40 #include "AdBlockerDNSServer.h" // customised 41 42 43 #include "config.h" 44 45 //static AsyncWebServer webServer(80); 46 static DNSServer dnsServer; 47 48 //std::unordered_set <std::string> blockSet ; // holds hashed list of blocked domains in memory [too much memory overhead] 49 std::vector<std::string> blockVec; // holds sorted list of blocked domains in memory 50 static uint8_t* downloadBuff; // temporary store for ptocessing downloaded blocklist file 51 static const char* BLOCKLISTFILE = {"/hosts.txt"}; 52 static uint32_t blockCnt, allowCnt = 0; 53 54 Adafruit_ST7789 display = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); 55 Adafruit_FlashTransport_ESP32 flashTransport; // internal SPI flash access 56 Adafruit_SPIFlash flash(&flashTransport); 57 FatFileSystem fatfs; // file system object from SdFat 58 59 char ssid[80] = DEFAULT_SSID; 60 char password[80] = DEFAULT_PASSWORD; 61 char hostname[80] = DEFAULT_HOSTNAME; 62 char hostfile[255] = DEFAULT_HOSTFILE; 63 64 65 bool init_filesystem(void); 66 bool parseSecrets(); 67 68 static float inline inKB(size_t inVal) { 69 return (float)(inVal / 1024.0); 70 } 71 72 void setup() { 73 #if !defined(EXPOSE_FS_ON_MSD) 74 DBG_OUTPUT_PORT.begin(115200); 75 while (!DBG_OUTPUT_PORT) delay(10); 76 delay(1000); 77 #endif 78 79 if (!flash.begin()) { 80 DBG_OUTPUT_PORT.println("failed to load flash"); 81 display.setTextColor(ST77XX_RED); 82 display.println("Failed to load flash"); 83 while (1) yield(); 84 } 85 86 if (! init_filesystem()) { 87 DBG_OUTPUT_PORT.printf("Aborting as no filesystem available"); 88 display.setTextColor(ST77XX_RED); 89 display.println("Failed to load filesys"); 90 while (1) yield(); 91 } 92 93 DBG_OUTPUT_PORT.printf("Sketch size %0.1fKB, PSRAM %s available\n", inKB(ESP.getSketchSize()), (psramFound()) ? "IS" : "NOT"); 94 95 96 // turn on TFT by default 97 pinMode(TFT_I2C_POWER, OUTPUT); 98 digitalWrite(TFT_I2C_POWER, HIGH); 99 pinMode(TFT_BACKLITE, OUTPUT); 100 digitalWrite(TFT_BACKLITE, LOW); 101 delay(10); 102 display.init(135, 240); // Init ST7789 240x135 103 display.setRotation(3); 104 display.fillScreen(ST77XX_BLACK); 105 digitalWrite(TFT_BACKLITE, HIGH); 106 107 display.setTextColor(ST77XX_WHITE); 108 display.setCursor(0, 0); 109 display.setTextSize(3); 110 display.print("ESPHole"); 111 112 display.setFont(&FreeSans9pt7b); 113 display.setTextSize(1); 114 display.setTextColor(ST77XX_WHITE); 115 display.setCursor(0, 40); 116 117 118 parseSecrets(); 119 startWifi(); 120 getNTP(); 121 122 bool haveBlocklist = fatfs.exists(BLOCKLISTFILE); 123 if (!haveBlocklist) { 124 DBG_OUTPUT_PORT.println("No blocklist stored, need to download ..."); 125 display.println("Downloading blocklist"); 126 if (downloadFile()) 127 haveBlocklist = createBlocklist(); 128 } else { 129 haveBlocklist = loadBlocklist(); 130 } 131 132 if (haveBlocklist) { 133 // DNS server startup 134 dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure); 135 if (dnsServer.start(DNS_PORT, "*", WiFi.localIP())) { 136 DBG_OUTPUT_PORT.printf("\nDNS Server started on %s:%d\n", WiFi.localIP().toString().c_str(), DNS_PORT); 137 } else { 138 puts("Aborting as DNS Server not running"); 139 return; 140 } 141 } else { 142 puts("Aborting as no resources available"); 143 return; 144 } 145 DBG_OUTPUT_PORT.printf("Free DRAM: %0.1fKB, Free PSRAM: %0.1fKB\n*** Ready...\n\n", inKB(ESP.getFreeHeap()), inKB(ESP.getFreePsram())); 146 } 147 148 uint32_t timestamp = 0; 149 void loop() { 150 dnsServer.processNextRequest(); 151 if (((timestamp + 2000) < millis()) || (timestamp > millis())){ 152 Serial.print("."); 153 timestamp = millis(); 154 #if !defined(DEBUG_ESP_DNS) 155 display.setFont(&FreeSans9pt7b); 156 display.setTextSize(2); 157 display.setTextColor(ST77XX_RED); 158 display.fillRect(0, 90, 240, 55, ST77XX_BLACK); 159 display.setCursor(0, 118); 160 display.print(blockCnt); 161 display.println(" Nahs!"); 162 #endif 163 164 } 165 //checkAlarm(); 166 } 167 168 static void startWifi() { 169 WiFi.mode(WIFI_AP_STA); 170 DBG_OUTPUT_PORT.printf("Connecting to SSID: %s\n", ssid); 171 //WiFi.config(ADBLOCKER, GATEWAY, SUBNET, RESOLVER); 172 WiFi.begin(ssid, password); 173 display.print("Conn'ing to "); 174 display.print(ssid); 175 display.print("..."); 176 177 while (WiFi.status() != WL_CONNECTED) { 178 delay(500); 179 DBG_OUTPUT_PORT.printf("."); 180 } 181 DBG_OUTPUT_PORT.println(""); 182 183 display.println("OK!"); 184 DBG_OUTPUT_PORT.print("Connected! IP address: "); 185 DBG_OUTPUT_PORT.println(WiFi.localIP()); 186 display.print("IP addr: "); 187 display.println(WiFi.localIP()); 188 } 189 190 IPAddress checkBlocklist(const char* domainName) { 191 // called from ESP32_DNSServer 192 IPAddress ip = IPAddress(0, 0, 0, 0); // IP to return for blocked domain 193 if (strlen(domainName)) { 194 uint64_t uselapsed = micros(); 195 // search for domain in blocklist 196 //bool blocked = (blockSet.find(std::string(domainName)) == blockSet.end()) ? false : true; 197 bool blocked = binary_search(blockVec.begin(), blockVec.end(), std::string(domainName)); 198 uint64_t checkTime = micros() - uselapsed; 199 200 uint32_t mselapsed = millis(); 201 // if not blocked, get IP address for domain from external DNS 202 if (!blocked) WiFi.hostByName(domainName, ip); 203 uint32_t resolveTime = millis() - mselapsed; 204 205 206 #ifdef DEBUG_ESP_DNS 207 display.setFont(&FreeSans9pt7b); 208 display.setTextSize(1); 209 display.setTextColor(ST77XX_WHITE); 210 display.setTextWrap(true); 211 display.fillRect(0, 90, 240, 55, ST77XX_BLACK); 212 display.setCursor(0, 105); 213 display.println(domainName); 214 #endif 215 DBG_OUTPUT_PORT.printf("%s %s in %lluus", (blocked) ? "*Blocked*" : "Allowed", domainName, checkTime); 216 if (!blocked) { 217 DBG_OUTPUT_PORT.printf(", resolved to %s in %ums\n", ip.toString().c_str(), resolveTime); 218 #ifdef DEBUG_ESP_DNS 219 display.setTextColor(ST77XX_GREEN); 220 display.println(ip.toString().c_str()); 221 #endif 222 } 223 else { 224 puts(""); 225 #ifdef DEBUG_ESP_DNS 226 display.setTextColor(ST77XX_RED); 227 display.println("BLOCKED"); 228 #endif 229 } 230 231 if (blocked) blockCnt++; 232 else allowCnt++; 233 } 234 return ip; 235 } 236 237 static bool loadBlocklist() { 238 // load blocklist file into memory from storage 239 size_t remaining = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); 240 DBG_OUTPUT_PORT.printf("Loading blocklist %s\n", hostfile); 241 uint32_t loadTime = millis(); 242 if (psramFound()) heap_caps_malloc_extmem_enable(5); // force vector into psram 243 blockVec.reserve((psramFound()) ? MAX_DOMAINS : 1000); 244 if (psramFound()) heap_caps_malloc_extmem_enable(100000); 245 246 File file = fatfs.open(BLOCKLISTFILE, FILE_READ); 247 if (file) { 248 DBG_OUTPUT_PORT.printf("File size %0.1fKB loading into %0.1fKB %s memory ...\n", inKB(file.size()), 249 inKB(remaining), (psramFound()) ? "PSRAM" : "DRAM"); 250 char domainLine[MAX_LINELEN + 1]; 251 int itemsLoaded = 0; 252 if (psramFound()) heap_caps_malloc_extmem_enable(5); // force vector into psram 253 254 while (file.available()) { 255 size_t lineLen = file.readBytesUntil('\n', domainLine, MAX_LINELEN); 256 if (lineLen) { 257 domainLine[lineLen] = 0; 258 //blockSet.insert(std::string(domainLine)); 259 blockVec.push_back(std::string(domainLine)); 260 itemsLoaded++; 261 if (itemsLoaded%500 == 0) { // check memory too often triggers watchdog 262 remaining = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); 263 if (remaining < (MIN_MEMORY)) { 264 DBG_OUTPUT_PORT.printf("Blocklist truncated to avoid memory overflow, %u bytes remaining\n", remaining); 265 break; 266 } 267 } 268 if (itemsLoaded >= MAX_DOMAINS ) { 269 // max 64K vectors else esp32 crashes 270 DBG_OUTPUT_PORT.printf("Blocklist truncated as maximum number of domains loaded - %u\n", MAX_DOMAINS); 271 break; 272 } 273 } 274 } 275 file.close(); 276 //for (int i=0; i < blockVec.size(); i++) DBG_OUTPUT_PORT.printf("%s\n", blockVec.at(i).c_str()); 277 278 if (psramFound()) heap_caps_malloc_extmem_enable(100000); 279 if (!itemsLoaded) { 280 puts("Aborting as empty blocklist read ..."); // SPIFFS issue 281 delay(100); 282 ESP.restart(); 283 } 284 display.print("Checking "); 285 display.print(itemsLoaded); 286 display.println(" domains"); 287 288 DBG_OUTPUT_PORT.printf("Loaded %u domains from blocklist file in %.01f secs\n", itemsLoaded, (float)((millis() - loadTime) / 1000.0)); 289 return true; 290 } 291 puts(" - not found"); 292 return false; 293 } 294 295 static bool sortBlocklist() { 296 // read downloaded blocklist from storage, sort in alpha order to allow binary search, rewrite to storage 297 free(downloadBuff); 298 delay(100); 299 if (loadBlocklist()) { 300 puts("Sorting blocklist alphabetically ..."); 301 uint32_t sortTime = millis(); 302 sort(blockVec.begin(), blockVec.end()); 303 DBG_OUTPUT_PORT.printf("Sorted blocklist after %0.1f secs, saving to file ...\n", (float)((millis() - sortTime) / 1000.0)); 304 sortTime = millis(); 305 std::string previous = ""; 306 int duplicate = 0; 307 308 // rewrite file with sorted domains 309 fatfs.remove(BLOCKLISTFILE); 310 File file = fatfs.open(BLOCKLISTFILE, FILE_WRITE); 311 if (file) { 312 for (auto domain : blockVec) { 313 if (domain.compare(previous) != 0) { 314 // store unduplicated domain 315 file.write((uint8_t*)domain.c_str(), strlen(domain.c_str())); 316 file.write((uint8_t*)"\n", 1); 317 previous.assign(domain); 318 } else duplicate++; 319 } 320 file.close(); 321 DBG_OUTPUT_PORT.printf("Saved into file removing %u duplicates after %0.1f secs, restarting ...\n", duplicate, (float)((millis() - sortTime) / 1000.0)); 322 delay(100); 323 ESP.restart(); // quicker to restart than clear vector 324 return true; 325 } else puts ("Failed to write to blocklist file"); 326 } 327 return false; 328 } 329 330 static size_t inline extractDomains() { 331 // extract domain names from downloaded blocklist 332 size_t downloadBuffPtr = 0; 333 char *saveLine, *saveItem = NULL; 334 char* tokenLine = strtok_r((char*)downloadBuff, "\n", &saveLine); 335 char* tokenItem; 336 337 // for each line 338 while (tokenLine != NULL) { 339 if (strncmp(tokenLine, "127.0.0.1", 9) == 0 || strncmp(tokenLine, "0.0.0.0", 7) == 0) { 340 // HOSTS file format matched, extract domain name 341 tokenItem = strtok_r(tokenLine, " \t", &saveItem); // skip over first token 342 if (tokenItem != NULL) tokenItem = strtok_r(NULL, " \t", &saveItem); // domain in second token 343 } else if (strncmp(tokenLine, "||", 2) == 0) 344 tokenItem = strtok_r(tokenLine, "|^", &saveItem); // Adblock format - domain in first token 345 else tokenItem = NULL; // no match 346 if (tokenItem != NULL) { 347 // write processed line back to buffer 348 size_t itemLen = strlen(tokenItem); 349 int wwwOffset = (strncmp(tokenItem, "www.", 4) == 0) ? 4 : 0; // remove any leading "www." 350 memcpy(downloadBuff + downloadBuffPtr, tokenItem + wwwOffset, itemLen - wwwOffset); 351 downloadBuffPtr += itemLen; 352 memcpy(downloadBuff + downloadBuffPtr, (uint8_t*)"\n", 1); 353 downloadBuffPtr++; 354 } 355 tokenLine = strtok_r(NULL, "\n", &saveLine); 356 } 357 downloadBuff[downloadBuffPtr] = 0; // string terminator 358 return downloadBuffPtr; 359 } 360 361 static bool createBlocklist() { 362 // after blocklist file downloaded, the content is parsed to extract the domain names, then written to storage 363 uint32_t createTime = millis(); 364 size_t blocklistSize = extractDomains(); 365 366 // check storage space available, else abort 367 size_t storageAvailable = flashFreeSpace(); 368 storageAvailable -= 1024*32; // leave overhead space 369 if (storageAvailable < blocklistSize) { 370 DBG_OUTPUT_PORT.printf("Aborting as insufficient storage %0.1fKB ...\n", inKB(storageAvailable)); 371 delay(100); 372 ESP.restart(); 373 } 374 DBG_OUTPUT_PORT.printf("Creating extracted unsorted blocklist of %0.1fKB\n", inKB(blocklistSize)); 375 376 377 Serial.println("-----------------------------------------------"); 378 Serial.println((const char *)downloadBuff); 379 Serial.println("-----------------------------------------------"); 380 381 File file = fatfs.open(BLOCKLISTFILE, FILE_WRITE); 382 if (file) { 383 // write buffer to file 384 size_t written = file.write(downloadBuff, blocklistSize); 385 if (!written) { 386 puts("Aborting as blocklist empty after writing ..."); // SPIFFS issue 387 delay(100); 388 ESP.restart(); 389 } 390 file.close(); 391 DBG_OUTPUT_PORT.printf("Blocklist file of %0.1fKB created in %.01f secs\n", inKB(written), (float)((millis() - createTime) / 1000.0)); 392 return (sortBlocklist()); 393 } else DBG_OUTPUT_PORT.printf("Failed to store blocklist %s\n", BLOCKLISTFILE); 394 free(downloadBuff); 395 return false; 396 } 397 398 static bool downloadFile() { 399 size_t downloadBuffPtr = 0; 400 WiFiClientSecure *client = new WiFiClientSecure; 401 if (!client) { 402 puts("Failed to create secure client"); 403 return false; 404 } else { 405 // scoping block 406 { 407 client->setInsecure(); // don't use a root cert 408 HTTPClient http; 409 // open connection to blocklist host 410 if (http.begin(*client, hostfile)) { 411 DBG_OUTPUT_PORT.printf("Downloading %s\n", hostfile); 412 uint32_t loadTime = millis(); 413 // start connection and send HTTP header 414 int httpCode = http.GET(); 415 416 if (httpCode > 0) { 417 DBG_OUTPUT_PORT.printf("Response code: %d\n", httpCode); 418 if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { 419 // get length of content (is -1 when Server sends no Content-Length header) 420 int len = http.getSize(); 421 if (len > 0) { 422 DBG_OUTPUT_PORT.printf("File size: %0.1fKB", inKB(len)); 423 } 424 else { 425 DBG_OUTPUT_PORT.printf("File size unknown"); 426 } 427 size_t availableMem = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); 428 DBG_OUTPUT_PORT.printf(", with %0.1fKB memory available for download ...\n", inKB(availableMem)); 429 if (len > 0 && len > availableMem) { 430 puts("Aborting as file too large"); 431 delay(100); 432 ESP.restart(); 433 } 434 435 int chunk = 128; // amount to consume per read of stream 436 // allocate memory for download buffer 437 downloadBuff = (psramFound()) ? (uint8_t*)ps_malloc(availableMem) : (uint8_t*)malloc(availableMem); 438 WiFiClient * stream = http.getStreamPtr(); // stream data to client 439 440 while (http.connected() && (len > 0 || len == -1)) { 441 size_t streamSize = stream->available(); 442 if (streamSize) { 443 // consume up to chunk bytes and write to memory 444 int readc = stream->readBytes(downloadBuff + downloadBuffPtr, ((streamSize > chunk) ? chunk : streamSize)); 445 downloadBuffPtr += readc; 446 if (len > 0) len -= readc; 447 if ((downloadBuffPtr + chunk) >= availableMem) { 448 puts("Aborting as file too large"); 449 delay(100); 450 ESP.restart(); 451 } 452 } 453 delay(1); 454 } 455 downloadBuff[downloadBuffPtr] = 0; // string terminator 456 Serial.println("-----------------------------------------------"); 457 Serial.println((const char *)downloadBuff); 458 Serial.println("-----------------------------------------------"); 459 DBG_OUTPUT_PORT.printf("Download completed in %0.1f secs, stored %0.1fKB\n", ((millis() - loadTime) / 1000.0), inKB(downloadBuffPtr)); 460 } 461 } else DBG_OUTPUT_PORT.printf("Connection failed with error: %s\n", http.errorToString(httpCode).c_str()); 462 } else { 463 DBG_OUTPUT_PORT.printf("Unable to download %s\n", hostfile); 464 delay(100); 465 ESP.restart(); 466 } 467 http.end(); 468 } 469 delete client; 470 } 471 return (downloadBuffPtr) ? true : false; 472 } 473 474 475 static inline time_t getEpochSecs() { 476 struct timeval tv; 477 gettimeofday(&tv, NULL); 478 return tv.tv_sec; 479 } 480 481 static void getNTP() { 482 // get current time from NTP server and apply to ESP32 483 const char* ntpServer = "0.adafruit.pool.ntp.org"; 484 const long gmtOffset_sec = 0; // offset from GMT 485 const int daylightOffset_sec = 3600; // daylight savings offset in secs 486 int i = 0; 487 do { 488 configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); 489 delay(1000); 490 } while (getEpochSecs() < 1000 && i++ < 5); // try up to 5 times 491 // set timezone as required 492 setenv("TZ", TIMEZONE, 1); 493 if (getEpochSecs() > 1000) { 494 time_t currEpoch = getEpochSecs(); 495 char timeFormat[20]; 496 strftime(timeFormat, sizeof(timeFormat), "%d/%m/%Y %H:%M:%S", localtime(&currEpoch)); 497 DBG_OUTPUT_PORT.printf("Got current time from NTP: %s\n", timeFormat); 498 } 499 else puts("Unable to sync with NTP"); 500 } 501 502 static void checkAlarm() { 503 // once per day at given time, load updated blocklist from host site 504 static time_t rolloverEpoch = setAlarm(UPDATE_HOUR); 505 if (getEpochSecs() >= rolloverEpoch) { 506 puts("Scheduled restart to load updated blocklist ..."); 507 delay(100); 508 ESP.restart(); 509 } 510 } 511 512 static time_t setAlarm(uint8_t alarmHour) { 513 // calculate future alarm datetime based on current datetime 514 struct tm* timeinfo; 515 time_t rawtime; 516 time(&rawtime); 517 timeinfo = localtime(&rawtime); 518 // set alarm date & time for next day at given hour 519 timeinfo->tm_mday += 1; 520 timeinfo->tm_hour = alarmHour; 521 timeinfo->tm_min = 0; 522 // return future datetime as epoch seconds 523 return mktime(timeinfo); 524 }