A terminal-based passive network monitor for Linux, written in C99. Sloth never injects packets, never scans, and never modifies kernel state — it observes what your host already sees and turns it into 27 views, six alert rules, an embedded WiFi-SIGINT toolkit (PNL aggregation, RSN/cipher/MFP inventory, EAPOL/PMKID capture, hidden-SSID reveal, seqnum-based MAC-randomisation deanonymisation), and an optional JSONL forensic log.
📖 Per-view deep dives live under docs/views/ — each
file explains the protocol, shows a text mockup, and lists what to watch
for in normal vs anomalous traffic.
v1.1 — WiFi SIGINT — sloth gained six new wireless capabilities on top of the v1.0 monitor: PNL aggregation per client MAC, RSN / cipher / AKM / MFP inventory from beacons, EAPOL / PMKID / 4-way handshake capture with hashcat-22000 export, hidden-SSID reveal, MAC-randomisation sequence-number deanonymisation, and a dashboard alert-hot IP override that paints any IP appearing in a CRIT alert deep-red across every panel for 1 h. See the v1.1 release notes for the full diff.
[1] Interfaces [2] Connections [3] WiFi [4] Packets [5] Processes
[6] Stats [7] Probe [8] ARP [9] mDNS [0] NBNS
[d] DHCP [s] SSDP [b] Beacons [a] Deauth [h] HTTP
[t] TLS [u] QUIC [r] DNS [p] NTP [i] ICMP
[v] Alerts [g] Devices [o] Dashboard [?] Help
[k] PNL [e] EAPOL [j] Seqnum [w] Assoc ← WiFi SIGINT
| View | What it shows |
|---|---|
| Interfaces | Per-interface RX/TX rates, errors/drops, MTU, link speed; sparkline history |
| Connections | Active TCP/UDP sockets with PID, RTT, retransmits, per-conn bandwidth |
| WiFi | Nearby APs from nl80211 scan: signal, channel, encryption |
| Packets | Live pcap capture with BPF filter, hex detail panel, pcap export |
| Processes | Process tree with fold/unfold |
| Stats | Session totals — bytes, packets, rates per interface since reset |
| Probe | 802.11 probe-request sniffer — unassociated clients and the SSIDs they're looking for |
| ARP | Layer-2 neighbour table with OUI vendor lookup |
| mDNS | Bonjour/Zeroconf service table from passive UDP/5353 |
| NBNS | NetBIOS Name Service table from UDP/137 |
| DHCP | Live DHCP event log: DISCOVER/REQUEST/ACK |
| SSDP | UPnP device table from UDP/1900 NOTIFY / M-SEARCH |
| Beacons | Passive 802.11 beacon sniffer — APs visible to a monitor-mode iface, with pairwise cipher / AKM / MFP status from the RSN IE, and hidden-SSID reveal from probe-responses |
| Deauth | 802.11 deauth/disassoc frames; flood detection per target MAC |
| HTTP | Plaintext HTTP requests: method, host, path |
| TLS | TLS ClientHello log: SNI host, version, and JA3 fingerprint |
| QUIC | QUIC Initial packets: version + DNS-resolved host |
| DNS | DNS query/response log: qname, qtype, answer, NXDOMAIN |
| NTP | NTP traffic: mode, stratum, reference ID |
| ICMP | ICMPv4 + ICMPv6 with named types (Echo, Unreachable, Neigh Sol, …) |
| View | What it shows |
|---|---|
| Alerts | Rule-derived events: port scans, deauth floods, NXDOMAIN bursts, threat-intel domain hits, threat-intel IP hits, periodic beaconing |
| Devices | One record per MAC, joined from ARP/DHCP/Beacons/Probe/Stations with OUI vendor |
| Dashboard | Composite at-a-glance view: interfaces, conns + top hosts, packets, and seven side-panel categories all tiled to fill the terminal |
| View | What it shows |
|---|---|
| PNL | Per-MAC Preferred Network List — every directed probe-request's source MAC aggregated with the unique set of SSIDs it has probed for. Randomised MACs are flagged so randomised vs burned-in is one glance. A device's PNL fingerprints its owner. |
| EAPOL | Captured EAPOL-Key frames + 4-way handshake state machine. M1 with a PMKID KDE = one-frame offline-crack vector. M1+M2 together = full handshake. --eapol-dir DIR writes captures in hashcat 22000 format. |
| Seqnum | Sequence-number-based MAC-randomisation deanonymisation. Pairs of MACs whose seqnum trails overlap within 64 seqnums / 30 s are the same physical radio across a MAC rotation. |
| Assoc | Client ↔ AP association inventory. Each row is a (BSSID, STA) pair we've observed confirmation for: EAPOL handshake completed, assoc-response status=0, or reassoc-response status=0. Disassoc / deauth removes the entry. |
sloth -o FILE— append a JSONL line for every DNS/TLS/QUIC/HTTP/NTP/ICMP record and every newly-fired alert. See JSONL schema below.sloth --pcap-dir DIR— when a rule fires with a known flow identifier (THREAT_IP, BEACONING, PORT_SCAN, NXDOMAIN_BURST, THREAT_DOMAIN), the matching packets are written to a per-alert pcap file underDIR.sloth --eapol-dir DIR— append each captured PMKID and 4-way handshake toDIR/eapol.22000in hashcat mixed format. Crack directly withhashcat -m 22000 eapol.22000 wordlist.txt. Sloth also writes a per-handshakeDIR/<bssid>_<sta>.pcap(raw 802.11, DLT 105) so each capture can be replayed throughaircrack-ng -w wordlist.txt -e <SSID> <file>.pcapor opened in Wireshark.
Sloth is fully passive — it never injects probe requests, never sends
deauth frames, never associates with anything. All wireless data is
sniffed by a monitor-mode interface that's been put into monitor mode
by an external tool (iw, airmon-ng, etc.) before sloth starts.
# 1. Set an adapter to monitor mode (external — sloth never touches link state).
sudo ip link set wlan1 down
sudo iw dev wlan1 set type monitor
sudo ip link set wlan1 up
# 2. Run sloth. It auto-discovers the monitor iface; --eapol-dir
# streams captured PMKIDs + 4-way handshakes to hashcat format.
sudo ./sloth --eapol-dir /tmp/sloth-eapol -o /tmp/sloth.jsonl
# 3. While sloth runs:
# [k] PNL — devices and the SSIDs they're probing for
# [b] Beacons — APs + cipher / AKM / MFP inventory + hidden-SSID reveal
# [e] EAPOL — captured handshakes (PMKID = single-frame crack)
# [j] Seqnum — randomised MACs correlated to the same physical radio
# [7] Probe — raw probe-request feed (Roaming clients)
# 4a. Offline crack against the streamed handshake / PMKID file.
hashcat -m 22000 /tmp/sloth-eapol/eapol.22000 rockyou.txt
# 4b. OR replay an individual handshake through aircrack-ng.
aircrack-ng -w rockyou.txt -e "SSID" \
/tmp/sloth-eapol/<bssid>_<sta>.pcapmake # full build (ncurses + pcap + nl80211)
make WITH_PCAP=0 # no capture, no probe view
make WITH_NCURSES=0 # headless / embedded
make embedded # shortcut: no ncurses, no pcap
make test # 1856 unit tests (no root, no terminal, no network)Requires libpcap-dev and libncursesw-dev for the full build. The test build needs neither.
Use [?] inside sloth for an up-to-date reference card.
| Key | View | Key | View | Key | View |
|---|---|---|---|---|---|
1 |
Interfaces | d |
DHCP | u |
QUIC |
2 |
Connections | s |
SSDP | r |
DNS |
3 |
WiFi | b |
Beacons | p |
NTP |
4 |
Packets | a |
Deauth | i |
ICMP |
5 |
Processes | h |
HTTP | v |
Alerts |
6 |
Stats | t |
TLS | g |
Devices |
7 |
Probe | ? |
Help | o |
Dash |
8 |
ARP | k |
PNL | e |
EAPOL |
9 |
mDNS | j |
Seqnum | w |
Assoc |
0 |
NBNS |
| Key | Action |
|---|---|
Tab |
Cycle views forward |
n |
Toggle DNS hostname resolution (in conn/proc/stats views) |
/ |
Filter current log view — type to refine, Enter to commit, Esc to cancel |
\ |
Clear filter |
q/Q |
Quit |
| Key | Action |
|---|---|
↑/↓ |
Navigate rows |
c |
Clear the current log view's ring buffer |
t |
(Interfaces) Toggle iface visibility |
Enter |
(Interfaces/Packets) Open detail panel |
f |
(Conns/Packets) Cycle filter |
s |
(Conns) Cycle sort |
Six rules feed VIEW_ALERTS. New keys also append to the JSONL stream and (if --pcap-dir is set) trigger a per-alert pcap dump.
| Rule | Severity | Trigger | match_ip / port |
|---|---|---|---|
PORT_SCAN |
CRIT | scanner's IP touched ≥ 8 distinct local ports | scanner IP / 0 |
DEAUTH_FLOOD |
WARN | ≥ 5 deauth/disassoc frames in 5 s to one target | — (L2 only) |
NXDOMAIN_BURST |
WARN | ≥ 10 NXDOMAIN replies to one src in 60 s | src / 53 |
THREAT_DOMAIN |
CRIT | DNS qname matches embedded IOC list | src / 53 |
THREAT_IP |
CRIT | conn remote IP matches embedded IOC list | remote IP / port |
BEACONING |
WARN | flow with ≥ 5 samples, mean ≥ 10 s, jitter/mean ≤ 0.25 | remote IP / port |
The IOC lists in src/threat_intel.c are intentionally synthetic (RFC 5737 doc IPs, .testing / .example sentinel domains). They exist so the alerts pipeline can be exercised in tests — replace them with your own feed for production use.
Each line is one JSON object. ts is a Unix timestamp; strings are RFC 8259 escaped.
{"type":"dns","ts":1700000000,"src":"192.168.1.5","qname":"example.com","qtype":"A","answer":"93.184.216.34","is_resp":1}
{"type":"tls","ts":1700000001,"src":"10.0.0.5","dst":"93.184.216.34","host":"example.com","ver":"TLS 1.3","ja3":"deadbeefcafef00d00112233445566ff"}
{"type":"quic","ts":1700000002,"src":"10.0.0.5","dst":"1.1.1.1","host":"cloudflare.com","ver":"v1"}
{"type":"http","ts":1700000003,"src":"10.0.0.5","host":"example.com","method":"GET","path":"/index.html"}
{"type":"ntp","ts":1700000004,"src":"10.0.0.1","dst":"192.168.1.5","mode":"server","version":4,"stratum":1,"ref":"GPS"}
{"type":"icmp","ts":1700000005,"src":"192.168.1.5","dst":"8.8.8.8","desc":"Echo Req","ty":8,"code":0,"seq":42,"v6":0}
{"type":"alert","ts":1700000006,"title":"THREAT_DOMAIN","detail":"192.168.1.5 queried malware.testing.com (IOC malware.testing.com)","key":"threat-d:malware.testing.com","sev":2,"ty":3,"count":1}The codebase is built around a platform vtable (platform_ops_t in include/sloth.h):
typedef struct {
int (*get_ifaces)(iface_stat_t *out, int max);
int (*get_conns)(conn_t *out, int max);
int (*wifi_scan)(wifi_ap_t *out, int max);
int (*get_wifi_stations)(wifi_sta_t *out, int max);
int (*get_arp)(arp_entry_t *out, int max);
int (*get_dhcp)(dhcp_lease_t *out, int max);
void (*init)(void);
void (*cleanup)(void);
} platform_ops_t;Adding a new data source means adding one function pointer here and implementing it in each backend (src/platform/linux.c, bsd.c, stub.c, win32.c) and the test fake (tests/fake_platform.c). Views read from sloth_state_t; they never call platform ops directly.
capture thread main loop (poll_data, 1 Hz)
────────────── ───────────────────────────
pcap_loop() g_platform.get_ifaces()
│ g_platform.get_conns()
▼ g_platform.get_arp()
dns_log_parse / record … other platform reads
tls_log_parse / record │
quic_log_parse / record ▼
http_log_parse / record *_snapshot() ◄── ring buffer copy
ntp_log_parse / record │
icmp_log_parse / record ▼
│ alerts_update()
├─► jsonl_emit_*() beacon_update()
│ (if -o set) devices_update()
└─► ring buffer │
▼
tui_draw()
Packet decode runs in a dedicated pcap thread. The eight log modules each have an independent ring buffer behind a pthread_mutex_t; *_snapshot() helpers copy the latest entries into sloth_state_t once per poll. Synthesis modules (alerts, devices, beacon-detect) read snapshot state and produce derived views.
include/sloth.h shared types: state, packet, conn, alert, device, ...
src/main.c CLI, signal handlers, main loop
src/tui.c ncurses / ANSI rendering, key polling
src/platform/linux*.c Linux backends (rtnetlink, nl80211, /proc, INET_DIAG)
src/capture/capture.c libpcap thread; per-protocol parser dispatch
src/{dns,tls,quic,http,
ntp,icmp,dns,...}_log.c ring buffers + per-record snapshot
src/{alerts,beacon_detect,
devices,threat_intel,
filter,jsonl,alert_pcap}.c synthesis + export
src/views/*.c one file per VIEW_*
src/md5.c RFC 1321 implementation used for JA3
tests/ unit tests, fake platform, scenarios
make test # 1856 assertions, no root, no terminal, no networkEvery real-data path is replaced by a controllable fake:
tests/fake_platform.c— implementsg_platformwith deterministic in-memory data. All vtable functions read from a globalfake_net_t.tests/null_tui.c— stubs ncurses functions as no-ops;TPRINTfalls back toprintf, so view draw functions execute their full rendering logic.tests/scenarios.c— named configurations (empty, idle, busy, many_conns, wifi_crowded, monitor_env) used by render smoke tests.
Protocol parsers (DNS, TLS, JA3, QUIC, HTTP, NTP, ICMP, mDNS, NBNS, DHCP, SSDP) are tested with raw byte arrays constructed by hand from the relevant RFCs. Each test computes byte values from first principles so the tests are not circular.
0x00,0x00, 0x84,0x00, // DNS hdr: ID=0, QR=1 AA=1
0x00,0x00, 0x00,0x01, // QDCOUNT=0, ANCOUNT=1
…
The MD5 implementation used for JA3 is independently validated against all RFC 1321 test vectors (tests/test_md5.c).
Code: 14k+ lines across ~80 files. Tests: 1856 assertions. License: see project root.
Sloth was built as a passive monitor. It will not scan, fuzz, attack, or attempt to deauth or de-associate anything. If that's what you need, use a different tool.