Spectre is a passive network monitoring tool. It watches the traffic on a network segment and builds a live, queryable picture of every device it sees — who they are, what they run, who they talk to — and surfaces anomalies in real time on a force-directed mesh graph.
Passive capture. Spectre never scans, probes, or injects packets onto the network it watches — every device fact is inferred from frames already on the wire, so it is undetectable on the monitored segment.
It does, by default, make outbound lookups to third-party services to enrich external IPs: ip-api.com (geo/ASN, over cleartext HTTP), reverse DNS, optional AbuseIPDB, and threat-feed downloads — plus any webhooks you configure. All are toggleable in
config.toml, and the barebones build (--no-default-features) compiles them out entirely for a truly silent, zero-egress sniffer.
One-liner (Linux — auto-installs Docker if needed, detects your interface, writes a compose file, and starts the stack):
curl -fsSL https://raw.githubusercontent.com/mikemich/Spctr/main/install.sh | sh
# then open http://localhost:3000Or drop in a compose file yourself:
# docker-compose.yml — no build needed, pulls from ghcr.io
services:
daemon:
image: ghcr.io/mikemich/spctr-daemon:latest
cap_add: [NET_RAW, NET_ADMIN]
environment: { SPECTRE_INTERFACE: eth0, SPECTRE_DB_PATH: /data/spectre.db }
volumes: ["./data:/data"]
networks: [spectre]
ui:
image: ghcr.io/mikemich/spctr-ui:latest
ports: ["3000:3000"]
environment: { DAEMON_HOST: daemon }
depends_on: [daemon]
networks: [spectre]
networks: { spectre: { driver: bridge } }SPECTRE_INTERFACE=eth0 docker compose -f docker-compose.ghcr.yml up -d
# open http://localhost:3000It has three parts:
| Component | Stack | Role |
|---|---|---|
spectre-daemon |
Rust, libpcap, axum, SQLite | Captures & parses packets, tracks devices, serves the API/WebSocket |
spectre-ui |
React, Vite, D3, Tailwind | Live mesh graph, device list/detail, alert feed |
docker-compose.yml |
Docker | One-command packaging of the whole system |
For every source IP it sees, Spectre builds a device profile:
- MAC address + OUI vendor (from a bundled, trimmed IEEE OUI database)
- First seen / last seen timestamps and total traffic
- OS fingerprint guessed from the SYN's TTL + TCP window + TCP option order, matched against a bundled signature file (Windows 10/11, Linux 4.x/5.x/6.x, Android, iOS, macOS, FreeBSD, OpenBSD, Cisco, …)
- Open service ports inferred from the TCP handshake direction
- DNS / mDNS hostnames parsed passively from responses
- DHCP hostname (option 12) when a DHCP exchange is seen
- JA4 TLS client fingerprint computed from scratch off the ClientHello (see FoxIO JA4 spec)
- HTTP User-Agent seen on port 80
- External IPs contacted with per-peer byte counts and timestamps
- Internal peers it has communicated with
External addresses are enriched in the background (passively, never blocking capture) and shown in the device table — flag, city/org, PTR hostname, ASN, an auto-tag chip, and an AbuseIPDB score:
- Reverse DNS (PTR) lookups
- Geo + org + ASN via the free ip-api.com batch endpoint (45 req/min, no key)
- Reputation via AbuseIPDB (optional — set
abuseipdb_keyor theABUSEIPDB_KEYenv var) - Auto-tagging from org/PTR/ASN patterns: Googlebot, Cloudflare, Shodan/ Censys/generic Scanner, AWS, Azure, DigitalOcean, Tor Exit Node; RFC1918 / link-local / multicast / Tailscale (100.64/10) are tagged Local
Lookups are queued asynchronously, run with a concurrency cap (10) and a token
bucket, retry-aware on 429, and every result is pushed to the UI live over
the WebSocket. Configure it under [enrichment] in config.toml; trigger a
manual refresh per device with POST /api/enrich {"ip": "x.x.x.x"} (the UI's
row ↻ button).
The rules engine raises an alert when:
- a new device appears,
- a known device's OS fingerprint changes,
- a device contacts a new external IP for the first time,
- an internal device initiates a connection to another internal device for the first time, or
- a device exposes traffic on a service port it has never used before,
- a source scans/sweeps many ports or hosts in a short window,
- the MAC bound to a known IP changes (possible ARP spoofing),
- a device shows periodic beaconing to one external destination, or
- a known attacker campaign reappears from a new IP (behavioral fingerprint match — see below).
High-severity alerts can be pushed to a webhook (Slack / Discord / ntfy /
generic JSON) — see [notifications] in config.toml.
IPs rotate, but tools have habits. Spectre derives a stable fingerprint_id
for each external host from host-stable L3/L4 SYN signals only — TTL
bucket, advertised-window bucket, and TCP option order. Path-dependent signals
(packet timing, hop count) are deliberately excluded, so the same scanner
produces the same fingerprint whether it hits you from one IP or fifty.
- When ≥ 4 distinct IPs share one fingerprint over more than an hour,
it's promoted to a named
Campaign-XXXXand surfaced in the Campaigns tab. A known campaign reappearing from a fresh IP fires a high-severity alert. - Conservative passive tool attribution flags distinctive mass-scanner shapes (Masscan/ZMap-like raw SYNs, option-less IoT-bot packets) without guessing at normal OS stacks.
- The device panel's Behavioral Identity section shows "this IP shares a fingerprint with N others" and links to its campaign.
The per-IP membership behind a fingerprint is local operator data — it's served only on your own daemon API and is never transmitted off the host.
- Auth: set
[auth] enabled = truewith atoken(or theSPECTRE_API_TOKENenv var) to require a bearer token on/apiand/ws. The UI shows a token prompt;/healthstays public for probes. - Retention:
[retention] device_max_age_secsevicts devices (and their connections) not seen for that long, from memory and the database;0keeps everything. The daemon also performs a final DB flush on shutdown.
docker compose up --buildThen open http://localhost:3000.
- The daemon and UI share a private bridge network; nginx reaches the daemon by service name. This works on Docker Desktop (macOS/Windows) and Linux alike — the UI will connect and render.
- The UI runs nginx on
:3000and reverse-proxies/apiand/wsto the daemon's API on:7777. - The SQLite database and logs are written to
./data(mounted volume), so state survives restarts.
Useful environment variables (set in your shell or a .env file):
| Variable | Default | Meaning |
|---|---|---|
SPECTRE_INTERFACE |
eth0 |
Capture interface (any for all) |
SPECTRE_TRUSTED_RANGES |
RFC1918 + link-local | Comma-separated CIDRs treated as internal |
DAEMON_HOST / DAEMON_PORT |
daemon / 7777 |
Where the UI proxies the API |
RUST_LOG |
info |
Log verbosity (debug, trace, …) |
⚠️ What the default Docker setup can and cannot see. In bridge mode the daemon only observes traffic on its own container interface — enough to see the system working end to end, but not your physical LAN. And on Docker Desktop for macOS/Windows, containers run inside a hidden Linux VM, so the daemon can never see your Mac's/PC's real Wi-Fi/Ethernet traffic. That's a Docker virtualization limit, not a Spectre bug. For real monitoring, see "Real capture" below.
Spectre is designed to run on a Linux host that sits on the network you want to watch (a server, a router/firewall, a Raspberry Pi, or a box plugged into a switch SPAN/mirror port). Two ways:
A) Run the daemon natively on the Linux host (simplest, recommended — see
the next section). Capture the real interface (e.g. eth0/wlan0) directly;
optionally still run the UI container pointed at it with
DAEMON_HOST=host.docker.internal.
B) Run the daemon container with host networking on Linux. Edit
docker-compose.yml: under daemon, replace networks: [spectre] with
network_mode: host; under ui, set DAEMON_HOST: host.docker.internal and
add extra_hosts: ["host.docker.internal:host-gateway"]. Then
SPECTRE_INTERFACE=eth0 docker compose up --build. (Host networking only does
the right thing on Linux.)
A VPS is a single server, so Spectre monitors that server's traffic — every client that connects to it, their JA4 TLS fingerprints, byte counts, and what the server talks to. (It is not a LAN full of devices, so expect a server-shaped graph, not a houseful of gadgets.)
There's a ready-made compose file that uses host networking and binds the API to localhost:
# 1. Install Docker + the compose plugin (Debian/Ubuntu)
curl -fsSL https://get.docker.com | sh
# 2. Get the code
git clone https://github.com/mikemich/Spctr.git && cd Spctr
# 3. Find your public interface
ip -o -4 route show to default | awk '{print $5}' # e.g. eth0, ens3, enp1s0
# 4. Launch (replace eth0 with yours)
SPECTRE_INTERFACE=eth0 docker compose -f docker-compose.vps.yml up --build -dThe dashboard is now on port 3000 and the API is bound to 127.0.0.1:7777 (reachable only by the local nginx, never the internet).
Spectre has built-in authentication — enable it before exposing the
dashboard anywhere. In config.toml:
[auth]
enabled = true
username = "admin"
password_hash = "" # generate with the command below
session_ttl_hours = 24
# or, for headless/API use, set a bearer token instead of a password:
# token = "…" (or the SPECTRE_API_TOKEN env var)Generate a bcrypt hash (the daemon ships a subcommand that reads the password from stdin):
echo -n 'your-password' | docker compose -f docker-compose.vps.yml exec -T daemon spectre-daemon hash-password
# paste the output into password_hash, then restartWith auth on, /api and /ws reject anything without a valid signed session
cookie (or bearer token); the UI shows a login page and /health stays public
for probes. Sessions are HMAC-signed with a random per-process secret.
Defence in depth — don't expose the ports publicly either. Even with auth
on, keep :3000/:7777 off the open internet. Pick one:
Option 1 — Firewall to your own IP (quickest):
sudo ufw allow OpenSSH
sudo ufw allow from <YOUR.HOME.IP.ADDR> to any port 3000 proto tcp
sudo ufw deny 7777
sudo ufw enableOption 2 — SSH tunnel (nothing public at all): set the dashboard to listen
on localhost by removing the ports/host-exposure, then from your laptop:
ssh -N -L 3000:127.0.0.1:3000 user@your-vps # open http://localhost:3000Option 3 — Reverse proxy with TLS + Basic Auth (proper public access). Put
Caddy or nginx in front of :3000, e.g. a Caddyfile:
spectre.example.com {
basic_auth { youruser <bcrypt-hash> }
reverse_proxy 127.0.0.1:3000
}
Spectre is passive — it never sends packets — so it won't disrupt your server's traffic. It does record metadata (IPs, fingerprints) to SQLite under
./data; treat that as sensitive.
To update later: git pull && docker compose -f docker-compose.vps.yml up --build -d.
Spectre can build as one self-contained executable that captures and serves the dashboard — the web UI is embedded into the binary. This is the easiest way to run it, and on macOS it captures your real network natively (unlike Docker, which is trapped in a Linux VM).
Download a prebuilt binary from the Releases page (macOS arm64/x86, Linux x86_64), then:
tar -xzf spectre-*.tar.gz && cd spectre-*
# macOS: capture needs root (or BPF access); Linux: root or setcap
sudo ./spectre ./config.toml # then open http://localhost:7777Or build it yourself (needs Node + Rust + libpcap):
cd spectre-ui && npm ci && npm run build && cd ..
cargo build --release -p spectre-daemon --features embed-ui
sudo ./target/release/spectre-daemon ./spectre-daemon/config.tomlThe dashboard, REST API, and WebSocket are all served from :7777 (the API
lives under /api). No nginx, no compose, no proxy.
For a small edge device you can build a silent variant that makes zero outbound connections — enrichment, threat feeds, and notifications are compiled out:
cargo build --release -p spectre-daemon --no-default-featuresA turnkey appliance installer (systemd service, low-priv capture user,
localhost-only API, SD-card-friendly retention) lives in rpi/:
sudo ./rpi/install.shSee rpi/README.md for SPAN-port placement, SSH-tunnel access,
tmpfs/USB storage, and cross-compiling for aarch64.
Watching your own LAN rather than a public server? Set:
# config.toml
mode = "homelan" # or the SPECTRE_MODE=homelan env varhomelan mode re-tunes the dashboard for a household network:
- 🏠 Home (phone-home) view — for every device on your network, the external services it has talked to: "your smart TV talked to 14 Samsung servers today." Click any device or destination to drill in.
- Friendly device-type icons inferred from OUI vendor + open ports — 📶 router, 📱 phone/tablet, 💻 laptop, 📺 smart TV, 🎮 game console, 🖨️ printer, 🔌 IoT — shown on the mesh graph and the device panel.
- DHCP hostnames are preferred as device labels.
It changes presentation only; capture and detection are identical to server
mode. The Raspberry Pi profile in rpi/ ships with
mode = "homelan" already set. On aarch64 (Raspberry Pi 4/5) use the prebuilt
arm64 images from Task 1 or cross-compile per rpi/README.md.
You need libpcap and the capability to capture raw packets.
# Build dependencies (Debian/Ubuntu)
sudo apt-get install -y libpcap-dev
# Build
cargo build --release -p spectre-daemon
# Configure (edit interface, trusted_ranges, etc.)
cp spectre-daemon/config.toml ./config.toml
$EDITOR config.toml
# Option A: run as root
sudo ./target/release/spectre-daemon ./config.toml
# Option B: grant the binary capture capabilities (no root needed to run)
sudo setcap cap_net_raw,cap_net_admin=eip ./target/release/spectre-daemon
./target/release/spectre-daemon ./config.tomlThe daemon reads its config path from (in order): the first CLI argument, the
SPECTRE_CONFIG environment variable, then ./config.toml. Any field can be
overridden with SPECTRE_* environment variables (see the table above).
cd spectre-ui
npm install
npm run dev # http://localhost:3000, proxies to http://localhost:7777Point it at a non-default daemon with SPECTRE_DAEMON=http://host:7777 npm run dev.
Spctr nodes can share behavioral fingerprints with a central hub to detect internet-wide scanner campaigns that no single node would see alone.
What gets shared: fingerprint hashes, IP counts, ASN, country, tool guess. What never leaves your node: IP addresses, hostnames, traffic content, TLS domains. The push type has no field that can hold an IP — the guarantee is enforced at compile time. Federation is off by default and, when enabled, never affects local operation if the hub is unreachable.
# Run your own hub (open source, self-hostable):
HUB_ADMIN_TOKEN=<secret> docker compose --profile hub up -d
# If running the hub, open port 7779:
sudo ufw allow 7779/tcp comment "Spctr hub"
# Register a sensor (prints sensor_id + one-time token for config.toml):
spectre-daemon register-sensor --hub http://162.19.253.164:7779 --region <hint>
# 162.19.253.164:7779 — temporary public hub until hub.spctr.io is liveFor constrained nodes, the standalone spctr-sensor binary (no UI/DB) does
the same with a smaller footprint — it computes byte-identical fingerprints to
the full daemon via shared crates:
curl -fsSL https://raw.githubusercontent.com/mikemich/Spctr/main/install-sensor.sh | bash
# defaults to the temporary public hub http://162.19.253.164:7779Full data contract: spctr-hub/PROTOCOL.md ·
Deploy guide: docs/deploy-sensor.md
Campaign cards show a 🌐 N sensors badge when a fingerprint is confirmed
across the network.
- Open Settings → Federation
- Enter the hub URL and click Register with hub
- Toggle federation on
Enable/disable happens live — no docker compose restart needed — and the
choice is written through to config.toml so it survives restarts.
Or from the CLI:
spectre-daemon register-sensor --hub http://162.19.253.164:7779
# Add the printed sensor_id + token to config.toml under [federation],
# set enabled = true, and restart.The daemon exposes (also mirrored under /api/... for the proxy):
| Endpoint | Description |
|---|---|
GET /devices |
All known devices as JSON |
GET /devices/:ip |
Full profile for one device |
GET /devices/search?q= |
Fuzzy search across IP/host/org/ASN/tag/country |
GET /devices/:ip/traffic?hours=N |
Per-device traffic series |
GET /devices/:ip/fingerprint |
Behavioral fingerprint + campaign for a device |
GET /reputation/:ip |
Reputation score 0–100 + breakdown (Task 18) |
GET /export/blocklist?format=ufw|iptables|nginx|cidr |
Firewall blocklist of high-reputation IPs (Task 22) |
GET /response/policy |
Blocklist export formats + auto-block threshold |
PUT /devices/:ip/note |
Set a device note |
GET /traffic/global?hours=N |
Global bandwidth series |
GET /graph |
{ nodes, edges } for the mesh graph |
GET /alerts?limit=N |
Recent anomaly events (?device=IP to filter) |
GET /export?format=csv|json|ndjson |
Export all devices |
GET /rules, PUT /rules/:name/toggle |
Alert rules |
POST /enrich |
Queue an IP for (re-)enrichment |
POST /auth/login, /auth/logout, GET /auth/me |
Session auth |
GET /honeypot/events?limit=N&ip=X |
Honeypot interactions |
GET /honeypot/stats |
Honeypot summary stats |
GET /knocks?limit=N&since=ts |
Inbound knocks to closed ports on this host (Task 17) |
GET /dns/anomalies?limit=N |
Suspicious DNS queries — exfil/tunnelling (Task 20) |
GET /score |
Spctr Score — posture self-assessment + issues + history (Task 21) |
GET /score/badge |
SVG score badge for the README |
GET /fingerprints |
Behavioral fingerprints (summaries) |
GET /fingerprints/:id |
One fingerprint with its member IPs (local data) |
GET /fingerprints/:id/ips |
Member IPs of a fingerprint (local data) |
GET /fingerprints/:id/signals |
Aggregate + per-member raw signals (audit/debug, local data) |
GET /campaigns |
Fingerprints promoted to named campaigns |
GET /hub/campaigns |
Cross-sensor campaigns from the federation hub (Task 27) |
GET /federation/status |
Federation state (never returns the sensor token) |
POST /federation/register |
Register this node with a hub; saves + persists credentials |
POST /federation/toggle |
Enable/disable federation at runtime (no restart) |
POST /federation/save |
Update hub URL / region hint (persisted) |
GET /operators |
Inferred operator profiles (local-only, Task 26) |
GET /operators/:id |
One operator + inferred behaviour / timezone |
GET /devices/:ip/operator |
The operator a device's fingerprint belongs to |
GET /stats |
Network-wide counters (operators, observed-domain IPs) |
GET /health |
Liveness probe |
WS /ws |
Live events: snapshot, device_new, device_updated, connection_new, alert, honeypot |
OS fingerprints are matched against spectre-daemon/data/signatures.toml,
which is embedded into the binary at compile time. To add a signature:
-
Capture a SYN from the target OS (client SYN, no ACK):
sudo tcpdump -ni <iface> -v \ 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'
-
Read off three things from the SYN:
- the TTL (it is rounded up to 64 / 128 / 255 at match time),
- the TCP window size, and
- the TCP option order.
-
Append a block to
signatures.toml. Option mnemonics areMSS,NOP,WS,SACK,TS,EOL, joined in order:[[signature]] os = "My OS 1.0" ttl = 64 window = 64240 # use 0 as a wildcard window options = "MSS,SACK,TS,NOP,WS" device_type = "desktop" # router | mobile | desktop | iot | unknown
-
Rebuild the daemon (
cargo build --release -p spectre-daemon). The matcher scores an exact TTL + option-order match at higher confidence when the window also matches, and falls back to a coarse TTL-family guess when no signature matches.
The trimmed OUI vendor database lives next door in data/oui.txt
(<6 hex prefix><TAB><vendor> per line) and is embedded the same way.
spectre/
├── spectre-daemon/ # Rust capture daemon
│ ├── src/
│ │ ├── main.rs # wiring: capture → process → persist → serve
│ │ ├── capture.rs # libpcap loop
│ │ ├── parse.rs # Ethernet/IP/TCP/UDP/ARP parsing (panic-free)
│ │ ├── fingerprint.rs # OS signatures, OUI lookup, JA4
│ │ ├── appl.rs # DNS/mDNS, DHCP, HTTP User-Agent extraction
│ │ ├── device.rs # Device/connection state + anomaly detection
│ │ ├── db.rs # SQLite (batched writes)
│ │ ├── api.rs # axum routes + WebSocket
│ │ ├── alerts.rs # alert vocabulary & severities
│ │ └── config.rs # TOML config + env overrides
│ └── data/
│ ├── oui.txt
│ └── signatures.toml
├── spectre-ui/ # React + Vite + D3 + Tailwind
├── docker-compose.yml
└── README.md
Point Spectre at a public-facing box and the internet introduces itself within minutes. Everything below is unsolicited traffic, auto-tagged for you:
| Tag | Who they are |
|---|---|
| 🔵 Googlebot | Google's crawler (verified via reverse DNS on googlebot.com). |
| 🟣 Censys / Shodan | Internet-wide scanners that index every reachable service. Their fingerprints will hit your SSH, HTTP, and odd ports within the hour. |
| 🟣 ONYPHE / BinaryEdge / Stretchoid | More mass-scan operators cataloguing the internet (tagged Scanner). |
| 🟠 Cloudflare / AWS / Azure / DigitalOcean | Cloud/CDN egress — bots, health checks, and whatever's hosted there reaching out. |
| 🟪 Tor Exit Node | Traffic arriving from the Tor network. |
| 🔴 Blocklisted | IPs on Firehol L1 / Emerging-Threats compromised lists — known-bad hosts probing you right now. |
The fun part: watch the mesh graph fill in real time as these get classified,
and set a rule (tag contains 'Scanner' and connections > 5) to get a
webhook ping the moment someone starts enumerating your ports.
- Passive only. Spectre never sends a packet — it cannot be detected on the wire and cannot disrupt the network it watches. No active scanning anywhere.
- Your data never leaves your server. Capture, enrichment results, and
history are stored locally in SQLite under
./data. The only outbound traffic is optional enrichment lookups (ip-api / AbuseIPDB / threat feeds) and webhooks you configure — all toggleable inconfig.toml. Federation is off by default. When enabled, only behavioral fingerprint hashes and coarse metadata (ASN, country) are shared with the hub — never IP addresses or traffic content. Full data contract: PROTOCOL.md - Auth built in. Enable
[auth](bcrypt + signed session cookies) before exposing the dashboard publicly; bind the API to localhost behind the UI's reverse proxy. See "Deploying on a VPS". - Sensitive by nature. The database records IPs, fingerprints, and contacted
domains — treat
./dataaccordingly.
| Spectre | ntopng | Zeek | darkstat | |
|---|---|---|---|---|
| Focus | Who's on/probing your network + threat intel | Traffic analytics | Protocol/IDS scripting | Bandwidth accounting |
| Passive (no scanning) | ✅ | ✅ | ✅ | ✅ |
| Live mesh graph | ✅ | partial | ❌ | ❌ |
| OS + JA4 fingerprinting | ✅ | partial | via scripts | ❌ |
| Attacker behavioral fingerprinting (survives IP rotation) | ✅ | ❌ | ❌ | ❌ |
| Operator profiling (multi-tool campaign inference, local-only) | ✅ | ❌ | ❌ | ❌ |
| Privacy-first federation (hashes only, never raw IPs) | ✅ | ❌ | ❌ | ❌ |
| Auto-tagging (Googlebot/Shodan/…) | ✅ | ❌ | ❌ | ❌ |
| Threat-feed correlation | ✅ built-in | ❌ | via scripts | ❌ |
| Single binary / one-line install | ✅ | ❌ | ❌ | ✅ |
| Footprint | tiny (Rust) | heavy | heavy | tiny |
Spectre isn't an IDS like Zeek or a full analyzer like ntopng — it's a zero-config, screenshot-worthy who-and-what view of a network with built-in threat context.
See ROADMAP.md for the full, tracked plan. Highlights:
- Passive capture, device tracking, OS fingerprinting, JA4
- IP enrichment (reverse DNS, geo/ASN, AbuseIPDB) + auto-tagging
- User-configurable alert rules engine (hot-reloaded)
- Traffic timeline + per-device sparklines
- Authentication (bcrypt + session cookies)
- Slide-over device detail panel with notes
- Mesh graph: tag colors, clustering, filters, PNG export
- Threat-feed correlation (Firehol / ET / Tor)
- Global search (Cmd+K) + CSV/JSON export
- Multi-arch Docker images on GHCR
- Optional honeypot mode (fake services, logs every interaction)
- Attacker behavioral fingerprinting + campaign detection (auditable signals)
- TLS SNI domains tagged by direction (inbound / outbound / observed)
- Operator profiling — local-only inference linking multi-tool campaigns
- Federated sensor network (privacy-first, hashes only — no raw IPs):
spctr-hub+ optional sensor push/pull + standalonespctr-sensor - Prebuilt
spctr-sensormusl binaries (amd64 + arm64) on every release - Telegram / Discord native notifiers
- JA4S / JA4H / JA4T fingerprints
- Prometheus
/metricsendpoint - PCAP import / replay mode
- Never crashes on bad packets. Every parser is bounds-checked and returns
Nonerather than panicking; the capture loop drops undecodable frames. - Batched persistence. The hot path only mutates in-memory state and marks rows dirty; a timer flushes to SQLite every 2 seconds inside one transaction.
- Resilient UI. The frontend keeps showing the last known data when the daemon goes offline, shows a "disconnected" banner, and reconnects the WebSocket automatically with backoff.
- JA4 from scratch.
fingerprint.rsparses the TLS record + ClientHello, strips GREASE, sorts cipher suites and extensions, and hashes per the FoxIO spec to produce theja4string (e.g.t13d1516h2_<12hex>_<12hex>).
MIT