NetSentinel — Real-Time Network IDS
A Python-based network intrusion detection system that catches ARP spoofing, port scans, DNS hijacking, and ICMP floods as they happen — not after the fact
NetSentinel — Real-Time Network IDS
Because I got tired of not knowing what was happening on my homelab network until it was too late.
The Goal
I wanted a tool that would watch live network traffic and fire alerts the moment something suspicious showed up — not aggregate logs for review later, not rely on a cloud service, and not require a $40k SIEM license. Just a Python process with root and a network interface, doing real detection against real attack patterns I actually care about.
The four threats I focused on are ones that show up constantly in lab environments and real networks alike: ARP spoofing (the foundation of most LAN MITM attacks), port scans (recon), ICMP floods (DoS), and DNS anomalies — both outright hijacking and the subtler signal of high-entropy subdomains that suggest DNS tunneling for C2 or exfil.
How It Works
NetSentinel runs Scapy in packet-sniffing mode, feeding live frames through a set of independent monitor modules. Each monitor maintains its own state — an ARP table, a sliding window of SYN timestamps, a DNS resolution cache — and calls a shared alerter when a threshold or anomaly condition is met. Alerts go to stdout and to a newline-delimited JSON log file for later analysis.
Detection Methods
| Threat | Method | Severity |
|---|---|---|
| ARP Spoofing | Tracks IP→MAC mappings; alerts the moment a reply claims a different MAC for a known IP | CRITICAL |
| Port Scan | Counts SYN packets per source in a sliding time window; alerts when N ports are hit in M seconds | HIGH |
| ICMP Flood | Counts echo requests per source in a sliding window; alerts past threshold | HIGH |
| DNS Hijacking | Caches domain→IP mappings from observed DNS responses; alerts when a domain suddenly resolves differently | HIGH |
| DNS Tunneling | Calculates Shannon entropy of subdomain labels; high entropy flags likely C2 or exfil traffic | MEDIUM |
The ARP monitor only acts on ARP replies (op=2), which cuts down noise considerably — requests are normal, replies with mismatched MACs are the actual attack signal. The DNS tunneling detection uses Shannon entropy on subdomain labels rather than on the full domain, which avoids false-positives from CDN hostnames with long random identifiers in the TLD portion.
MITRE ATT&CK Mapping
Every alert maps to a specific technique, which was a deliberate design choice. I wanted the log to be useful for writing detections in other contexts, not just for reading.
| Detection | Tactic | Technique |
|---|---|---|
| ARP Spoofing | Credential Access / Collection | T1557.002 — ARP Cache Poisoning |
| Port Scan | Discovery | T1046 — Network Service Discovery |
| ICMP Flood | Impact | T1499.003 — Application Exhaustion Flood |
| DNS Hijacking | Command and Control | T1071.004 — DNS |
| DNS Tunneling | Exfiltration / C2 | T1041, T1071.004 |
Configuration
Thresholds are tunable in config.yaml without touching code — scan window size, ICMP count ceiling, entropy threshold for tunneling, and a whitelist for IPs that should never trigger alerts (like your own router). The entropy threshold in particular needed tuning: too low and you get noise from legitimate CDN queries, too high and real C2 beaconing slips past.
Log Format
Every event writes a structured JSON record:
{
"timestamp": "2024-06-14T14:32:01.123456",
"severity": "CRITICAL",
"event_type": "arp_spoofing",
"source_ip": "192.168.1.55",
"destination": "LAN",
"detail": "ARP table poisoning: 192.168.1.1 was aa:bb:cc:dd:ee:ff, now claiming 11:22:33:44:55:66"
}A --summary flag parses the log and prints a breakdown by severity, event type, and top source IPs — no root required for that part.
Tech & Tools
- Python 3.8+ — main runtime
- Scapy — packet capture and layer parsing (ARP, IP, DNS, ICMP, TCP)
- PyYAML — configuration loading
- Shannon entropy — implemented directly in
dns_monitor.py, no external dependency - Modular monitor architecture — each detector is an independent class with a
process(pkt)method
What I Learned
The ARP table state problem is interesting. The monitor only has visibility into what it sees on the wire — it builds its IP→MAC table from scratch each run. That means if NetSentinel starts after a spoofing attack is already underway, it won't catch the transition. It's a fundamental limitation of passive detection without a trusted baseline, and it's the same problem enterprise NDR tools deal with. Solving it properly would require either persisting the table between runs or importing an authoritative baseline from DHCP leases.
DNS tunneling detection via entropy works, but the threshold matters a lot. A value of 3.5 catches the obvious cases without drowning in false positives, but it's not a substitute for volume analysis or behavioral baselining. A real C2 channel that uses short, low-entropy subdomains would slide past entirely.
Scapy's layer traversal for DNS answers took more care than expected. DNS responses can chain multiple resource records in a linked structure, and iterating them correctly without crashing on malformed packets required explicit exception handling around the payload traversal loop.
Writing structured JSON events from the start pays off immediately. The --summary mode took almost no effort to add because the log was already machine-readable. That's not a novel observation, but it was a good reminder that "we'll parse it later" is a lot easier when you didn't have to think about parsing.
What's Next
- Slack or Discord webhook for real-time alerting
- PCAP capture triggered on alert (so you get the evidence, not just the notification)
- Passive OS fingerprinting from observed traffic
- A lightweight Flask dashboard for live event viewing
systemdservice file for always-on monitoring without a persistent terminal session
