A Rust library and CLI tool for network device discovery and tracking via DHCP, mDNS, SSDP/UPnP, LLMNR, NetBIOS (NBNS), WS-Discovery (WSD), CoAP, Matter, KNXnet/IP, MQTT, Plex GDM, IP Camera discovery (SADP, Dahua, RTSP), link-layer frames (ARP, NDP, LLDP, CDP), and IEEE OUI vendor identification.
Release history: CHANGELOG.md.
- DHCPv4 Support: Capture and parse DHCP DISCOVER, OFFER, REQUEST, ACK, NAK, RELEASE, and INFORM messages
- DHCPv6 Support: Capture and parse SOLICIT, ADVERTISE, REQUEST, CONFIRM, RENEW, REBIND, REPLY, RELEASE, DECLINE, RECONFIGURE, and INFO-REQUEST messages
- mDNS Support (optional): Passive and active mDNS discovery for enhanced device identification
- SSDP/UPnP Support (optional): Passive and active SSDP discovery for UPnP and media devices
- ARP Sniffing: Passive Layer 2 sniffing of ARP frames (Request & Reply) to dynamically discover silent network devices
- DHCP Option 55, 60, & 43 OS/Vendor Fingerprinting: Identifies device operating systems and brands using PRL (Parameter Request List), Vendor Class Identifiers, and Option 43 Vendor-Specific Information (e.g., Windows, macOS/iOS, Android/Linux, HP printers, Sonos, Ubiquiti/UniFi, Cisco, Yealink, Polycom, etc.)
- LLMNR Sniffing (optional): Passive sniffing of Link-Local Multicast Name Resolution (LLMNR) traffic to resolve and update local device hostnames
- NetBIOS Name Service (NBNS) Sniffing (optional): Passive sniffing of NetBIOS traffic on UDP port 137 to resolve hostnames and infer device roles (e.g., file server). Gated under
mdnsfeature. - WS-Discovery (WSD) Sniffing (optional): Passive sniffing of SOAP XML multicast probe traffic on UDP port 3702 to discover network hardware, PCs, and IP Cameras. Gated under
ssdpfeature. - mDNS TXT Record Parsing: Extracts model, md, and ty metadata from DNS-SD records to identify specific hardware devices (Apple TV, Chromecast, Sonos speakers, and printer models)
- Device Classification: Expanded classification engine mapping hostnames, mDNS TXT metadata, and service fingerprints to specific device types and vendors (Roku, Sonos, Apple TV, Google Chromecast, ESP32 IoT, Raspberry Pi, Synology NAS, Playstation, Xbox, Nintendo, smart plugs, printers, etc.)
- Manual Classification Overrides: Pin a device's type (and optionally vendor) by MAC address via
--overrideor an overrides file, for devices with no reliable passive signature. Overrides always win over heuristics and persist across live updates. - Device Attribute Fingerprinting: Identifies a device by the set of attributes it advertises rather than the name it sends, so a device with a randomised MAC and no hostname is still recognisable. Attribute keys are collected from mDNS, SSDP and DHCP, plus the bare product names out of the SSDP
SERVER/USER-AGENTheaders. Versions, serial numbers and user-chosen names are discarded, so none of them enters a fingerprint. Matched against a shareable catalogue (lanwatch-fingerprints.txt). See Device Attribute Fingerprinting. - Agent Fingerprinting: Reads the product names a device announces in its SSDP
SERVERandUSER-AGENTheaders, so a responder's firmware stack (openwrt,miniupnpd,portable-sdk-for-upnp-devices) and a searching host's operating system or application become attributes in their own right. A multi-word product name stays one token, and versions and(...)comments are dropped. See Device Attribute Fingerprinting. - Repeating Active Discovery:
--query-intervalre-sends the mDNS and SSDP probes on a timer, so a device that was asleep or off the LAN at startup is still found. See Repeating the Active Probes. - Dashboard Device Review: Correct a device's type and vendor and give it a product name directly in the web dashboard. Corrections are stored in the database, survive a restart, and always beat heuristic classification. One click then exports every reviewed device as a catalogue file that carries no MAC, IP or hostname.
- Smart Home & IoT Discovery: Extracts model and vendor metadata from HomeKit (HAP) and Matter service advertisements (Google, Apple, Amazon, Eve, Signify/Philips Hue, Aqara, IKEA, Nanoleaf, Tuya, Somfy, TP-Link, Lutron, Yale, Belkin, Bosch, etc.)
- Specialized IoT & Constrained Protocols: Captures and parses CoAP (Constrained Application Protocol) on UDP port 5683, KNXnet/IP building automation traffic on UDP port 3671, and MQTT/MQTT-SN on TCP/UDP port 1883 to dynamically identify sensors, smart lights, smart plugs, and home automation systems
- Media Server & Player Discovery: Parses Plex GDM (Good Day Mate) XML/HTTP-like discovery broadcasts on UDP ports 32410, 32412, and 32414 to locate and identify Plex Media Servers and Players
- IP Camera & CCTV Discovery (Physical Security): Identifies physical security hardware by parsing Hikvision SADP (port 9999) XML discovery messages, Dahua discovery (port 37810) JSON payloads, and RTSP (TCP port 554) server responses
- IEEE OUI Database: Built-in vendor identification from MAC addresses using IEEE OUI (Organizationally Unique Identifier) prefixes (40,000+ entries)
- Device Tracking & Persistence: Automatically track detected devices and persist them to a high-performance embedded SQLite database (
devices.db) with index-sorted queries and Write-Ahead Logging (WAL) for lock-free, concurrent reads and writes. - HTTP API (optional): Opt-in REST API server to query devices as JSON. (Requires the
http-apifeature.) - Library API: Use as a library in your own Rust projects
- CLI Tool: Run as a standalone command-line tool
- Type-Safe: Strongly typed enums for message types, operations, and options
- Kernel-Side Capture Filter: On Linux, the capture socket carries a BPF filter, so the kernel discards bulk traffic (TCP 80/443, QUIC, DNS) before it is ever copied to LANwatch. On a busy router this is the difference between touching every frame on the segment and touching only discovery traffic. Everything the parsers accept still arrives, and if the filter cannot be attached, capture falls back to an unfiltered socket rather than stopping.
- Cross-Platform: Works on macOS, Linux, and other Unix-like systems. The capture filter above is Linux-only; other platforms capture unfiltered, exactly as before.
Add to your Cargo.toml. The http-api feature is opt-in to keep binary size small by default, while mdns and ssdp provide additional active and passive discovery:
[dependencies]
# Smallest binary footprint, core DHCP & MAC tracking only
lanwatch = "0.18"
# With HTTP API server and active discovery protocols
lanwatch = { version = "0.18", features = ["http-api", "mdns", "ssdp"] }Or clone and build from source. Release builds are automatically optimized for size (opt-level = "z", strip = true, panic = "abort"):
git clone <repository-url>
cd lanwatch
# Smallest possible build
cargo build --release
# Enable everything
cargo build --release --all-featuresLANwatch is pre-1.0, so minor versions may break API compatibility. Only library consumers are affected; the CLI and its flags are unchanged.
No action is required, and no stored data is reset. The new a: namespace
adds tokens; it changes none that already exist, so a saved attribute set stays
correct and simply grows as the device announces itself again.
Three things are worth knowing:
- A device's
fullhash moves once it advertises an agent product.a:is not part of thecorehash, so core matching against a catalogue built before or after this release still works.FINGERPRINT_VERSIONis deliberately unchanged: bumping it would delete every stored token to add one namespace. TokenNamespacegained a variant. An exhaustivematchover it in library code needs a new arm. Nothing else in the library surface changed.--query-intervalis opt-in. Without it, the active probes are sent once at startup, exactly as before.
No action is required. Nothing in the CLI, the API, the database or the library surface changed.
On Linux the capture socket now carries a BPF filter, so the kernel drops
traffic that no parser reads before it is copied. Discovery results are
unchanged: everything the parsers accept still reaches them. If a future build
adds a protocol on a new port, FILTER_EXPRESSION in src/capture_filter.rs
must be regenerated, and the test suite fails until it is.
Two things are worth knowing:
- A packet sniffer attached to the same socket sees only discovery traffic. If you were reading LANwatch's socket for anything else, it is no longer a full tap.
- The filter is an optimisation, never a requirement. On a non-Linux system, or if the socket cannot be prepared, LANwatch prints a warning and captures unfiltered exactly as it did before.
No action is required. The three fingerprint columns and the two new tables
(device_attributes, device_overrides) are created on an existing database at
startup. There is no migration step and no database wipe.
Two things are worth knowing:
- Attribute tokens start from zero. They are only recorded while a build
with this feature is capturing, so a device seen for months has no tokens
until the new binary runs. A device needs 4 tokens, 2 minutes of age and
1 minute of quiet before it is usable, and DHCP alone yields only 3 tokens —
run with
--mdnsand--ssdp(or--mdns-query/--ssdp-query) or most devices will never ripen. - The API now accepts writes and has no authentication. If your listener is
reachable beyond a trusted network, add
--api-readonly.
Library consumers: DeviceInfo gained four fields (fingerprint,
fingerprint_label, fingerprint_confidence, product_label), so struct
literals need updating. CSV field order and semantics are unchanged.
The parallel borrowed "view" types have been removed. They mirrored the owned packet structs but were built from an already-parsed packet, so they allocated to hold borrows of data the owned types already exposed by reference. Call the owned packet directly instead.
// Before
let view = packet.view();
for record in view.all_records() {
if let MdnsRecordDataView::A(addr) = &record.data { /* ... */ }
}
let vendor = ssdp.view().detect_vendor_from_view();
// After
for record in packet.all_records() {
if let MdnsRecordData::A(addr) = &record.data { /* ... */ }
}
let vendor = ssdp.detect_vendor();| Removed | Replacement |
|---|---|
MdnsPacketView, MdnsRecordView, MdnsQuestionView, MdnsRecordDataView |
MdnsPacket, MdnsRecord, MdnsQuestion, MdnsRecordData |
SsdpPacketView, SsdpMessageTypeView |
SsdpPacket, SsdpMessageType |
MdnsPacket::view(), SsdpPacket::view() |
no longer needed |
SsdpPacket::fingerprint_text() |
detect_vendor() / detect_device_type() |
detect_vendor_from_view() |
SsdpPacket::detect_vendor() |
detect_device_type_from_view() |
SsdpPacket::detect_device_type() |
Three accessors now borrow instead of cloning, so callers that stored the
result need .to_string() (or to hold the packet alive):
SsdpPacket::service_terms() -> Vec<&str>(wasVec<String>)MdnsPacket::get_ipv4_addresses() -> Vec<(&str, Ipv4Addr)>(wasVec<(String, _)>)MdnsPacket::get_ipv6_addresses() -> Vec<(&str, Ipv6Addr)>(wasVec<(String, _)>)
CctvPacket gained a public claimed_mac: Option<[u8; 6]> field, so literal
construction of it needs updating. source_mac is now always the MAC observed
in the Ethernet header; a MAC asserted by the packet body appears in
claimed_mac and is never treated as an identity. See
Handling Untrusted Traffic.
# List available interfaces
sudo cargo run
# Sniff DHCP traffic on a specific interface (saves to devices.db by default)
sudo cargo run -- en0 # macOS
sudo cargo run -- eth0 # Linux
# Specify a custom output database file
sudo cargo run -- en0 -o /path/to/devices.db
sudo cargo run -- en0 --output devices.db
# Load additional OUI database entries
sudo cargo run -- en0 --oui /path/to/oui.txt
sudo cargo run -- en0 -u ieee-oui.txt
# Start with HTTP API server (requires the http-api feature)
sudo cargo run --features http-api -- en0 --api 0.0.0.0:8080
# Start with API on default address (127.0.0.1:8080)
sudo cargo run --features http-api -- en0 --api-default
# Enable mDNS sniffing for enhanced device discovery (requires mdns feature)
sudo cargo run --features mdns -- en0 --mdns
# Enable SSDP/UPnP sniffing for enhanced device discovery (requires ssdp feature)
sudo cargo run --features ssdp -- en0 --ssdp
# Enable mDNS with active querying (sends discovery probes)
sudo cargo run --features mdns -- en0 --mdns-query
# Enable SSDP with active M-SEARCH discovery probes
sudo cargo run --features ssdp -- en0 --ssdp-query
# Pin a specific device's type when it can't be auto-detected (repeatable)
sudo cargo run -- en0 --override 'c0:84:7d:11:22:33=Security System'
# Load per-device classification overrides from a file
sudo cargo run -- en0 --overrides overrides.txt
# Load a device attribute fingerprint catalogue
sudo cargo run --all-features -- en0 --fingerprints lanwatch-fingerprints.txt
# Write the observed attribute sets to a labelling worksheet, then exit
# (reads the database only; no interface and no root needed)
cargo run -- --dump-fingerprints dump.txt
# Erase one device completely: sightings, attribute tokens and your correction
# (stop the service first, or a live capture writes the device straight back)
cargo run -- -o devices.db --forget aa:bb:cc:dd:ee:ff
# Combine all options
sudo cargo run --all-features -- en0 -o devices.db --api 0.0.0.0:8080 --mdns-query --ssdp-query -u oui.txt
# Show help
cargo run -- --helpNote: Root/sudo privileges are typically required for packet capture.
The tool saves detected devices to an embedded SQLite database file. The schema contains the following fields:
- first_seen: ISO 8601 timestamp of first detection
- last_seen: ISO 8601 timestamp of last DHCP/mDNS/LLDP activity
- mac_address: Device MAC address (for DHCPv6, extracted from DUID-LL/LLT when available; otherwise stored as
duid:...) - ip_address: IPv4 address (requested or assigned)
- ipv6_address: Primary IPv6 address selected based on preference (Global Unicast (GUA) > Unique Local (ULA) > Link-Local (LLA))
- ipv6_addresses: Semicolon-separated list of all detected IPv6 addresses for this device
- hostname: Device hostname if available (empty if not)
- device_type: Device type inferred from discovery info (e.g., "Chromecast", "Apple TV", "Router", "Switch")
- vendor: Detected vendor based on OUI registry, hostnames, or hardware descriptors
- services: Semicolon-separated list of mDNS services (requires
mdnsfeature) - system_description: Detailed hardware or system description parsed from link-layer protocols (e.g., LLDP system descriptions or CDP software versions)
- fingerprint: Attribute fingerprint of the device, e.g.
lwfp1:7f3a91c2d4e5b608:1a2b3c4d5e6f7081:34(see Device Attribute Fingerprinting) - fingerprint_label: Name of the matched catalogue profile, if any
- fingerprint_confidence: Confidence of that match, 0-99. Never 100: a fingerprint is evidence, not proof
- fingerprint_flags: Comma-separated flags; currently only
cappedfor a device that advertised more attributes than the per-device cap
A second table, device_attributes, holds the raw attribute tokens per device (mac_address, token, first_seen, last_seen). The token set is stored, not only the hash, because a hash alone can never answer "how close is this?".
A third table, device_overrides, holds the manual classifications entered from the dashboard (mac_address, device_type, vendor, label, reviewed_at, plus the heuristic guess each override replaced, so "reset to guess" can restore it). It lives in its own table rather than as columns on devices, so a correction survives a device being removed and rediscovered.
Removing a device and forgetting one are different actions:
| Action | Clears | Keeps |
|---|---|---|
Remove Device (dashboard, DELETE /devices/{mac}) |
the devices row |
your correction and the collected tokens |
Reset to guess (dashboard, DELETE /devices/{mac}/classification) |
your correction | the sighting and the tokens |
lanwatch --forget <MAC> |
all three | nothing |
Discovery is keyed on MAC alone, so a device still on the LAN reappears within seconds of a removal. Keeping the correction means one careless click cannot destroy review work; the device comes back already labelled. --forget is the deliberate opposite, for a device that has left the network for good and whose rows no dashboard can reach any more. Stop the service before running it, or a live capture may still hold the device in memory and write it straight back.
The fingerprint columns and both new tables are added to an existing database automatically on startup. No migration step and no database wipe is needed.
Database writes are executed in transactions using single-row transactional upserts, eliminating write-amplification and improving write efficiency to O(1) performance. Reads are concurrent and lock-free thanks to WAL mode.
When mDNS sniffing is enabled, the tool can identify devices based on the services they advertise. You can provide a custom services file to enhance identification:
The mDNS parser is defensive against malformed packets, including cyclic compression pointers and oversized DNS header counts.
# Use a custom services file
sudo cargo run --features mdns -- en0 --mdns -s mdns-services.txtServices file format:
# Comment lines start with #
_service._tcp.local # Description of the service
_http._tcp.local # Web Server
_airplay._tcp.local # AirPlay, Apple
_googlecast._tcp.local # Google Chromecast streaming protocol, Google
The tool includes built-in detection for:
- Vendors: Apple, Google, Amazon, Spotify, NVIDIA, Microsoft, Sony, Samsung, etc.
- Device Types: Chromecast, Apple TV, Fire TV, AirPlay Device, Printer, Scanner, NAS, Smart Home Device, Android TV, NVIDIA Shield, etc.
Loading a services file allows for more comprehensive identification. Device types are inferred from service descriptions (e.g., "_googlecast._tcp" → "Chromecast").
To prevent the binary from growing bloated and containing stale database listings, LANwatch does not bundle the IEEE OUI database. Instead, you can download the latest official IEEE registry dynamically and load it at runtime.
How to download and use the latest OUI registry:
-
Download the latest official OUI database file:
sudo cargo run -- --download-oui # This downloads the official list from standards-oui.ieee.org and saves it to `ieee-oui.txt`The download shells out to
curland acceptshttps://URLs only, with--proto =httpsso a redirect cannot downgrade the transport. Library callers passing a custom URL todownload_ieee_ouiare held to the same rule. -
Run LANwatch loading the downloaded OUI database:
sudo cargo run -- en0 --oui ieee-oui.txt
Default OUI location:
At startup, LANwatch automatically looks for ieee-oui.txt (the file --download-oui produces) in the current working directory, falling back to oui.txt if that isn't found. If either is found, it will load it automatically.
You can load any custom OUI file using the --oui parameter:
# Load a custom OUI mapping file
sudo cargo run -- en0 --oui custom-oui.txtSupported OUI file formats:
Both the --oui/-u flag and the default auto-load path auto-detect which format below a file is in, so either can be pointed at an IEEE registry download or a hand-written override list interchangeably.
# IEEE OUI format (from IEEE registry downloads)
00-1A-2B (hex) Vendor Name, Inc.
# Simple colon format
00:1A:2B Vendor Name
# Simple dash format
00-1A-2B Vendor Name
# Compact format (no separators)
001A2B Vendor Name
# Comments start with #
# This is a comment line
The IEEE maintains the official OUI registry at: https://standards-oui.ieee.org/oui/oui.txt
Vendor priority: mDNS-detected vendors take precedence over OUI lookups, as mDNS provides more specific identification (e.g., "Google Chromecast" vs just "Google" from OUI).
Some devices expose no reliable passive signature — no mDNS/SSDP services, an opaque hostname, and
a MAC OUI belonging to a generic module maker rather than the product vendor. For example, a
SimpliSafe video doorbell built on an AMPAK Wi-Fi module (OUI c0:84:7d) reports the vendor "AMPAK
Technology, Inc." and, because its DHCP fingerprint requests option 249, is auto-classified as
"PC/Windows". There is no safe automatic rule for such devices, so LANwatch lets you pin the
classification manually by MAC address.
A manual override always wins over heuristic classification. It is applied to existing database entries at startup and re-enforced during live capture, so subsequent DHCP renewals (or any other heuristic) cannot revert it.
Inline override (--override, repeatable):
# Format: MAC=DeviceType
sudo cargo run -- en0 --override 'c0:84:7d:11:22:33=Security System'
# Pin several devices at once
sudo cargo run -- en0 \
--override 'c0:84:7d:11:22:33=Security System' \
--override 'aa:bb:cc:dd:ee:ff=Printer'Override file (--overrides):
sudo cargo run -- en0 --overrides overrides.txtOverride file format:
# Comment lines start with #
# Format: MAC,DeviceType[,Vendor] (Vendor is optional)
c0:84:7d:11:22:33, Security System, SimpliSafe
aa:bb:cc:dd:ee:ff, Printer
The MAC address is matched case-insensitively and accepts common separators. The device type should
be a canonical type name (e.g. Security System, Smart Doorbell, Security Camera, Printer); an
unrecognized name is preserved verbatim as a custom label. The optional third field pins the vendor.
Some devices ship a large, generic list of mDNS/SSDP attributes that the vendor never cleaned up. Classifying those by service name alone produces confident nonsense: a sprinkler controller reported as a Chromecast, a doorbell as an audio device. Fingerprinting answers a different question -- what set of attributes does this device advertise? -- and can therefore recognise a model it has never seen a name for.
Full design: docs/fingerprint-design.md.
What is collected. Attribute keys, never values. mDNS service types and TXT keys, SSDP
ST/NT targets and header names, product names from the SSDP SERVER/USER-AGENT headers,
DHCPv4 option 55/60/43, DHCPv6 option 15/16. TXT values carry firmware versions, serial numbers and
user-typed names, so they are discarded. A dumped catalogue is safe to share.
Each attribute becomes one short token with a namespace prefix:
m:_googlecast._tcp mDNS service type
t:_googlecast._tcp/md mDNS TXT key, scoped to its service
s:urn:schemas-upnp-org:device:mediarenderer:1 SSDP ST/NT
h:server SSDP header name
a:sonos SERVER/USER-AGENT product, version stripped
d:55=1-3-6-15-119-252 DHCPv4 option 55, ORDER PRESERVED
v:msft vendor class, version suffix stripped
e:311 DHCPv6 IANA enterprise number
Two hashes. core covers the protocol identity and survives a firmware update; full adds TXT
keys, header names and agent products, and is tighter. Agent products stay out of core because a
general purpose host advertises them only while an application is running, so opening a browser
would otherwise move the host's core hash. The stored string is
lwfp1:<core>:<full>:<token count>, hashed with FNV-1a 64-bit. The lwfp1 prefix is the format
version; a rules change bumps it and old state is reset automatically at load.
Where to see the tokens. /devices carries only the packed hash. The tokens behind it are a
separate route, GET /devices/{mac}/attributes, which the dashboard's device panel also shows under
Attributes, grouped by namespace. Tokens that feed the stable core hash are marked.
Matching. An exact full match scores 95, an exact core match 85, and a fuzzy match uses
Jaccard similarity over core tokens (accepted only at 0.60 and above). A profile flagged generic
loses 25 points and never sets a device type -- it only supplies an honest label such as
Generic UPnP MediaRenderer. Confidence is capped at 99, never 100.
Precedence: a manual override always wins, then a match at 70 or above, then the existing heuristics, then a match between 40 and 69 which only fills fields still unset. Below 40 nothing is applied, though the tokens are still recorded.
Building the catalogue. lanwatch-fingerprints.txt ships empty on purpose: with no profile the
existing heuristics run unchanged, so a fingerprint only ever adds information. Fill it from your own
network:
The normal way is through the dashboard. Nothing is typed into a file by hand:
# 1. Capture normally for a few days so the attribute sets can settle
sudo lanwatch eth0 --mdns --ssdp --api-default
# 2. Open http://127.0.0.1:8080, click a device, and use "Identify this device".
# Correct the type and vendor if they are wrong, then give it a product name.
# A reviewed device gets a check mark in the list.
# 3. Click "Export Fingerprints". The downloaded file carries no MAC, IP or
# hostname, so unlike a dump it is safe to share.
# 4. Merge into the shared catalogue
make fingerprints-merge FILE=~/Downloads/lanwatch-fingerprints.txtThe product name is what marks a device as reviewed, and only reviewed and
ripe devices are exported. Corrections are stored in the device_overrides
table, so they survive a restart and keep beating heuristic classification.
There is also an offline path, for a capture host with no dashboard. It writes a private worksheet that must be edited by hand and never committed:
lanwatch -o devices.db --dump-fingerprints dump.txt
$EDITOR dump.txt # fill in `label`; type and vendor are pre-filled
make fingerprints-merge FILE=dump.txtThe catalogue is tab separated with seven fields:
# core_hash device_type vendor label flags tokens meta
7f3a...c1 Media Player Google Chromecast (3rd gen) - m:_googlecast... n=4,oui=1
0000...00 - - Generic UPnP MediaRenderer generic s:urn:... n=31,oui=9
meta records how many units a hash was seen on (n=) and how many distinct OUI vendors those
units had (oui=). That second number is the automatic generic detector: one hash appearing under
three or more unrelated vendors cannot be one product, so it is a copied default profile and is
flagged as such.
The worksheet pre-fills device_type and vendor from LANwatch's heuristics, but never label.
That is deliberate. A catalogue match at confidence 70 or above overrides the heuristic, so a
guess copied straight back in would become permanent and start confirming itself. The empty label
is the review mark: make fingerprints-merge skips every row that still has - there, so nothing
reaches the shared catalogue until a person has named the product.
Never commit a dump. Its comment lines carry MAC and IP addresses so you know what you are labelling.
make fingerprints-mergestrips every comment, and refuses the whole file outright if an address reaches a data line./fingerprint-dump*.txtand/dump.txtare in.gitignore.
Note: The HTTP API requires the
http-apifeature, which is enabled by default. Build with--no-default-featuresto disable it.
When started with --api or --api-default, the tool exposes a REST API for querying devices:
Endpoints:
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Serves the interactive Web Dashboard |
/devices |
GET | List all devices as JSON (sorted by last_seen) |
/devices/{mac} |
GET | Get a specific device by MAC address |
/devices/count |
GET | Get device count |
/devices/{mac} |
DELETE | Remove a single device by MAC address |
/devices |
DELETE | Remove all tracked devices (flush) |
/devices/{mac}/classification |
PUT | Pin the device type, vendor and product name |
/devices/{mac}/classification |
DELETE | Drop the pin and restore the heuristic guess |
/devices/{mac}/attributes |
GET | The raw fingerprint tokens behind that device's hash |
/device-types |
GET | Every canonical device type name, for a picker |
/fingerprints/export |
GET | The reviewed devices as a shareable catalogue file |
/favicon.ico |
GET | The dashboard's tab icon (the same embedded PNG as /logo.png) |
/health |
GET | Health check endpoint |
The API has no authentication. Start with
--api-readonlyto refuse every write (PUT, and bothDELETEroutes) whenever the listener is reachable beyond a trusted network.
Note: Removal only clears current state (in-memory and the database row). Discovery is passive and keyed solely by MAC address, so a device still active on the LAN will simply reappear as a new entry the next time it's observed (DHCP renewal, ARP, mDNS, etc.).
Example Requests:
# Get all devices
curl http://localhost:3000/devices
# Get device count
curl http://localhost:3000/devices/count
# Get a specific device
curl http://localhost:3000/devices/AA:BB:CC:DD:EE:FF
# Health check
curl http://localhost:3000/health
# Remove a single device
curl -X DELETE http://localhost:3000/devices/AA:BB:CC:DD:EE:FF
# Pin a device's identity (this is what the dashboard's Identify panel calls)
curl -X PUT http://localhost:3000/devices/AA:BB:CC:DD:EE:FF/classification \
-H 'Content-Type: application/json' \
-d '{"device_type":"Printer","vendor":"Brother","label":"Brother HL-L2350DW"}'
# Drop the pin and go back to the heuristic guess
curl -X DELETE http://localhost:3000/devices/AA:BB:CC:DD:EE:FF/classification
# See the attribute tokens one device advertises
curl http://localhost:3000/devices/AA:BB:CC:DD:EE:FF/attributes | jq
# Just the agent products (SSDP SERVER / USER-AGENT)
curl -s http://localhost:3000/devices/AA:BB:CC:DD:EE:FF/attributes \
| jq '.data.attributes[] | select(.token | startswith("a:"))'
# Download the reviewed devices as a shareable fingerprint catalogue
curl -O http://localhost:3000/fingerprints/export
# Remove all devices
curl -X DELETE http://localhost:3000/devicesExample Response (/devices):
{
"success": true,
"count": 1,
"data": [
{
"mac_address": "AA:BB:CC:DD:EE:FF",
"ip_address": "192.168.1.100",
"ipv6_address": "fe80::1",
"ipv6_addresses": ["fe80::1", "2001:db8::1"],
"hostname": "mydevice",
"services": ["_http._tcp", "_airplay._tcp"],
"vendor": "Apple",
"device_type": "AirPlay Device",
"first_seen": "2026-01-16T10:25:00Z",
"last_seen": "2026-01-16T10:30:45Z"
}
]
}LANwatch features a premium web-based dashboard accessible directly at http://localhost:8080/ (or the custom address configured via the --api flag) when running the sniffer. The dashboard dynamically updates as devices are discovered, offering search filtering, category tabs, and detailed inspection views.
Key UI Features:
- Simultaneous IPv4 & IPv6 Tracking: Displays both IPv4 and multiple IPv6 addresses (link-local, local, global unicast) for each device tile, including a themed badge for additional detected IPv6 addresses.
- Scope-based IP Classification: In the inspection drawer, addresses are clearly organized and labeled by scope (
IPv4,IPv6 Link-Local,IPv6 Unique Local,IPv6 Global). - Secure Context Clipboard Fallback: Integrated copy-to-clipboard buttons for all IP and MAC addresses that work seamlessly in both secure contexts (HTTPS/localhost) and insecure contexts (HTTP/remote IP) with visual green success animations.
- Remove / Flush Devices: Each device tile and the inspection drawer offer a "Remove Device" action, and a toolbar "Flush All" button clears the entire tracked list — both gated behind a confirmation prompt.
- App Icon: The dashboard declares a tab icon and an
apple-touch-icon, so a browser tab shows the LANwatch mark instead of its blank placeholder, and "Add to Home Screen" on a phone gets the same mark. The icon is the PNG already compiled into the binary and served from/logo.png, so it works on a host with no route to the internet./favicon.icoanswers with the same image, for clients that ask for it regardless of what the page declares.
use lanwatch::{DhcpSniffer, DhcpEvent, DeviceTracker, Dhcpv6Option};
fn main() {
let mut sniffer = DhcpSniffer::new("en0").expect("Failed to create sniffer");
let mut tracker = DeviceTracker::new("devices.db").expect("Failed to create tracker");
sniffer.run(|event| {
match &event {
DhcpEvent::V4(packet) => {
let is_new = tracker.update_from_dhcpv4(packet);
println!("DHCPv4: {} -> {}", packet.source_ip, packet.dest_ip);
println!(" Type: {:?}", packet.message_type);
println!(" Client MAC: {}", packet.client_mac_string());
if is_new {
println!(" [New or updated device]");
}
}
DhcpEvent::V6(packet) => {
let is_new = tracker.update_from_dhcpv6(packet);
println!("DHCPv6: {} -> {}", packet.source_ip, packet.dest_ip);
println!(" Type: {}", packet.message_type);
}
}
true // Continue sniffing
});
}use lanwatch::{parse_dhcpv4_payload, parse_dhcpv6_payload};
use std::net::{Ipv4Addr, Ipv6Addr};
// Parse a DHCPv4 payload
if let Some(packet) = parse_dhcpv4_payload(
&payload_bytes,
Ipv4Addr::new(0, 0, 0, 0),
Ipv4Addr::new(255, 255, 255, 255),
68, 67,
) {
println!("Message type: {:?}", packet.message_type);
}
// Parse a DHCPv6 payload
if let Some(packet) = parse_dhcpv6_payload(
&payload_bytes,
Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1),
Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 1, 2),
546, 547,
) {
println!("Message type: {}", packet.message_type);
println!("Transaction ID: {}", packet.transaction_id_string());
println!("Options: {:?}", packet.options);
}Run the included examples:
# Basic sniffer with packet counter
sudo cargo run --example basic_sniffer en0
# Parse sample payloads (no root required)
cargo run --example parse_payloadDhcpSniffer- Main sniffer struct for capturing DHCP packetsNetworkSniffer- Extended sniffer for DHCP + mDNS/SSDP (requiresmdnsand/orssdpfeature)DhcpEvent- Enum containing eitherV4(Dhcpv4Packet)orV6(Dhcpv6Packet)NetworkEvent- Enum for DHCP, mDNS, or SSDP events (requiresmdnsand/orssdpfeature)Dhcpv4Packet- Parsed DHCPv4 packet with all fieldsDhcpv6Packet- Parsed DHCPv6 packet with all fieldsMdnsPacket- Parsed mDNS packet (requiresmdnsfeature)MdnsRecord- DNS resource record from mDNSMdnsQuerier- Active mDNS query sender (requiresmdnsfeature)SsdpPacket- Parsed SSDP packet (requiresssdpfeature)SsdpQuerier- Active SSDP M-SEARCH sender (requiresssdpfeature)DeviceTracker- Track detected devices and persist to an embedded SQLite databaseDeviceInfo- Information about a detected deviceOuiRegistry- IEEE OUI database for MAC-to-vendor lookupsDhcpError- Error types for sniffer operationsApiServer- HTTP API server for querying devices
DHCPv4:
Dhcpv4MessageType: Discover, Offer, Request, Decline, Ack, Nak, Release, InformDhcpv4Operation: BootRequest, BootReply
DHCPv6:
Dhcpv6MessageType: Solicit, Advertise, Request, Confirm, Renew, Rebind, Reply, Release, Decline, Reconfigure, InfoRequestDhcpv6Option: ClientId, ServerId, IaNa, ClientFqdn, Other
DHCPV4_SERVER_PORT(67)DHCPV4_CLIENT_PORT(68)DHCPV6_CLIENT_PORT(546)DHCPV6_SERVER_PORT(547)MDNS_PORT(5353) - requiresmdnsfeatureMDNS_IPV4_MULTICAST(224.0.0.251) - requiresmdnsfeatureMDNS_IPV6_MULTICAST(ff02::fb) - requiresmdnsfeatureSSDP_PORT(1900) - requiresssdpfeatureSSDP_IPV4_MULTICAST(239.255.255.250) - requiresssdpfeatureSSDP_IPV6_MULTICAST(ff02::c) - requiresssdpfeature
list_interfaces()- List available network interfacesfind_interface(name)- Find interface by nameis_dhcpv4_ports(src, dest)- Check if ports indicate DHCPv4is_dhcpv6_ports(src, dest)- Check if ports indicate DHCPv6is_mdns_ports(src, dest)- Check if ports indicate mDNS (requiresmdnsfeature)is_ssdp_ports(src, dest)- Check if ports indicate SSDP (requiresssdpfeature)parse_dhcpv4_payload(payload, src, dst, src_port, dst_port)- Parse DHCPv4 from raw bytesparse_dhcpv6_payload(payload, src, dst, src_port, dst_port)- Parse DHCPv6 from raw bytesparse_mdns_payload(payload, source_mac, src, dst)- Parse mDNS from raw bytes (requiresmdnsfeature)parse_ssdp_payload(payload, source_mac, src, dst)- Parse SSDP from raw bytes (requiresssdpfeature)display_safe(value)- Wrap untrusted text so printing it cannot emit terminal escapes or display-spoofing codepoints (borrows, no allocation)build_mdns_query(name, record_type)- Build an mDNS query packet (requiresmdnsfeature)build_ssdp_search_request(search_target)- Build an SSDP M-SEARCH request (requiresssdpfeature)start_api_server(addr, tracker)- Start HTTP API server in background thread (requireshttp-apifeature)to_json()/to_json_sorted()- Export devices as JSON (requireshttp-apifeature)
| Feature | Default | Description |
|---|---|---|
http-api |
✗ | Enables the HTTP REST API server, JSON export, and Web Dashboard |
mdns |
✗ | Enables mDNS (Multicast DNS) sniffing for enhanced device discovery |
ssdp |
✗ | Enables SSDP/UPnP sniffing and active M-SEARCH discovery |
# Build with minimal default features
cargo build --release
# Build with HTTP API server & web dashboard
cargo build --release --features http-api
# Build with mDNS support
cargo build --release --features mdns
# Build with SSDP support
cargo build --release --features ssdp
# Build with all features
cargo build --release --all-featuresWhen the mdns feature is enabled, the tool can capture mDNS traffic to discover:
- Device hostnames (
.localnames) - Service types (HTTP, AirPlay, Chromecast, printers, etc.)
- IP address to hostname mappings
Passive mode (--mdns): Captures mDNS announcements as devices broadcast them.
Active mode (--mdns-query): Also sends multicast queries for common services. To prevent multicast leakage over WAN ports on multi-homed hosts and routers, LANwatch queries the local IPv4 address of the specified network interface and binds the outgoing query sockets directly to it. Common queried services include:
_http._tcp.local- Web servers_airplay._tcp.local- Apple AirPlay devices_googlecast._tcp.local- Chromecast devices_printer._tcp.local- Network printers_smb._tcp.local- SMB file shares- And more...
When the ssdp feature is enabled, the tool can capture SSDP (Simple Service Discovery Protocol) traffic to discover UPnP devices:
- Media servers and renderers (DLNA/UPnP)
- Network routers and gateways
- Smart home devices and IoT equipment
- Printers and scanning devices
- Vendor fingerprinting via SSDP headers
Passive mode (--ssdp or --upnp): Listens for SSDP announcements (NOTIFY messages and responses).
Active mode (--ssdp-query): Also sends M-SEARCH discovery probes. Similar to active mDNS, the outgoing socket binds directly to the local interface IPv4 address to prevent queries from leaking onto the WAN interface:
ssdp:all- All SSDP devicesupnp:rootdevice- All UPnP root devices- MediaServer and MediaRenderer devices
- Internet Gateway Devices (routers)
- Printer and DIAL-enabled devices (Chromecast, etc.)
The tool extracts vendor information and device types from SSDP server headers and service descriptors.
--mdns-query and --ssdp-query send their probes once, at startup. A device that was asleep,
off the LAN, or simply not listening in that first second is only found later if it announces itself
on its own. Many devices never do.
--query-interval <DUR> re-sends whichever active probes are enabled, on a timer. DUR accepts
90s, 15m or 2h; a bare number means seconds. The minimum is 30 seconds, because active
discovery puts multicast on the wire and a typo such as 1 must not turn LANwatch into a flood
source.
# Ask again every 15 minutes for the whole run
sudo lanwatch eth0 --mdns-query --ssdp-query --query-interval 15m --api-defaultThe flag does nothing on its own: with neither --mdns-query nor --ssdp-query it warns and is
ignored. Passive capture is unaffected either way.
When raw network sniffing is enabled (via the mdns or ssdp features), LANwatch automatically captures Layer 2 ARP frames (both Request and Reply operations). This allows LANwatch to discover silent devices on the local network link that do not transmit DHCP, mDNS, or SSDP/UPnP traffic, expanding discovery coverage significantly.
When parsing DHCPv4 payloads, LANwatch extracts:
- Option 55 (Parameter Request List): Matches common signature sequences (such as Option 249 for Microsoft Windows, Option 95 for Apple, and Options 26 & 28 for Android/Linux) to identify the operating system.
- Option 60 (Vendor Class Identifier): Matches vendor class identifier strings to classify device brands and operating systems (e.g.,
MSFT-> Microsoft PC/Windows,Android-> Google Android Phone,Hewlett-Packard JetDirect-> HP Printer,Roku-> Roku Media Player,Sonos-> Sonos Smart Speaker,Apple TV-> Apple TV). - Option 43 (Vendor-Specific Info): Extracts printable ASCII sequences from Option 43 sub-options to identify enterprise and VoIP hardware (e.g.,
UniFi-> Ubiquiti Network Device,Yealink/Polycom/Mitel/Avaya-> IP Phone,Cisco).
Under the mdns feature gate, LANwatch listens passively to UDP port 5355 LLMNR packets. LLMNR requests and responses (which share standard RFC 1035 DNS structure) are sniffed and parsed to automatically associate IP addresses with local device hostnames without needing active reverse DNS queries.
Under the mdns feature gate, LANwatch passively sniffs NetBIOS Name Service (NBNS) packets on UDP port 137. It decodes the two-character nibble-encoded NetBIOS names to resolve local network hostnames, and checks name suffix roles (e.g. 0x20 indicating the File Sharing service) to identify and classify roles like "File Server".
Under the ssdp feature gate, LANwatch passively listens to UDP port 3702 WS-Discovery SOAP XML probe and announcement traffic. By parsing elements such as <wsd:Types> (e.g., NetworkVideoTransmitter for IP Cameras) and publisher <pub:ModelName> metadata, WSD sniffing dynamically identifies smart hardware, printers, and surveillance equipment.
When processing mDNS traffic under the mdns feature gate, LANwatch extracts key-value parameters from TXT records (such as model, md, or ty). These hardware and model descriptors (e.g., AppleTV14,1, Sonos Play:1, or printer models) are cross-referenced to determine exact manufacturer and device classifications.
Under the mdns feature gate, LANwatch monitors mDNS announcements to extract metadata from HomeKit Accessory Protocol (_hap._tcp) and Matter (_matter._tcp) service instances. It decodes Manufacturer, Model, Category Identifier (determining device type like Outlet, Smart Light, Sensor, Bridge), and Pairing status.
Under the ssdp feature gate, LANwatch passively sniffs UDP port 5683 to parse CoAP (Constrained Application Protocol) payloads (e.g., CoRE Link Format) and UDP port 3671 to parse KNXnet/IP building automation traffic, dynamically categorizing sensors, smart bulbs, switches, and home automation systems.
Under the ssdp feature gate, LANwatch sniffs physical security endpoints via:
- Hikvision SADP: Sniffing UDP ports 9999 and 37020 for SOAP XML probe/response frames, extracting exact model names and serial numbers.
- Dahua Discovery: Sniffing UDP port 37810 JSON-over-UDP frames, parsing camera models and serial numbers.
- RTSP Traffic: Sniffing TCP port 554 to identify generic surveillance and media streaming cameras. Only traffic originating from port 554 with an RTSP-shaped payload is counted, so a client connecting to a camera is not itself recorded as one.
A device is always keyed on the MAC observed in the Ethernet header. SADP and Dahua payloads may also claim a MAC; that value is surfaced separately as CctvPacket::claimed_mac and never used as an identity, since any host on the segment can assert an arbitrary one.
Under the ssdp feature gate, LANwatch passively sniffs MQTT (TCP port 1883) and MQTT-SN (UDP port 1883) CONNECT frames to discover IoT client devices, extracting their unique Client Identifier and protocol level.
Under the ssdp feature gate, LANwatch sniffs Plex GDM discovery messages broadcast over UDP ports 32410, 32412, and 32414. By parsing the HTTP-like text payload, it extracts device friendly name, product (e.g. Plex Media Server, Plex for Apple TV), unique resource identifier UUID, and the service port.
| Protocol | Port / Protocol | Target Devices | Data Extracted |
|---|---|---|---|
| CoAP | UDP 5683 | Thread, Zigbee IP, low-power IoT | Device endpoints and resource directories |
| Matter | UDP 5353 (mDNS) | Modern Smart Plugs, Bulbs, Hubs | Vendor ID (VID) & Product ID (PID) |
| KNXnet/IP | UDP 3671 | Smart Building / HVAC Controls | Smart switches, actuator controls |
| SADP | UDP 37020 / 9999 | Hikvision CCTV & IP Cameras | Model number, serial number, firmware version |
| Dahua | UDP 37810 | Dahua CCTV & IP Cameras | Camera model, software version |
| MQTT | TCP/UDP 1883 | IoT Devices, Smart Appliances | Client ID, protocol flavor (MQTT / MQTT-SN) |
| Plex GDM | UDP 32410-32414 | Plex Media Servers & Players | Friendly name, product type, resource UUID, service port |
A Makefile is provided to simplify common development, testing, linting, and documentation tasks.
- Build with all features:
make build
- Build with minimal default features:
make build-minimal
- Build in optimized release mode with all features:
make release
- Cross-compile release binary for ARM64 (aarch64-unknown-linux-gnu):
make release-aarch64
- Run all unit tests:
make test - Generate crate documentation (enforcing 100% doc coverage):
make doc
- Lint the codebase with Clippy (warnings treated as errors):
make clippy
- Verify the bundled dashboard calls no undefined function:
make check-dashboard
- Merge a reviewed fingerprint export into the shared catalogue:
make fingerprints-merge FILE=lanwatch-fingerprints.txt
- Format the codebase:
make fmt
- Verify code formatting:
make fmt-check
- Quickly typecheck code:
make check
- Build all example binaries with all features enabled:
make examples
- List all available make targets:
make help
Everything LANwatch parses arrives unauthenticated from the local segment, and
any host on that segment can send whatever it likes. The parsers are written to
treat all of it as hostile input: malformed or truncated frames yield None
rather than panicking or over-allocating, and the limits below bound what a
single sender can cost you.
| Limit | Value | Why |
|---|---|---|
| DNS compression-pointer jumps per name | 64 | A pointer chain through fresh offsets would otherwise make name decoding quadratic, and it is paid again for every record in the packet |
| Decoded DNS name length | 255 bytes | RFC 1035 §2.3.4 |
| SSDP payload parsed | 8 KiB | A 64 KiB datagram of one-character headers is thousands of allocations at line rate |
| SSDP headers retained | 64, 512 bytes each | Same |
| Services stored per device | 64, 128 bytes each | The list is re-serialized into one SQLite cell on every flush, so unbounded growth costs write amplification as well as memory |
| CCTV model / serial fields | 255 bytes | These land in the database verbatim |
| Consecutive capture errors before giving up | 100 | A removed interface would otherwise spin forever on stderr |
Traffic beyond a limit is dropped, not truncated: a value that long is malformed rather than merely verbose, and silently trimming it would invent a plausible-looking model or hostname no device ever reported.
A device is keyed on the MAC address observed in the Ethernet header. Where a
protocol lets a payload assert its own MAC — Hikvision SADP <MAC>, Dahua
"mac" — that value is recorded separately and never becomes an identity.
Otherwise a single crafted datagram could create or overwrite any entry in the
inventory, including a real device's.
DHCPv4 frames are validated against the RFC 2131 magic cookie and an Ethernet
htype/hlen before chaddr is read as a MAC, so an unrelated datagram that
happens to use port 67/68 cannot contribute six arbitrary bytes as a device.
Device names, models, and descriptions are attacker-controlled strings that end up on your terminal, in the database, in JSON responses, and in the dashboard. They are neutralized in two places, depending on what the value is for:
- Free text (LLDP/CDP TLVs, CCTV model and serial, MQTT client ID, Plex GDM and KNX names, CoAP payloads) is sanitized in the parsers, as it comes off the wire. Control characters, ANSI escapes, bidirectional overrides, and zero-width characters are stripped, and the result is capped at 255 characters. Everything downstream receives already-clean text.
- Protocol identifiers (DNS record names, NetBIOS names, SSDP header
values) must keep their exact bytes to drive service matching and vendor
fingerprinting, so they are stored as received and passed through
display_safe()at the point they are printed.
The dashboard additionally HTML-escapes every value it renders, and the CLI
prints TXT record contents with Rust's Debug formatting, which escapes
control characters on its own.
The capture thread never blocks on the database worker. If the worker falls behind, events are dropped and counted rather than stalling capture — blocking would overflow the kernel's ring buffer and lose the same packets invisibly. A warning is printed every 1000 dropped events, and a final tally on exit:
Warning: worker cannot keep up; 1000 event(s) dropped so far
Seeing this means your capture has gaps. It usually indicates the database is on slow storage, or the interface is carrying more discovery traffic than one worker can classify.
These measures bound resource use and prevent one host from corrupting another
host's record. They are not an authentication boundary. In particular the HTTP
API has no authentication: it binds 127.0.0.1:8080 by default, and DELETE /devices will wipe the database for anyone who can reach it. If you bind it to
a routable address, put your own access control in front of it.
LANwatch includes several configurations and design patterns to maximize execution speed and scalability on hotpaths under high network traffic:
- Host-Specific CPU Optimizations (
target-cpu=native): Configured globally in.cargo/config.tomlto compile LANwatch utilizing all instruction set extensions (AVX2, SSE4.2, NEON, etc.) supported by your local CPU, allowing LLVM to perform advanced loop vectorization and unrolling. - O(1) Transactional Database Updates: Migrated the storage backend from full-file serializations to SQLite with Write-Ahead Logging (WAL), reducing write amplification from O(N) full file rewrites to O(1) single-row updates.
- O(log N) Indexed Sorting: Refactored the REST API query engine to run paginated, pre-sorted index scans inside SQLite directly rather than cloning and sorting the entire registry in-memory, cutting memory overhead to O(page_size) and sorting to O(log N).
- Zero-Allocation Hostname Classification: Device classification in the parser loops avoids heap allocations for case conversion. Service de-duplication compares against the normalized form without building it, so the common "already advertised" case allocates nothing.
- Allocation-Free Packet Inspection: Classification reads directly from the parsed packet rather than through an intermediate borrowed copy. Walking an mDNS response's records allocates nothing (previously two allocations and 320 bytes to build the intermediate), and SSDP vendor and device-type detection match the rule tables against the packet's fields in place.
- Integer-Keyed OUI Registry: An OUI is three bytes, so the vendor table is keyed on the prefix packed into a
u32rather than an"AA:BB:CC"string — removing roughly 35,000 key allocations when the full IEEE registry is loaded, along with a string hash and comparison on every lookup. - Dynamic Allocator Swap: For concurrent HTTP API and high-traffic packet capture, you can preload lock-free allocators (
mimallocorjemalloc) dynamically without altering code:LD_PRELOAD=/usr/lib/libmimalloc.so ./target/release/lanwatch eth0 --api
- Global Customization Skills: Standard optimization and test coverage configurations can be invoked using the global skills
/home/richard/.gemini/config/skills/senior_performance_engineer/and/home/richard/.gemini/config/skills/rust_coverage/.
- pnet_datalink / pnet_packet - Low-level networking libraries for packet capture and frame parsing
- rusqlite - Embedded SQLite database engine with bundled C library for device persistence
- serde - Serialization framework for device data structures
- serde_json - JSON serialization for HTTP API export (optional,
http-apifeature) - tiny_http - Lightweight HTTP server for the REST API and Web Dashboard (optional,
http-apifeature)
This project is dual-licensed under the MIT License and the Apache License, Version 2.0.
- See the LICENSE-MIT file for the MIT License details.
- See the LICENSE-APACHE file for the Apache License details.
Richard Vidal-Dorsch
Contributions are welcome! Please feel free to submit a Pull Request.




