Network Sweeper

A pocket-sized ESP32 network auditor. It autonomously scans Wi-Fi, fingerprints vulnerable ports, and syncs data to a cloud dashboard.

IntermediateFull instructions provided4 hours9
Network Sweeper

Things used in this project

Story

Read more

Schematics

Schematic

Code

main

C/C++
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiManager.h> 
#include <vector>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include <HTTPClient.h>

// Bibliotecas do kernel LwIP para Sockets de baixo nível
#include "lwip/sockets.h"
#include <errno.h>
#include "lwip/etharp.h"
#include "lwip/ip4_addr.h"

// Bibliotecas para o display I2C
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C* activeLcd = nullptr;
bool lcdAvailable = false;

// ============================
// CONFIGURAÇÕES GERAIS
// ============================
const char* ANON_KEY = "yourAnonKey";
const char* KNOWN_DEVICES_FILE = "/known_macs.json";

// Endereços das funções RPC no Supabase (Sistema de Lotes)
const char* URL_START_SCAN = "start_scan";
const char* URL_BATCH_UPLOAD = "insert_scan_batch";

unsigned long lastScanTime = 0;
const unsigned long SCAN_INTERVAL = 3 * 60 * 1000; 

AsyncWebServer server(80);
std::vector<String> knownMACs; 

struct ServiceInfo {
  int port;
  String name;
  bool is_vulnerable;
};

// false = porta comum segura | true = porta crítica/legado
const ServiceInfo target_services[] = {
  {80, "HTTP", false}, {443, "HTTPS", false}, {8080, "HTTP-Alt", false}, 
  {8443, "HTTPS-Alt", false}, {22, "SSH", false}, {23, "Telnet", true}, 
  {3389, "RDP", false}, {139, "NetBIOS", true}, {445, "SMB", false}, 
  {21, "FTP", true}, {9100, "RawPrint", false}, {515, "LPD", false}
};
const int num_services = sizeof(target_services) / sizeof(target_services[0]);

struct NetworkDevice {
  IPAddress ip;
  String mac;
  String status;
  bool vulnerable;
  bool is_new; 
  std::vector<ServiceInfo> active_ports;
};

std::vector<NetworkDevice> activeDevices;

// =========================================================================
// CONTROLE DO DISPLAY LCD FÍSICO
// =========================================================================
void lcdPrint(String line1, String line2) {
  if (lcdAvailable && activeLcd != nullptr) {
    activeLcd->clear();
    
    activeLcd->setCursor(0, 0);
    activeLcd->print(line1.substring(0, 16)); 
    
    activeLcd->setCursor(0, 1);
    activeLcd->print(line2.substring(0, 16));
  }
  
  Serial.println("\n[LCD] " + line1 + " | " + line2);
}

bool i2cDeviceFound(uint8_t address) {
  Wire.beginTransmission(address);
  return Wire.endTransmission() == 0;
}

uint8_t scanI2CAddress() {
  for (uint8_t address = 1; address < 127; address++) {
    if (i2cDeviceFound(address)) {
      Serial.printf("[Boot] Dispositivo I2C encontrado em 0x%02X\n", address);
      return address;
    }
  }

  return 0;
}

void initLcdIfPresent() {
  uint8_t lcdAddress = 0;

  if (i2cDeviceFound(0x3F)) {
    lcdAddress = 0x3F;
  } else if (i2cDeviceFound(0x27)) {
    lcdAddress = 0x27;
  } else {
    lcdAddress = scanI2CAddress();
  }

  if (lcdAddress != 0) {
    activeLcd = new LiquidCrystal_I2C(lcdAddress, 16, 2);
    activeLcd->init();
    activeLcd->backlight();
    lcdAvailable = true;
    Serial.printf("[Boot] LCD inicializado em 0x%02X\n", lcdAddress);
    return;
  }

  Serial.println("[Boot] LCD nao encontrado em nenhum endereco I2C. Continuando apenas pelo Serial.");
}

// =========================================================================
// GESTÃO DE ESTADO (PERSISTÊNCIA SPIFFS)
// =========================================================================
void loadKnownDevices() {
  if (!SPIFFS.exists(KNOWN_DEVICES_FILE)) return;
  
  File file = SPIFFS.open(KNOWN_DEVICES_FILE, FILE_READ);
  DynamicJsonDocument doc(4096);
  DeserializationError error = deserializeJson(doc, file);
  file.close();

  if (!error) {
    JsonArray array = doc.as<JsonArray>();
    for (JsonVariant v : array) {
      knownMACs.push_back(v.as<String>());
    }
    Serial.printf("Memoria restaurada: %d dispositivos conhecidos.\n", knownMACs.size());
  }
}

void saveKnownDevices() {
  File file = SPIFFS.open(KNOWN_DEVICES_FILE, FILE_WRITE);
  DynamicJsonDocument doc(4096);
  JsonArray array = doc.to<JsonArray>();
  
  for (String mac : knownMACs) {
    array.add(mac);
  }
  
  serializeJson(doc, file);
  file.close();
}

bool isDeviceNew(String mac) {
  if (mac == "Desconhecido") return false; 
  for (String known : knownMACs) {
    if (known == mac) return false;
  }
  return true;
}

// =========================================================================
// FUNÇÃO DE BAIXO NÍVEL: Sockets POSIX
// =========================================================================
int checkSocketStatus(IPAddress ip, int port, int timeout_ms) {
  int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (sock < 0) return 2;

  int flags = fcntl(sock, F_GETFL, 0);
  fcntl(sock, F_SETFL, flags | O_NONBLOCK);

  struct sockaddr_in dest_addr;
  dest_addr.sin_family = AF_INET;
  dest_addr.sin_port = htons(port);
  dest_addr.sin_addr.s_addr = static_cast<uint32_t>(ip);

  int res = connect(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
  
  if (res < 0 && errno == EINPROGRESS) {
    fd_set write_set;
    FD_ZERO(&write_set);
    FD_SET(sock, &write_set);
    
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = timeout_ms * 1000; 
    
    res = select(sock + 1, NULL, &write_set, NULL, &tv);
    
    if (res > 0) {
      int sock_err = 0;
      socklen_t optlen = sizeof(sock_err);
      getsockopt(sock, SOL_SOCKET, SO_ERROR, &sock_err, &optlen);
      close(sock); 
      
      if (sock_err == 0) return 0; 
      if (sock_err == ECONNREFUSED || sock_err == ECONNRESET) return 1; 
      return 2; 
    }
  }
  close(sock);
  return 2; 
}

// =========================================================================
// UPLOAD SUPABASE EM LOTES (ANTI-ESTOURO DE RAM)
// =========================================================================
void uploadToSupabase() {
  if (WiFi.status() != WL_CONNECTED) return;
  
  lcdPrint("Iniciando Upload", "Conectando API...");

  HTTPClient http;
  String scan_id = "";

  // -------------------------------------------------------------------------
  // FASE A: Criar o cabeçalho do Scan e recuperar o UUID gerado
  // -------------------------------------------------------------------------
  http.begin(URL_START_SCAN);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("apikey", ANON_KEY);
  http.addHeader("Authorization", String("Bearer ") + ANON_KEY);

  DynamicJsonDocument docStart(512);
  docStart["p_network_ssid"] = WiFi.SSID();
  String startPayload;
  serializeJson(docStart, startPayload);

  int httpCode = http.POST(startPayload);
  
  if (httpCode >= 200 && httpCode < 300) {
    String response = http.getString();
    response.replace("\"", ""); // Limpa as aspas para obter o UUID limpo
    scan_id = response;
    scan_id.trim();
    Serial.println("[Nuvem] Scan ID gerado: " + scan_id);
  } else {
    lcdPrint("Erro Inicializ.", "HTTP: " + String(httpCode));
    http.end();
    return;
  }
  http.end();

  // -------------------------------------------------------------------------
  // FASE B: Transmitir os Dispositivos em lotes (Chunks de 10 em 10)
  // -------------------------------------------------------------------------
  int totalDevices = activeDevices.size();
  int chunkSize = 10; 
  int totalBatches = (totalDevices + chunkSize - 1) / chunkSize;

  for (int i = 0; i < totalDevices; i += chunkSize) {
    int currentBatch = (i / chunkSize) + 1;
    lcdPrint("Enviando Lote", String(currentBatch) + "/" + String(totalBatches));

    http.begin(URL_BATCH_UPLOAD);
    http.addHeader("Content-Type", "application/json");
    http.addHeader("apikey", ANON_KEY);
    http.addHeader("Authorization", String("Bearer ") + ANON_KEY);

    // Buffer controlado de 4KB - perfeitamente seguro para a Heap do ESP32
    DynamicJsonDocument docBatch(4096);
    docBatch["p_scan_id"] = scan_id;
    JsonArray devicesArr = docBatch.createNestedArray("p_devices");

    for (int j = i; j < i + chunkSize && j < totalDevices; j++) {
      const auto& dev = activeDevices[j];
      JsonObject devObj = devicesArr.createNestedObject();
      devObj["ip"] = dev.ip.toString();
      devObj["mac"] = dev.mac;
      devObj["status"] = dev.status;
      devObj["vulnerable"] = dev.vulnerable;
      devObj["is_new"] = dev.is_new;

      JsonArray portsArr = devObj.createNestedArray("open_ports");
      for (const auto& port : dev.active_ports) {
        JsonObject portObj = portsArr.createNestedObject();
        portObj["port"] = port.port;
        portObj["name"] = port.name;
        portObj["is_vulnerable"] = port.is_vulnerable;
      }
    }

    String batchPayload;
    serializeJson(docBatch, batchPayload);

    int batchHttpCode = http.POST(batchPayload);
    Serial.printf("[Lote %d/%d] Resposta HTTP: %d\n", currentBatch, totalBatches, batchHttpCode);
    
    if (batchHttpCode < 200 || batchHttpCode >= 300) {
      Serial.println("[Erro] Falha ao enviar lote.");
    }
    http.end();
    delay(150); // Respiro obrigatório para estabilização da pilha TCP
  }

  lcdPrint("Upload Concluido", "Hosts: " + String(totalDevices));
}

// =========================================================================
// MOTOR DE VARREDURA
// =========================================================================
void performDeepScan() {
  Serial.println("[Scan] Iniciando varredura profunda");
  lcdPrint("Iniciando Scan", "Varrendo Rede...");
  
  activeDevices.clear();
  bool updatedKnownList = false;

  IPAddress localIP = WiFi.localIP();
  IPAddress subnetMask = WiFi.subnetMask();
  
  uint32_t ip_num = localIP[0] << 24 | localIP[1] << 16 | localIP[2] << 8 | localIP[3];
  uint32_t mask_num = subnetMask[0] << 24 | subnetMask[1] << 16 | subnetMask[2] << 8 | subnetMask[3];
  uint32_t network_num = ip_num & mask_num;
  uint32_t broadcast_num = network_num | (~mask_num);

  // --- FASE 1: DESCOBERTA ---
  int scannedHosts = 0;
  for (uint32_t i = network_num + 1; i < broadcast_num; i++) {
    IPAddress targetIP(i >> 24, (i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF);
    if (targetIP == localIP) continue; 

    scannedHosts++;
    if (scannedHosts % 25 == 0) {
      Serial.println("[Scan] Verificados " + String(scannedHosts) + " hosts. IP atual: " + targetIP.toString());
    }
    yield();

    int trigger = checkSocketStatus(targetIP, 44444, 200);
    
    bool deviceIsAlive = false;
    String statusMsg = "";
    String macFound = "Desconhecido";

    if (trigger == 1 || trigger == 0) {
      deviceIsAlive = true;
      statusMsg = "Exposto";
    } 

    ip4_addr_t *ipaddr = nullptr;
    struct netif *netif = nullptr;
    struct eth_addr *eth_ret = nullptr;

    for (int j = 0; j < ARP_TABLE_SIZE; j++) {
      if (etharp_get_entry(j, &ipaddr, &netif, &eth_ret)) {
        if (ipaddr == nullptr || eth_ret == nullptr) continue;
        IPAddress cachedIP(ip4_addr1(ipaddr), ip4_addr2(ipaddr), ip4_addr3(ipaddr), ip4_addr4(ipaddr));
        if (cachedIP == targetIP) {
          deviceIsAlive = true;
          char macStr[18];
          snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
                   eth_ret->addr[0], eth_ret->addr[1], eth_ret->addr[2],
                   eth_ret->addr[3], eth_ret->addr[4], eth_ret->addr[5]);
          macFound = String(macStr);
          if (trigger == 2) statusMsg = "Stealth";
          break; 
        }
      }
    }

    if (deviceIsAlive) {
      bool isNew = isDeviceNew(macFound);
      
      if (isNew && macFound != "Desconhecido") {
        lcdPrint("NOVO DETECTADO!", targetIP.toString());
        knownMACs.push_back(macFound);
        updatedKnownList = true;
        delay(1500); 
      }

      activeDevices.push_back({targetIP, macFound, statusMsg, false, isNew, std::vector<ServiceInfo>()});
    }
  }

  if (updatedKnownList) saveKnownDevices();

  // --- FASE 2: FINGERPRINTING ---
  lcdPrint("Analisando", "Portas e Vulns");
  
  for (auto& device : activeDevices) {
    lcdPrint("Checando IP:", device.ip.toString());

    for (int p = 0; p < num_services; p++) {
      int status = checkSocketStatus(device.ip, target_services[p].port, 300);
      
      if (status == 0) { 
        device.active_ports.push_back(target_services[p]);
        if (target_services[p].is_vulnerable) device.vulnerable = true;
      }
      delay(20); 
    }
  }
  
  lcdPrint("Scan Concluido", "Hosts: " + String(activeDevices.size()));
  Serial.println("[Scan] Concluido. Hosts ativos: " + String(activeDevices.size()));
  delay(1000);
  uploadToSupabase();
  
  lcdPrint("Modo Standby", WiFi.localIP().toString());
}

// =========================================================================
// SETUP E LOOP
// =========================================================================
void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println();
  Serial.println("===== Netsweeper boot =====");
  Serial.println("[Boot] Serial iniciado em 115200");
  
  // Inicialização obrigatória do Hardware I2C e Backlight
  Serial.println("[Boot] Inicializando LCD I2C...");
  Wire.begin(21, 22);
  initLcdIfPresent();
  
  lcdPrint("Iniciando", "Sistema...");

  if (!SPIFFS.begin(true)) {
    Serial.println("Erro ao iniciar SPIFFS");
  } else {
    Serial.println("[Boot] SPIFFS iniciado");
  }
  loadKnownDevices(); 

  lcdPrint("Configurando", "Rede WiFi...");
  Serial.println("[WiFi] Tentando conectar. Se falhar, abre o AP Netsweeper_AP / admin123");
  WiFiManager wm;
  
  // Cria portal cativo chamado "Netsweeper_AP" com senha "admin123" se falhar em conectar
  bool res = wm.autoConnect("Netsweeper_AP", "admin123");

  if(!res) {
    Serial.println("[WiFi] Falha ao conectar. Reiniciando...");
    lcdPrint("Falha na Rede", "Reiniciando...");
    delay(3000);
    ESP.restart();
  } 

  Serial.println("[WiFi] Conectado: " + WiFi.localIP().toString());
  lcdPrint("Conectado!", WiFi.localIP().toString());
  delay(2000);

  server.begin();
  Serial.println("[Web] Servidor iniciado na porta 80");
  
  performDeepScan();
  lastScanTime = millis();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    lcdPrint("Rede Perdida!", "Reconectando...");
    WiFi.reconnect();
    while (WiFi.status() != WL_CONNECTED) {
      delay(1000);
    }
    lcdPrint("Reconectado!", WiFi.localIP().toString());
  }

  if (millis() - lastScanTime >= SCAN_INTERVAL) {
    performDeepScan();
    lastScanTime = millis();
  }
}

Credits

JULIO CESAR DA MOTA LIMEIRA
2 projects • 0 followers
Fernando F de Carvalho
26 projects • 8 followers

Comments