ho scritto uno sniffer di pacchetti per imparare i protocolli, poi ho mentito su cosa rileva

cybersecurity · Oct 4, 2026 · 5 min read

ho costruito sniffcli perché volevo capire che cosa attraversa davvero la mia rete. avevo usato tcpdump per anni e restavo completamente incapace di spiegare che cosa significasse una singola riga del suo output.

il piano era semplice: scrivere la cosa per conto mio, leggere i pacchetti, imparare i protocolli.

il piano ha funzionato. ma non nella direzione che mi aspettavo.

prima la parte onesta

il readme dice che lo strumento fa «rilevamento di brute force ssh». non lo fa. ecco l'intera funzionalità:

if dstPort == 22 {
  sshCounters[info.Source]++
  if sshCounters[info.Source] > 3 {
    info.IsAlert = true
    info.Info = "POTENTIAL SSH BRUTE FORCE"
  }
}

è tutto. conta i pacchetti verso la porta 22, per indirizzo di origine, e urla dopo il quarto.

questo non può rilevare un attacco di brute force. non può vedere una password, perché ssh cifra l'intera sessione. lo scambio di autenticazione avviene dentro uno stream tcp cifrato, e uno sniffer passivo sul filo non ha modo di entrarci. anche se accendessi tcpdump -A vedrei ciphertext binario.

quello che in realtà rileva è «qualcosa sulla mia rete ha parlato con la porta 22 più di tre volte». non è rilevamento di brute force. è un contatore di connessioni con un messaggio spaventoso attaccato.

perché ho comunque tenuto la funzione

perché non è inutile, è solo mal nominata. più di tre connessioni alla porta 22 in una finestra breve è un segnale reale — l'ho innescato io stesso con dei retry di ssh dopo un refuso. e lo stesso meccanismo intercetta uno scanner che sta percorrendo la rete.

quello che avrei dovuto fare è etichettarla per quello che è. «connessioni ssh ripetute» è onesto. «rilevamento di brute force» promette qualcosa che il codice non può consegnare, e quella promessa l'ho scritta io.

la lezione generale, che mi è costata un tempo vergognosamente lungo: una funzione nominata dopo una minaccia implica una capacità che potresti non aver costruito. se il tuo strumento dichiara di rilevare attacchi, la prova è se riesce a vedere ciò che gli attaccanti inviano davvero. il mio non ne vede nessuna parte.

cosa ho imparato sui protocolli

più di quanto mi aspettassi, e nulla dalla documentazione.

è lo stratiamento la parte che alla fine è scattata. un singolo frame non è una cosa, è una pila, e gopacket ti consegna ogni strato separatamente:

if arpLayer := packet.Layer(layers.LayerTypeARP); arpLayer != nil {
  // ...
} else if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil {
  // ...
} else if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
  // ...
}

tcp non è «il pacchetto». è uno strato che può esistere senza ip (alcune incapsulazioni), ed è per questo che il codice reale controlla tcp e udp nel loro ramo dopo gli strati di indirizzo, invece di assumere un ordine fisso.

«chi ha» è ciò che un broadcast arp è: una macchina che chiede a tutta la rete «chi ha questo ip, dimmi il tuo mac». è la prima cosa che vedi se inizi a sniffare su una rete cablata, e inonda di continuo perché arp non ha memoria.

il traffico multicast e di discovery è quasi tutto il rumore. 224.0.0.0/24 per ipv4, ff02:: per ipv6, più mdns su 5353, ssdp su 1900, dhcp su 67/68. i miei primi venti minuti di sniffing sono stati quasi interamente questo. il flag -c esiste solo per nasconderlo.

una connessione è bidirezionale, un pacchetto no. lo strumento legge tcp.SrcPort e tcp.DstPort su ogni singolo frame, quindi la stessa conversazione appare due volte con le porte scambiate. non c'è riassemblaggio integrato, e non ne scrivo uno. è un limite reale e preferisco nominarlo che fingere.

la parte che ho sbagliato due volte

isLocalIP decide se un pacchetto è «in uscita insolita». tratta loopback, link-local, multicast e i tre intervalli privati come locali.

il bug: tutto ciò che sta dentro 192.168.0.0/16 conta come locale, incluse le altre macchine della mia rete. quindi un dispositivo che esfiltra in silenzio verso il nas accanto produce zero alert. la mia regola dice «non sta uscendo di casa», che non è la stessa cosa di «non sta uscendo dalla mia macchina».

la correzione onesta è confrontare con l'indirizzo dell'interfaccia reale, non con un intervallo di reti possibili. non l'ho corretto. è scritto qui, ed è l'unico motivo per cui esiste questo paragrafo.

la parte di cui sono più contento

una ui concorrente che non scatta.

lo sniffer gira nella sua goroutine e spinge i pacchetti nel programma tea:

p := tea.NewProgram(m, tea.WithAltScreen())
go startSniffing(iface, watchlist, p)
if _, err := p.Run(); err != nil {
  // ...
}

il loop non tocca mai direttamente la ui, invia solo messaggi. è tutta la ragione per cui l'interfaccia resta reattiva durante un picco di traffico — una regola che già conoscevo dal lato frontend del mio sito, e che ho apprezzato davvero solo quando la ui è diventata un terminale invece del dom.

è anche la ragione per cui HighPerformanceRendering è esplicitamente false: è un log in sola lettura che l'utente scorre, quindi lasciar saltare i frame a bubbletea è più economico che mantenerli fluidi.

e la piccola cosa che l'ha reso piacevole da usare: una mappa di alias dei dispositivi, così 192.168.1.185 viene mostrato come «main rig» e un mac sorvegliato compare in arancione. gli strumenti di sicurezza hanno una reputazione di essere sgradevoli, e metà dell'uso renderne uno piacevole è lasciar vedere la propria casa in parole umane.

cosa direi al mio io di prima

apri il file che hai scritto e conta le righe che fanno davvero quello che il nome promette.

se la risposta sono tre righe e contano qualcosa di adiacente alla minaccia, hai scritto un contatore di connessioni. rinominalo prima che qualcuno si fidi — te compreso, sei mesi dopo, quando ti chiedi perché gli alert ti sembrano sbagliati.