escribí un sniffer de paquetes para aprender protocolos, y luego mentí sobre lo que detecta
cybersecurity · Oct 4, 2026 · 5 min read
construí sniffcli porque quería entender qué cruza realmente mi red. llevaba años usando tcpdump y seguía siendo incapaz de explicar qué significaba una sola línea de su salida.
el plan era simple: escribir la cosa por mí mismo, leer los paquetes, aprender los protocolos.
el plan funcionó. pero no en la dirección que esperaba.
primero la parte honesta
el readme dice que la herramienta hace «detección de fuerza bruta ssh». no lo hace. aquí está la función entera:
if dstPort == 22 {
sshCounters[info.Source]++
if sshCounters[info.Source] > 3 {
info.IsAlert = true
info.Info = "POTENTIAL SSH BRUTE FORCE"
}
}
eso es todo. cuenta los paquetes al puerto 22, por dirección de origen, y grita después del cuarto.
esto no puede detectar un ataque de fuerza bruta. no puede ver una contraseña, porque ssh cifra la sesión entera. el intercambio de autenticación ocurre dentro de un flujo tcp cifrado, y un sniffer pasivo sobre el cable no tiene forma de entrar. incluso si activara tcpdump -A vería texto cifrado binario.
así que lo que realmente detecta es «algo en mi red habló con el puerto 22 más de tres veces». eso no es detección de fuerza bruta. es un contador de conexiones con un mensaje assustador pegado.
por qué mantuve la función
porque no es inútil, está mal nombrada. más de tres conexiones al puerto 22 en una ventana breve es una señal real — la he activado yo mismo con reintentos de ssh tras una errata. y el mismo mecanismo atrapa a un escáner que recorre la red.
lo que debería haber hecho es etiquetarla por lo que es. «conexiones ssh repetidas» es honesto. «detección de fuerza bruta» promete algo que el código no puede entregar, y esa promesa la escribí yo.
la lección general, que me costó un tiempo vergonzosamente largo aprender: una función nombrada después de una amenaza implica una capacidad que puede que no hayas construido. si tu herramienta dice detectar ataques, la prueba es si puede ver lo que los atacantes envían realmente. la mía no ve nada de eso.
lo que aprendí sobre los protocolos
más de lo que esperaba, y nada de ello de documentación.
el estratificado es la parte que por fin encajó. un solo frame no es una cosa, es una pila, y gopacket te entrega cada capa por separado:
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 no es «el paquete». es una capa que puede existir sin ip (algunas encapsulaciones), y por eso el código real comprueba tcp y udp en su propia rama después de las capas de dirección, en lugar de asumir un orden fijo.
«quién tiene» es lo que es un broadcast arp: una máquina preguntando a toda la red «quién tiene esta ip, dime tu mac». es lo primero que ves si empiezas a esnifar en una red con cable, e inunda constantemente porque arp no tiene memoria.
el tráfico multicast y de descubrimiento es casi todo el ruido. 224.0.0.0/24 para ipv4, ff02:: para ipv6, más mdns en 5353, ssdp en 1900, dhcp en 67/68. mis primeros veinte minutos de esnifado fueron casi enteramente esto. el flag -c existe solo para ocultarlo.
una conexión es bidireccional y un paquete no. la herramienta lee tcp.SrcPort y tcp.DstPort en cada frame, así que la misma conversación aparece dos veces con los puertos intercambiados. no hay reensamblado incorporado, y yo no escribo ninguno. es una limitación real y prefiero nombrarla que fingir.
la parte que hice mal dos veces
isLocalIP decide si un paquete es «salida inusual». trata como locales loopback, link-local, multicast y los tres rangos privados.
el bug: todo lo que está dentro de 192.168.0.0/16 cuenta como local, incluidas otras máquinas de mi propia red. así que un dispositivo exfiltrando en silencio hacia el nas de al lado produce cero alertas. mi regla dice «no está saliendo de casa», que no es lo mismo que «no está saliendo de mi máquina».
la corrección honesta es comparar con la dirección real de la interfaz, no con un rango de redes posibles. no lo he corregido. está anotado aquí, y ése es el único motivo por el que existe este párrafo.
la parte de la que más estoy contento
una interfaz concurrente que no da tirones.
el sniffer corre en su propia goroutine y empuja los paquetes al programa tea:
p := tea.NewProgram(m, tea.WithAltScreen())
go startSniffing(iface, watchlist, p)
if _, err := p.Run(); err != nil {
// ...
}
el bucle nunca toca la interfaz directamente, solo envía mensajes. esa es toda la razón por la que la interfaz sigue respondiendo durante un pico de tráfico — una regla que ya conocía del lado del frontend de mi propio sitio, y que solo aprecié de verdad cuando la interfaz era una terminal en lugar del dom.
también es por eso que HighPerformanceRendering está explícitamente en false: es un registro de solo lectura que el usuario desplaza, así que dejar que bubbletea salte fotogramas es más barato que mantenerlos fluidos.
y la cosa pequeña que lo hizo agradable de usar: un mapa de alias de dispositivos, para que 192.168.1.185 se muestre como «equipo principal» y una mac vigilada aparezca en naranja. las herramientas de seguridad tienen fama de ser desagradables, y la mitad de hacer una agradable es dejar ver al usuario su propia casa en palabras humanas.
qué le diría a mi yo del pasado
abre el archivo que escribiste y cuenta las líneas que realmente hacen lo que el nombre promete.
si la respuesta son tres líneas y cuentan algo adyacente a la amenaza, has escrito un contador de conexiones. renómbralo antes de que alguien confíe en él — incluido tú, seis meses después, cuando te preguntes por qué las alertas te parecen raras.