feat: add soft scan mode (slow ICMP) to avoid switch/AP rate-limiting

Reduces ICMP concurrency from 100 to 10 workers when soft_scan=true,
spreading out probes to avoid rate-limiting on managed switches and APs.
The option is hidden in the UI when TCP check is active (redundant).

Update README (en/fr/es), docs/backend.md with the new scan modes table
and a troubleshooting entry for ICMP rate-limiting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:26:50 +02:00
parent aa39898c80
commit 5c34143b52
7 changed files with 70 additions and 6 deletions
+12 -1
View File
@@ -429,13 +429,24 @@ npm run dev # Servidor dev Vite en :5173, proxifica /api/ hacia :8000
**Causa** — Algunos equipos de red (especialmente UniFi Security Gateway, Dream Machine y equipos similares) activan el proxy-ARP y responden a los pings ICMP para **todas** las IPs de la subred, suplantando la IP de origen de la respuesta. La verificación de IP de origen integrada en el escáner no puede filtrar estos falsos positivos.
**Solución** — Activar la opción **"Verificación TCP (anti proxy-ARP)"** en la pantalla de configuración del escaneo. Esta opción sondea cada host en los puertos TCP 22, 80, 443, 8080 y 8443 tras el ping ICMP:
**Solución** — Activar la opción **"Escaneo TCP (anti proxy-ARP)"** en la pantalla de configuración del escaneo. Esta opción usa TCP en lugar de ICMP para detectar hosts activos (puertos 22, 80, 443, 8080, 8443):
- Un **host real** responde con RST (puerto cerrado) o acepta la conexión → considerado activo.
- Una **IP fantasma**: el gateway descarta silenciosamente el SYN sin responder → timeout → eliminada.
> **Nota**: un equipo cuyo firewall descarte silenciosamente (*DROP*, sin RST) **todos** los puertos sondeados no será descubierto automáticamente y deberá añadirse manualmente.
### Faltan equipos en el escaneo (rate-limiting ICMP)
**Síntoma** — Algunos hosts (APs Wi-Fi, switches, dispositivos IoT) responden a un ping directo pero no aparecen en un escaneo completo de la subred.
**Causa** — Cuando 100 workers ICMP concurrentes inundan un `/24`, algunos equipos o switches gestionados limitan las respuestas ICMP. El equipo ignora la sonda durante el escaneo aunque un ping simple funcione correctamente.
**Solución** — Dos opciones:
- Activar **"Escaneo suave (ICMP lento)"**: reduce la concurrencia de 100 a 10 workers. El escaneo tarda más pero las sondas ICMP se distribuyen en el tiempo, evitando el rate-limiting. Ideal para redes sin proxy-ARP.
- Activar **"Escaneo TCP (anti proxy-ARP)"**: evita ICMP por completo. Las sondas TCP no están sujetas al mismo rate-limiting. Ideal cuando hay tanto proxy-ARP como rate-limiting.
---
## 🏗️ Arquitectura
+12 -1
View File
@@ -429,13 +429,24 @@ npm run dev # Serveur dev Vite sur :5173, proxifie /api/ vers :8000
**Cause** — Certains équipements réseau (notamment UniFi Security Gateway, Dream Machine et équipements similaires) activent le proxy-ARP et répondent aux pings ICMP pour **toutes** les IP du sous-réseau, en usurpant l'IP source de la réponse. La vérification de l'IP source intégrée au scan ne peut donc pas filtrer ces faux positifs.
**Solution** — Cocher l'option **"Vérification TCP (anti proxy-ARP)"** dans l'écran de configuration du scan. Cette option sonde chaque hôte sur les ports TCP 22, 80, 443, 8080 et 8443 après le ping ICMP :
**Solution** — Cocher l'option **"Scan TCP (anti proxy-ARP)"** dans l'écran de configuration du scan. Cette option utilise TCP au lieu de ICMP pour détecter les hôtes actifs (ports 22, 80, 443, 8080, 8443) :
- Un **vrai hôte** répond avec RST (port fermé) ou accepte la connexion → considéré vivant.
- Une **IP fantôme** : le gateway droppe silencieusement le SYN sans répondre → timeout → éliminée.
> **Note** : un équipement dont le pare-feu bloque silencieusement (*DROP*, sans RST) **tous** les ports sondés ne sera pas découvert automatiquement et devra être ajouté manuellement.
### Des équipements manquent dans le scan (rate-limiting ICMP)
**Symptôme** — Quelques hôtes (AP Wi-Fi, switchs, appareils IoT) répondent à un ping direct mais n'apparaissent pas lors d'un scan complet du sous-réseau.
**Cause** — Quand 100 workers ICMP concurrents inondent un `/24`, certains équipements ou switchs managés bridant les réponses ICMP. L'équipement ignore la sonde pendant le scan alors qu'un ping simple fonctionne.
**Solution** — Deux options :
- Activer **"Scan doux (ICMP lent)"** : réduit la concurrence de 100 à 10 workers. Le scan prend plus de temps mais les sondes ICMP sont étalées, évitant le rate-limiting. Idéal pour les réseaux sans proxy-ARP.
- Activer **"Scan TCP (anti proxy-ARP)"** : contourne ICMP entièrement. Les sondes TCP ne sont pas soumises au même rate-limiting. Idéal si proxy-ARP et rate-limiting sont tous deux présents.
---
## 🏗️ Architecture
+12 -1
View File
@@ -429,13 +429,24 @@ npm run dev # Vite dev server on :5173, proxies /api/ to :8000
**Cause** — Some network equipment (notably UniFi Security Gateway, Dream Machine, and similar devices) enables proxy-ARP and responds to ICMP pings for **every** IP in the subnet, spoofing the source IP of the reply. The built-in source-IP check in the scanner cannot filter these false positives.
**Fix** — Enable the **"TCP check (anti proxy-ARP)"** option in the scan configuration screen. This option probes each host on TCP ports 22, 80, 443, 8080, and 8443 after the ICMP ping:
**Fix** — Enable the **"TCP check (anti proxy-ARP)"** option in the scan configuration screen. This option uses TCP instead of ICMP to detect live hosts (ports 22, 80, 443, 8080, 8443):
- A **real host** replies with RST (port closed) or accepts the connection → marked alive.
- A **ghost IP**: the gateway silently drops the SYN without replying → timeout → discarded.
> **Note**: a device whose firewall silently drops (*DROP*, without RST) **all** probed ports will not be discovered automatically and must be added manually.
### Some devices are missing from the scan (ICMP rate-limiting)
**Symptom** — A few hosts (APs, switches, IoT devices) respond to a direct ping but are not found during a full subnet scan.
**Cause** — When 100 concurrent ICMP workers flood a `/24`, some devices or managed switches rate-limit ICMP responses. The device drops the probe during the scan even though a single ping works fine.
**Fix** — Two options:
- Enable **"Soft scan (slow ICMP)"**: reduces concurrency from 100 to 10 workers. The scan takes longer but ICMP probes are spread out, avoiding rate-limiting. Best for subnets without proxy-ARP.
- Enable **"TCP check (anti proxy-ARP)"**: bypasses ICMP entirely. TCP probes are not subject to the same rate-limiting. Best when both proxy-ARP and rate-limiting are present.
---
## 🏗️ Architecture
+6 -1
View File
@@ -29,6 +29,7 @@ class ScanRequest(BaseModel):
dns_server: str = ""
targets: list[ScanTarget]
tcp_check: bool = False
soft_scan: bool = False
@field_validator("dns_server")
@classmethod
@@ -189,7 +190,11 @@ def scan(req: ScanRequest):
t0 = time.time()
results: list[DiscoveredHost] = []
with ThreadPoolExecutor(max_workers=100) as pool:
# Soft scan reduces ICMP concurrency to avoid rate-limiting on switches/APs.
# Has no effect in tcp_check mode (TCP probes are not rate-limited the same way).
workers = 10 if (req.soft_scan and not req.tcp_check) else 100
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(_scan_one, *args, tcp_check=req.tcp_check) for args in tasks]
for f in as_completed(futures):
host = f.result()
+12 -2
View File
@@ -125,7 +125,9 @@ Ping sweep + DNS PTR reverse lookup for one or more CIDR ranges.
"dns_server": "192.168.1.1",
"targets": [
{ "vlan_id": 1, "cidr": "192.168.1.0/24" }
]
],
"tcp_check": false,
"soft_scan": false
}
// Response
@@ -136,7 +138,15 @@ Ping sweep + DNS PTR reverse lookup for one or more CIDR ranges.
}
```
Maximum 1024 hosts per target (rejects /21 and wider). 100 concurrent workers. DNS queries use `dnspython` with a 1s timeout.
Maximum 1024 hosts per target (rejects /21 and wider). DNS queries use `dnspython` with a 1s timeout.
**Scan modes** (mutually exclusive options):
| `tcp_check` | `soft_scan` | Behaviour |
|:-----------:|:-----------:|-----------|
| `false` | `false` | ICMP ping, 100 concurrent workers (default) |
| `false` | `true` | ICMP ping, 10 concurrent workers — avoids rate-limiting by switches/APs |
| `true` | _(ignored)_ | TCP-only (ports 22, 80, 443, 8080, 8443), 100 workers — eliminates proxy-ARP false positives |
---
@@ -63,6 +63,14 @@
<div class="input-hint">{{ t('tcpCheckHint') }}</div>
</div>
<div v-if="!tcpCheck" class="field field-toggle">
<label class="toggle-row">
<input type="checkbox" v-model="softScan" class="toggle-checkbox" />
<span class="toggle-label">{{ t('softScanLabel') }}</span>
</label>
<div class="input-hint">{{ t('softScanHint') }}</div>
</div>
<div v-if="configError" class="error-box">{{ configError }}</div>
</div>
@@ -186,6 +194,7 @@ const emit = defineEmits(['close', 'refresh'])
const step = ref('config')
const dnsServer = ref('')
const tcpCheck = ref(false)
const softScan = ref(false)
const selectedVlanIds = ref([])
const results = ref([])
const selectedIps = ref([])
@@ -272,6 +281,7 @@ async function startScan() {
dns_server: dnsServer.value.trim(),
targets,
tcp_check: tcpCheck.value,
soft_scan: softScan.value,
})
results.value = resp.data.hosts
scanMeta.value = { total_scanned: resp.data.total_scanned, duration_s: resp.data.duration_s }
+6
View File
@@ -130,6 +130,8 @@ const LANGS = {
scanNote: 'Chaque hôte est pingé puis interrogé en DNS.',
tcpCheckLabel: 'Scan TCP (anti proxy-ARP)',
tcpCheckHint: 'Utilise TCP au lieu de ICMP pour détecter les hôtes actifs (ports 22, 80, 443, 8080, 8443). Élimine les faux positifs proxy-ARP (UniFi…) et détecte les équipements dont le ping ICMP est bridé sous charge. Peut rater les équipements qui bloquent silencieusement (DROP) tous ces ports.',
softScanLabel: 'Scan doux (ICMP lent)',
softScanHint: 'Réduit la concurrence ICMP de 100 à 10 workers pour éviter le rate-limiting des switchs et AP. Le scan prend plus de temps mais manque moins d'équipements.',
hostsFound: 'hôte(s) découvert(s)',
addressesScanned: 'adresses scannées',
newHosts: 'nouveaux',
@@ -289,6 +291,8 @@ const LANGS = {
scanNote: 'Each host is pinged then queried via DNS.',
tcpCheckLabel: 'TCP scan (anti proxy-ARP)',
tcpCheckHint: 'Uses TCP instead of ICMP to detect live hosts (ports 22, 80, 443, 8080, 8443). Eliminates proxy-ARP false positives (UniFi) and detects hosts whose ICMP is rate-limited under load. May miss devices that silently block (DROP) all probed ports.',
softScanLabel: 'Soft scan (slow ICMP)',
softScanHint: 'Reduces ICMP concurrency from 100 to 10 workers to avoid rate-limiting by switches and APs. The scan takes longer but misses fewer devices.',
hostsFound: 'host(s) found',
addressesScanned: 'addresses scanned',
newHosts: 'new',
@@ -447,6 +451,8 @@ const LANGS = {
scanNote: 'Cada host es pingado y luego consultado en DNS.',
tcpCheckLabel: 'Escaneo TCP (anti proxy-ARP)',
tcpCheckHint: 'Usa TCP en lugar de ICMP para detectar hosts activos (puertos 22, 80, 443, 8080, 8443). Elimina falsos positivos de proxy-ARP (UniFi) y detecta hosts con ICMP limitado bajo carga. Puede omitir equipos que bloqueen silenciosamente (DROP) todos los puertos sondeados.',
softScanLabel: 'Escaneo suave (ICMP lento)',
softScanHint: 'Reduce la concurrencia ICMP de 100 a 10 workers para evitar el rate-limiting de switches y APs. El escaneo tarda más pero detecta más equipos.',
hostsFound: 'host(s) descubierto(s)',
addressesScanned: 'direcciones escaneadas',
newHosts: 'nuevos',