Concepts
LAN Fence combines two ways of seeing the network:
| Mode | What happens |
|---|---|
active | An ARP sweep of the subnet — lanfence scan runs one, lanfence monitor repeats it every scan_interval_seconds. |
passive | Inside lanfence monitor, LAN Fence also listens for ARP traffic on the wire between sweeps, so a device that joins mid-interval is caught sooner. |
Every sighting is folded into a persistent SQLite database
keyed by MAC address, which tracks each device's lifecycle:
new_device the first time it's ever seen,
reappeared if it had gone offline and came back, and
disconnected when an active sweep no longer sees it. Only a
device newly joining or reappearing produces a finding — a plain
lifecycle disconnect does not.
Fingerprinting
Each device is checked against three things: an offline OUI →
vendor lookup, the built-in rogue-device
signatures (matched against vendor and hostname), and whether its
MAC is locally administered — the U/L bit set,
indicating a randomized or spoofed address rather than one assigned by
a hardware vendor. Extend the vendor table and signatures with your own
vendor_file: / rogue_signatures_file: in
config — both are merged with the packaged data, never replace it.
Commands
| Command | Purpose |
|---|---|
lanfence scan | One-time active ARP scan; prints the device table and any findings. |
lanfence monitor | Continuous: active sweeps on an interval plus passive ARP sniffing, dispatching alerts as findings occur, until Ctrl+C. |
lanfence report | Summarize device events and findings from the database over a window (--since 24h). |
lanfence allow [MAC] | Trust a device (--name, --notes); findings about it are downgraded to info. --list shows entries, --remove untrusts one, no arguments also lists. |
lanfence check | Verify the host — root, scapy, interface/subnet auto-detection, database writability — before a run. |
lanfence link | Symlink the launcher onto root's PATH (default /usr/local/bin, --bin-dir to choose) so sudo lanfence works. Re-execs under sudo itself; --remove undoes it, --no-sudo skips the escalation. |
lanfence upgrade | Check PyPI directly for a newer release and run the right upgrade command for how this copy was installed. --check reports without installing. |
scan / monitor options
| Option | Meaning |
|---|---|
--interface, -i | Network interface to scan/monitor. Default: auto-detect. |
--subnet, -s | CIDR subnet to sweep. Default: derived from the interface's own address. |
--interval | (monitor) Active-sweep interval override, in seconds. |
--passive / --no-passive | (monitor) Also passively sniff ARP traffic between sweeps. Default: on. |
--alert | (scan) Dispatch alerts for findings via configured channels. Default: off. |
--alert / --no-alert | (monitor) Dispatch alerts as findings occur. Default: on. |
--format, -f | table or json. Default: table. |
--config, -c FILE | YAML config file. |
--fail-on-findings | (scan / report) Exit non-zero when medium+ findings are present. |
--verbose, -v | Repeatable: -v info logs, -vv debug. |
Usage
# Check the host is ready
lanfence check
# One-time active ARP scan
sudo lanfence scan
sudo lanfence scan --format json | jq .
# Continuous monitoring: active sweeps + passive sniffing, with alerts
sudo lanfence monitor
sudo lanfence monitor --interface eth0 --interval 30
# Trust your own hardware so repeat sightings stay quiet
lanfence allow 52:8a:1c:99:f4:2d --name "Ross's laptop"
lanfence allow --list
lanfence allow --remove 52:8a:1c:99:f4:2d
# Summarize the last day, and fail CI if anything medium+ showed up
lanfence report --since 24h --fail-on-findings
sudo: lanfence: command not found? A
pipx / pip install --user install puts
lanfence in ~/.local/bin, which is not on
root's PATH — sudo resets PATH
to a fixed secure_path. Run lanfence link
once — it re-execs under sudo and symlinks the launcher
into /usr/local/bin, so sudo lanfence then
just works. (Or use sudo "$(which lanfence)" … ad
hoc.)
Exit codes
lanfence scan --fail-on-findings and
lanfence report --fail-on-findings exit based on the
highest finding severity in the result:
| Highest severity | Exit code |
|---|---|
| none / info | 0 |
| medium | 10 |
| high | 20 |
Without the flag, both commands always exit 0 on a
completed run.
10 built-in rogue-device signatures
Heuristics, not proof — a MAC vendor prefix or hostname is trivially spoofed by anyone who wants to hide, so a match here is a lead to investigate, not a verdict. Alongside these keyword signatures, any device with no vendor OUI match and a locally administered MAC (the U/L bit set — common for MAC-randomization privacy features, VMs/containers, or spoofed hardware) is flagged as locally_administered_mac, severity info.
| Signal | Category | Severity | Why it matters |
|---|---|---|---|
| Vendor contains "espressif" | esp32_esp8266 |
medium |
ESP32/ESP8266 Wi-Fi chipset. Extremely common in legitimate IoT (smart plugs, sensors), but also the basis for cheap DIY hidden cameras, rogue access points, and ESP32-based Wi-Fi implants. |
| Vendor contains "raspberry pi" | raspberry_pi |
info |
Widely used for entirely legitimate purposes, but also the common hardware basis for network-tap / rogue-implant projects (P4wnP1, home-built taps). |
| Vendor contains "asix electronics" | usb_ethernet_gadget |
medium |
USB-to-Ethernet chipset used both by ordinary dongles and by commercial BadUSB / implant tools (Bash Bunny, LAN Turtle, O.MG cable) presenting as a wired adapter. |
| Hostname contains "pwnagotchi" | pwnagotchi |
high |
Pwnagotchi, an offensive Wi-Fi handshake-capture tool. Its default hostname is distinctive; a real one on your network is not accidental. |
| Hostname contains "bashbunny" | bash_bunny |
high |
Hak5 Bash Bunny, a USB attack platform that can present itself as a network adapter once plugged into a host. |
| Hostname contains "lanturtle" | lan_turtle |
high |
Hak5 LAN Turtle, a covert USB-Ethernet implant designed to be left plugged into a network for remote access. |
| Hostname contains "pineapple" | wifi_pineapple |
high |
Hak5 WiFi Pineapple, a rogue-access-point / Wi-Fi auditing platform. Legitimate on an authorized pentest; otherwise a strong signal of an unauthorized AP. |
| Hostname contains "flipper" | flipper_zero |
medium |
Flipper Zero, a multi-tool device that can also appear on the network via its Wi-Fi dev board add-on. |
| Hostname contains "omg-cable" | omg_cable |
high |
An O.MG Cable — a USB cable with an embedded implant that can expose a Wi-Fi access point / network interface for remote control. |
| Hostname contains "esp32" | esp32_esp8266 |
info |
Suggests an ESP32-based device — the weaker, hostname-only counterpart to the vendor-based ESP32 signature. |
Extend or override these with your own rogue_signatures_file:
(same YAML shape as lanfence/data/rogue_signatures.yaml,
with vendor_keywords / hostname_keywords lists)
and vendor_file: (same tab-separated shape as
lanfence/data/oui_vendors.txt) in config — both are merged
with the packaged data.
Allowlist
lanfence allow <mac> --name X registers a device you
trust. When that device next produces a new_device or
reappeared event, its finding's severity is downgraded to
info and the title and rationale note that it's
allowlisted — the finding still appears, it just stops being loud.
lanfence allow --list shows current entries;
lanfence allow --remove <mac> untrusts one. The
allowlist lives at allowlist_file (default
~/.config/lanfence/allowlist.yaml) and is written
atomically.
Alerting
Findings can be dispatched to up to three channels, gated by
alerts.min_severity (default medium):
syslog (local socket, configurable facility),
email (SMTP, with or without TLS) and a
webhook (HTTP POST to a URL you choose).
lanfence monitor dispatches alerts by default as findings
occur (--no-alert to disable); lanfence scan
only dispatches when you pass --alert. The only network
destinations LAN Fence ever contacts are the ones you
configure here.
Configuration
Optional YAML config, passed with --config lanfence.yaml
to any command. Every field is optional and has a sensible default.
scan:
interface: null # null = auto-detect
subnet: null # null = derive from the interface's own address
scan_interval_seconds: 60 # how often `monitor` repeats an active sweep
active_scan_timeout_seconds: 3
passive: true # also sniff ARP traffic between sweeps
resolve_hostnames: true # try reverse DNS for each device
dns_timeout_seconds: 1
alerts:
min_severity: medium # info | medium | high - dispatch threshold
syslog:
enabled: false
address: /dev/log
facility: user
email:
enabled: false
smtp_host: localhost
smtp_port: 587
use_tls: true
username: null
password: null
from_addr: null
to_addrs: []
webhook:
enabled: false
url: null
timeout_seconds: 5
db_path: ~/.local/share/lanfence/lanfence.db
allowlist_file: ~/.config/lanfence/allowlist.yaml
vendor_file: null # extra OUI table, merged with the packaged one
rogue_signatures_file: null # extra signatures, merged with the packaged ones
Report & JSON structure
lanfence scan --format json prints one
ScanResult for that sweep; lanfence report
--format json --since 24h prints the events and findings
accumulated across a time window from the database:
{
"started_at": "...", "ended_at": "...",
"interface": "eth0", "subnet": "192.168.1.0/24",
"mode": "active | passive | active+passive",
"devices": [
{ "mac", "ip", "hostname", "vendor", "status: online|offline",
"first_seen", "last_seen", "allowlisted", "allowlist_name",
"fingerprints" }
],
"events": [
{ "mac", "event_type: new_device|reappeared|disconnected",
"timestamp", "ip", "hostname" }
],
"findings": [
{ "mac", "title", "severity: info|medium|high",
"rationale", "recommendation", "evidence" }
],
"errors": []
}
All device- and finding-supplied text (hostnames, vendor strings, evidence lines) is length-bounded and stripped of control characters before it reaches a report, a console line or an alert, so a hostile device on the network cannot inject terminal escapes or oversized fields.
Third-party dependencies
LAN Fence does not vendor third-party source code. It depends on a small set of Python libraries.
| Package | Licence | Notes |
|---|---|---|
typer | MIT | CLI framework |
pydantic | MIT | data models |
PyYAML | MIT | config, allowlist and signatures parsing |
rich | MIT | console rendering |
scapy | GPL-2.0-only | optional (lanfence[scan]) — only needed for scan/monitor's raw ARP send/receive; imported as an ordinary, unmodified dependency, never vendored |
pytest | MIT | dev only |
See Security & trust for the note
on distributing something built on LAN Fence alongside GPL-licensed
scapy.
Scope & non-goals
- CLI only — no web dashboard.
- Observation only: LAN Fence sends nothing beyond a standard ARP "who-has" request. It never joins, deauthenticates, spoofs, blocks or throttles another device.
- No offensive capability of any kind, by design.
- Cannot prove a device is malicious, or that a MAC address is genuine — see the honest limit on Security & trust.
See Contributing for how to propose changes, and Security & trust for how to report vulnerabilities.