Skip to content
rvidoPublic

About

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.

Resources

Stars

7 stars

Watchers

2 watching

Forks

Repository files navigation

LANwatch

CI License: MIT/Apache-2.0 GitHub stars codecov

LANwatch Logo

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.

LANwatch System Architecture

Release history: CHANGELOG.md.

Features

  • 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 mdns feature.
  • 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 ssdp feature.
  • 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 --override or 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-AGENT headers. 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 SERVER and USER-AGENT headers, 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-interval re-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-api feature.)
  • 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.

Installation

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-features

Upgrading

LANwatch is pre-1.0, so minor versions may break API compatibility. Only library consumers are affected; the CLI and its flags are unchanged.

0.17 → 0.18

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 full hash moves once it advertises an agent product. a: is not part of the core hash, so core matching against a catalogue built before or after this release still works. FINGERPRINT_VERSION is deliberately unchanged: bumping it would delete every stored token to add one namespace.
  • TokenNamespace gained a variant. An exhaustive match over it in library code needs a new arm. Nothing else in the library surface changed.
  • --query-interval is opt-in. Without it, the active probes are sent once at startup, exactly as before.

0.16 → 0.17

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.

0.15 → 0.16

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 --mdns and --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.

0.14 → 0.15

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> (was Vec<String>)
  • MdnsPacket::get_ipv4_addresses() -> Vec<(&str, Ipv4Addr)> (was Vec<(String, _)>)
  • MdnsPacket::get_ipv6_addresses() -> Vec<(&str, Ipv6Addr)> (was Vec<(String, _)>)

0.13 → 0.14

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.

Usage

Command Line

# 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 -- --help

Note: Root/sudo privileges are typically required for packet capture.

Database Schema

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 mdns feature)
  • 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 capped for 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.

mDNS Service Identification

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.txt

Services 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").

IEEE OUI Database

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:

  1. 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 curl and accepts https:// URLs only, with --proto =https so a redirect cannot downgrade the transport. Library callers passing a custom URL to download_ieee_oui are held to the same rule.

  2. 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.txt

Supported 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).

Device Classification Overrides

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.txt

Override 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.

Device Attribute Fingerprinting

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.txt

The 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.txt

The 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-merge strips every comment, and refuses the whole file outright if an address reaches a data line. /fingerprint-dump*.txt and /dump.txt are in .gitignore.

HTTP API

Note: The HTTP API requires the http-api feature, which is enabled by default. Build with --no-default-features to 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-readonly to refuse every write (PUT, and both DELETE routes) 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/devices

Example 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"
    }
  ]
}

Web Dashboard

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.ico answers with the same image, for clients that ask for it regardless of what the page declares.

Device Inspection Info

Library Usage

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
    });
}

Parsing Raw Payloads

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);
}

Examples

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_payload

API Reference

Main Types

  • DhcpSniffer - Main sniffer struct for capturing DHCP packets
  • NetworkSniffer - Extended sniffer for DHCP + mDNS/SSDP (requires mdns and/or ssdp feature)
  • DhcpEvent - Enum containing either V4(Dhcpv4Packet) or V6(Dhcpv6Packet)
  • NetworkEvent - Enum for DHCP, mDNS, or SSDP events (requires mdns and/or ssdp feature)
  • Dhcpv4Packet - Parsed DHCPv4 packet with all fields
  • Dhcpv6Packet - Parsed DHCPv6 packet with all fields
  • MdnsPacket - Parsed mDNS packet (requires mdns feature)
  • MdnsRecord - DNS resource record from mDNS
  • MdnsQuerier - Active mDNS query sender (requires mdns feature)
  • SsdpPacket - Parsed SSDP packet (requires ssdp feature)
  • SsdpQuerier - Active SSDP M-SEARCH sender (requires ssdp feature)
  • DeviceTracker - Track detected devices and persist to an embedded SQLite database
  • DeviceInfo - Information about a detected device
  • OuiRegistry - IEEE OUI database for MAC-to-vendor lookups
  • DhcpError - Error types for sniffer operations
  • ApiServer - HTTP API server for querying devices

Message Types

DHCPv4:

  • Dhcpv4MessageType: Discover, Offer, Request, Decline, Ack, Nak, Release, Inform
  • Dhcpv4Operation: BootRequest, BootReply

DHCPv6:

  • Dhcpv6MessageType: Solicit, Advertise, Request, Confirm, Renew, Rebind, Reply, Release, Decline, Reconfigure, InfoRequest
  • Dhcpv6Option: ClientId, ServerId, IaNa, ClientFqdn, Other

Constants

  • DHCPV4_SERVER_PORT (67)
  • DHCPV4_CLIENT_PORT (68)
  • DHCPV6_CLIENT_PORT (546)
  • DHCPV6_SERVER_PORT (547)
  • MDNS_PORT (5353) - requires mdns feature
  • MDNS_IPV4_MULTICAST (224.0.0.251) - requires mdns feature
  • MDNS_IPV6_MULTICAST (ff02::fb) - requires mdns feature
  • SSDP_PORT (1900) - requires ssdp feature
  • SSDP_IPV4_MULTICAST (239.255.255.250) - requires ssdp feature
  • SSDP_IPV6_MULTICAST (ff02::c) - requires ssdp feature

Helper Functions

  • list_interfaces() - List available network interfaces
  • find_interface(name) - Find interface by name
  • is_dhcpv4_ports(src, dest) - Check if ports indicate DHCPv4
  • is_dhcpv6_ports(src, dest) - Check if ports indicate DHCPv6
  • is_mdns_ports(src, dest) - Check if ports indicate mDNS (requires mdns feature)
  • is_ssdp_ports(src, dest) - Check if ports indicate SSDP (requires ssdp feature)
  • parse_dhcpv4_payload(payload, src, dst, src_port, dst_port) - Parse DHCPv4 from raw bytes
  • parse_dhcpv6_payload(payload, src, dst, src_port, dst_port) - Parse DHCPv6 from raw bytes
  • parse_mdns_payload(payload, source_mac, src, dst) - Parse mDNS from raw bytes (requires mdns feature)
  • parse_ssdp_payload(payload, source_mac, src, dst) - Parse SSDP from raw bytes (requires ssdp feature)
  • 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 (requires mdns feature)
  • build_ssdp_search_request(search_target) - Build an SSDP M-SEARCH request (requires ssdp feature)
  • start_api_server(addr, tracker) - Start HTTP API server in background thread (requires http-api feature)
  • to_json() / to_json_sorted() - Export devices as JSON (requires http-api feature)

Feature Flags

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-features

Network Discovery Flow (Passive vs Active)

Passive vs Active Network Discovery

mDNS Discovery

When the mdns feature is enabled, the tool can capture mDNS traffic to discover:

  • Device hostnames (.local names)
  • 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...

SSDP/UPnP Discovery

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 devices
  • upnp: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.

Repeating the Active Probes (--query-interval)

--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-default

The flag does nothing on its own: with neither --mdns-query nor --ssdp-query it warns and is ignored. Passive capture is unaffected either way.

ARP Sniffing (Layer 2 Passive Discovery)

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.

DHCP Option 55, 60 & 43 OS/Vendor Fingerprinting

DHCP Option Fingerprinting and Classification

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).

LLMNR Sniffing (Link-Local Multicast Name Resolution)

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.

NetBIOS Name Service (NBNS) Sniffing

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".

WS-Discovery (WSD) Sniffing

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.

mDNS TXT Record Parsing

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.

Smart Home & IoT Discovery (Matter & HomeKit)

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.

Constrained & Building Automation Protocols (CoAP & KNXnet/IP)

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.

IP Camera & CCTV Discovery (Physical Security)

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.

MQTT & MQTT-SN Sniffing

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.

Plex GDM (Good Day Mate) Protocol

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.

Summary of IoT & Camera Discovery Protocols

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

Development and Build Automation

A Makefile is provided to simplify common development, testing, linting, and documentation tasks.

Available Makefile Targets:

  • 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

Handling Untrusted Traffic

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.

Parsing limits

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.

Identity is never self-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.

Text harvested from the network

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.

Backpressure

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.

Scope

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.

Performance Tuning & Optimizations

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.toml to 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 u32 rather 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 (mimalloc or jemalloc) 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/.

Dependencies

  • 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-api feature)
  • tiny_http - Lightweight HTTP server for the REST API and Web Dashboard (optional, http-api feature)

License

This project is dual-licensed under the MIT License and the Apache License, Version 2.0.

Author

Richard Vidal-Dorsch

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

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.

Resources

Stars

7 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages