A pre-host XDP packet inspection daemon for Linux. An eBPF program attaches to a network interface at the earliest possible kernel hook, validates every inbound packet, and exports rich metadata through a BPF ring buffer to a Rust userspace daemon that reconstructs flows, runs signature and behavioural anomaly detection, enforces firewall rules via nftables, and streams live telemetry to a web dashboard.
NIC → XDP eBPF (kernel) → ring buffer → collector → flow tracker
→ signature engine → response engine → nftables
→ anomaly engine ↗
→ telemetry logger
→ dashboard (HTTP + WebSocket)
Kernel-level inspection (XDP)
- Validates IP headers, TCP/UDP lengths, and checksum before the packet reaches the kernel network stack
- Drops land attacks, bogon/martian sources, tiny fragments, impossible TCP flag combinations, and NULL/XMAS scans at wire speed
- Exports a 104-byte
PacketMetarecord per packet to userspace via BPF ring buffer
Signature detection
- JSON rule file hot-reloadable without daemon restart (
sentinel rules reload) - Match on: protocol, port, src/dst CIDR, payload substring, packet size range, IP reputation, domain patterns, TCP flags, port category
- Per-rule verdict (
suspicious/malicious) and severity (low→critical) - Ships with 30 rules covering port scans, C2 beaconing, web shells, SQL injection, crypto miners, RDP/SMB attacks, and more
Behavioural anomaly detection
- Scored heuristic model: per-IP float score 0–100 with exponential decay (60 s half-life)
- 9 heuristics: XMAS scan, NULL scan, FIN scan, SYN flood, high packet rate, port scan, excessive DNS, large transfer, beaconing
- Beaconing detection via inter-arrival time coefficient of variation (CV < 0.15 over ≥ 10 samples)
- Alert fires only on upward band crossing (0–30 normal → 31–60 suspicious → 61–100 malicious) to suppress repeated noise
Automated response (nftables)
- Five configurable enforcement tiers:
log-only,alert,block-flow,temp-ban,perm-ban - Creates and owns an
inet sentinel_gatenftables table with three sets:banned_ips(timeout),permanent_bans,blocked_flows(src_ip × dst_port) - Separate policies for suspicious vs. malicious alerts
--response-dry-runfor safe testing without firewall changes
Flow tracking
- Reconstructs TCP/UDP/ICMP flows from individual packet events
- Configurable TTL (default 60 s) and max concurrent flows (default 65 536)
- Emits flow summaries at TCP FIN/RST, TTL expiry, or shutdown
Structured telemetry
- Human-readable coloured output or machine-readable JSONL
- File logging with size-based rotation (configurable MB limit, keeps last 3 files)
- Minimum severity filter:
info,low,medium,high,critical - Canonical event fields:
timestamp,src_ip,dst_ip,flow_id,detection_type,severity,reason
Live web dashboard
- Single-page dark-theme UI served from the daemon — no separate web server needed
- Live Traffic Map: animated Canvas showing source IPs orbiting a centre node, particles colour-coded by verdict
- Recent Alerts: scrolling table with severity colour coding
- Top Talkers: live horizontal bar chart by packet count
- Blocked IPs: table with live countdown timers
- Pushes updates every second via WebSocket
CLI control (sentinel)
sentinel start— exec-replace intosentinel-gatewith all flags forwardedsentinel stop— graceful shutdown via Unix domain socketsentinel status— live stats: packets, alerts, anomalies, responses, uptimesentinel rules reload— hot-swapsignatures.jsonwithout restartsentinel interface attach— attach to a new interface (requires restart; returns clear error)
| Requirement | Minimum |
|---|---|
| Linux kernel | 5.8 (BPF ring buffer) |
| Rust toolchain | 1.75 |
| clang | 14+ |
| libbpf-dev | any recent |
| nftables | optional — response engine degrades gracefully |
| CAP_NET_ADMIN | required to attach XDP |
Ubuntu / Debian:
sudo apt install clang llvm libbpf-dev nftables# Compile everything (eBPF C program + Rust binaries)
make
# Or separately
make bpf # produces ebpf/xdp_filter.o
cargo build --releaseBoth binaries land in target/release/:
sentinel-gate— the daemon (7.8 MB)sentinel— the CLI (1.9 MB)
# Basic — SKB mode, all defaults
sudo sentinel-gate --interface eth0
# Native driver mode (faster, requires driver support)
sudo sentinel-gate --interface eth0 --mode native
# JSON telemetry to file, malicious traffic temporarily banned
sudo sentinel-gate \
--interface eth0 \
--log-format json \
--log-file /var/log/sentinel-gate.jsonl \
--response-malicious temp-ban \
--ban-duration 600
# Pre-populate kernel-level IP blocklist
sudo sentinel-gate \
--interface eth0 \
--block-ip 198.51.100.1 \
--block-ip 203.0.113.0/24
# Safe testing — dry-run response engine, debug logging
sudo sentinel-gate \
--interface eth0 \
--log-level debug \
--response-dry-run
# Dashboard on a custom address/port
sudo sentinel-gate \
--interface eth0 \
--dashboard-bind 0.0.0.0 \
--dashboard-port 8080
# Disable dashboard
sudo sentinel-gate --interface eth0 --dashboard-port 0 -i, --interface <IFACE> Network interface [default: eth0]
-b, --bpf-object <PATH> eBPF object file [default: ebpf/xdp_filter.o]
-s, --signatures <PATH> Signature rules [default: rules/signatures.json]
-m, --mode <MODE> XDP mode: skb | native | hw [default: skb]
-l, --log-level <LEVEL> Trace level: trace|debug|info|warn|error [default: info]
--log-format <FMT> human | json [default: human]
--log-file <PATH> Write JSONL telemetry to file
--log-rotate-mb <MB> Rotate log at N MB [default: 100]
--log-min-severity <SEV> info|low|medium|high|critical [default: info]
--block-ip <IP> Pre-block source IP (repeatable)
--poll-sleep-us <US> Ring buffer poll interval µs [default: 500]
--channel-capacity <N> Collector→processor channel size [default: 4096]
--stats-every <N> Log stats every N packets [default: 100000]
--flow-ttl <SECS> Flow idle expiry [default: 60]
--max-flows <N> Max concurrent flows [default: 65536]
--reap-interval <SECS> Reaper tick interval [default: 10]
--response-suspicious <ACT> log-only|alert|block-flow|temp-ban|perm-ban [default: log-only]
--response-malicious <ACT> log-only|alert|block-flow|temp-ban|perm-ban [default: temp-ban]
--ban-duration <SECS> Temp ban duration [default: 600]
--nft-table <NAME> nftables table name [default: sentinel_gate]
--response-dry-run Log responses without executing nft
--dashboard-port <PORT> Dashboard port (0 = off) [default: 9090]
--dashboard-bind <ADDR> Dashboard bind address [default: 127.0.0.1]
Open http://127.0.0.1:9090 while the daemon is running.
![Dashboard layout: 2×2 grid — Traffic Map, Alerts, Top Talkers, Blocked IPs]
The dashboard uses a persistent WebSocket connection. The status pill in the top-right shows LIVE when connected and OFFLINE when the daemon is not reachable.
REST endpoints for scripting:
| Endpoint | Returns |
|---|---|
GET /api/status |
Uptime, interface, packet/alert counts |
GET /api/alerts |
Last 200 signature + anomaly alerts |
GET /api/talkers |
Top 20 IPs by packet count |
GET /api/blocked |
Active temporary bans with expiry |
# All CLI commands (no sudo needed if socket is group-accessible)
sudo sentinel status
sudo sentinel stop
sudo sentinel rules reload
sudo sentinel interface attach --interface eth1 --mode native
# Daemon start via CLI (exec-replaces into sentinel-gate)
sudo sentinel start --interface eth0 --mode skb --response-dry-runRules live in rules/signatures.json. The engine hot-reloads on sentinel rules reload — no restart required.
{
"rules": [
{
"id": "SIG-001",
"name": "Null Scan",
"severity": "high",
"verdict": "suspicious",
"tags": ["scan", "stealth", "tcp"],
"match": {
"protocol": "tcp",
"tcp_flags": { "all_zero": true }
}
},
{
"id": "SIG-016",
"name": "Known C2 Beacon Pattern",
"severity": "critical",
"verdict": "malicious",
"tags": ["c2", "beacon", "http"],
"match": {
"protocol": "tcp",
"dst_port": [80, 443, 8080, 8443],
"payload_contains": "POST /gate.php"
}
}
],
"ip_reputation": {
"malicious": ["198.51.100.0/24"],
"scanner": ["203.0.113.1"]
}
}Match fields: protocol, src_port, dst_port, src_cidr, dst_cidr, tcp_flags, payload_contains, port_category, ip_reputation, domain_contains, packet_size_min, packet_size_max
sentinel-gate/
├── ebpf/
│ └── xdp_filter.c # XDP eBPF program (C, compiles to BPF ELF)
├── rules/
│ └── signatures.json # Detection rules
├── src/
│ ├── main.rs # Entry point, CLI args, pipeline wiring
│ ├── collector.rs # Ring buffer reader + processor/reaper tasks
│ ├── packet_parser.rs # PacketMeta ABI struct + decode helpers
│ ├── flow_tracker.rs # Per-flow state machine (DashMap)
│ ├── signature_engine.rs # JSON rule loader + match engine
│ ├── anomaly_engine.rs # Scored heuristic model + beaconing detection
│ ├── response_engine.rs # nftables enforcement + ban deduplication
│ ├── logger.rs # Structured telemetry (human/JSON, file rotation)
│ ├── control.rs # Unix socket IPC server + SharedStats
│ ├── dashboard.rs # Axum HTTP + WebSocket dashboard backend
│ └── bin/
│ └── sentinel.rs # CLI binary
├── static/
│ └── dashboard.html # Single-file dashboard UI (embedded at build time)
├── build.rs # Compiles eBPF C program via clang
├── Makefile
└── Cargo.toml
main task (current_thread runtime)
│
├─ Collector::run() polls ring buffer, sends PacketMeta via mpsc
│ └──────────────────────────────────────────────────────────────────┐
│ ↓
├─ processor_loop (spawned) flow tracking → signature → anomaly → response
│
├─ reaper_loop (spawned) TTL-expired flow eviction on a tick
│
├─ ControlServer (spawned) Unix socket IPC — stop / status / reload
│
└─ DashboardServer (spawned) Axum HTTP + WebSocket live feed
RingBuf<&mut MapData> is not Send, so the collector runs inline in the main task. All other pipeline stages hold only Arc<T> values and are freely spawnable.
The eBPF program performs these checks in order before the packet reaches the kernel network stack:
| Code | Check | Action |
|---|---|---|
| H1 | Ethernet frame too short | XDP_DROP (no ring buffer entry) |
| H2 | IPv4 IHL < 5 | XDP_DROP |
| H3 | IP total length < IHL×4 | XDP_DROP |
| S1 | Kernel IP blocklist hit | XDP_DROP + ring buffer |
| S2 | Bogon / martian source | XDP_DROP + ring buffer |
| S3 | Land attack (src == dst) | XDP_DROP + ring buffer |
| S4 | Tiny first fragment | XDP_DROP + ring buffer |
| S5 | IP checksum mismatch | XDP_PASS (suspicious) |
| S6 | Non-first IP fragment | XDP_PASS (suspicious) |
| S7 | IP total length > wire length | XDP_PASS (suspicious) |
| S8 | TTL ≤ 1 | XDP_PASS (suspicious) |
| S9 | TCP data offset invalid | XDP_DROP + ring buffer |
| S10 | Impossible TCP flag combos | XDP_DROP + ring buffer |
| S11 | NULL / XMAS scan | XDP_PASS (suspicious) |
| S12 | UDP length < 8 | XDP_DROP + ring buffer |
| S13 | UDP length > IP payload | XDP_PASS (suspicious) |
- WSL2: SKB mode (
--mode skb) works. Native/HW modes require a driver with XDP support. - Permissions:
sentinel-gaterequiresCAP_NET_ADMIN. The control socket at/run/sentinel-gate/control.sockis created as root; prefixsentinelsubcommands withsudoor adjust socket permissions. - nftables: If nftables setup fails (missing binary, insufficient privileges), the response engine logs a warning and falls back to
log-onlyfor all firewall actions. Use--response-dry-runto test without touching firewall state.
GPL-2.0