A single-binary home-lab network survey and inventory tool. It discovers every device on your LAN, keeps a curated inventory that scans never clobber, runs OS updates across your Linux boxes over SSH, and stores the occasional credential encrypted — all from one static Go binary with an embedded web UI.
Built for a mixed lab — a UniFi gateway, single-board computers, a NAS, a few servers, IoT gadgets, and assorted microcontrollers — where SSH-based and agentless discovery both matter.
Screenshots use a synthetic demo dataset, not a real network.
Home labs rot: software goes out of date, services get stood up and
forgotten, and nobody remembers what 192.168.1.139 actually is.
netsurvey gives you one always-current picture plus the levers to act on it.
- Agentless discovery, multiple sources merged into one registry:
- Ping sweep (unprivileged ICMP, TCP fallback) — also primes the ARP cache so it finds devices that drop pings.
- ARP/neighbor table read — catches everything the sweep touched.
- mDNS / DNS-SD — names and service types (printers, speakers, the
_ssh._tcpboxes) even when port scanning is blocked. - nmap (optional, auto-detected) — port/service detection on full scans.
- UniFi Dream Router API — the authoritative client list, including devices that are asleep during the scan (via the all-known-clients API).
- SSH enrichment — OS, kernel, uptime, package counts, pending updates, listening ports, docker containers, for any host you enroll.
- Inventory with a clean web UI: filterable device table, per-device detail, a staleness/updates dashboard, and curated fields (name, location, owner, tags, presence mode) that re-scans never overwrite.
- SSH key bootstrap + update runner: push a managed key to a host, then
run
apt/dnfupdates with dry-run, concurrency limits, and live per-host logs in the browser. - Minimal encrypted secret store (
age): stash the odd web-UI password or API key, unlocked per-session with a passphrase, never written in clear. - Remote command execution: presets + a free-form box per host, gated by login + CSRF + an unlocked secret store, with every run logged.
Device inventory — every device merged from all sources, with curated names, type pills, and last-seen status:
Device detail — discovered identity, nmap-detected services with versions, mDNS advertisements, the remote-command panel, the curated metadata form, and per-device encrypted secrets:
Requires nothing at runtime (pure-Go static binary). To build:
brew install go # or your platform's Go ≥ 1.26
make build # produces ./netsurvey
make cross # static binaries for every lab arch in ./distnmap is optional but recommended for port/service detection:
brew install nmap (macOS) or apt install nmap (Debian/Pi).
./netsurvey scan # discover the LAN, print a table
./netsurvey serve # web UI at http://127.0.0.1:8472On first visit the web UI asks you to set a password (argon2id). Trigger a scan from the dashboard, then start naming and tagging devices.
./netsurvey bootstrap pi@192.168.1.50 --install-sudoers
./netsurvey scan # now collects OS/package facts for it
./netsurvey update --hosts pi5 --dry-run # preview pending updates
./netsurvey update --hosts pi5 --apply # apply themBootstrap generates a managed homelab-ops ed25519 key on first run, asks
for the host password once (never stored), installs the key, verifies it,
and optionally installs a tightly-scoped sudoers.d rule for the update
commands. Host keys are trusted on first use (known_hosts in the data dir).
Once a host is bootstrapped, its detail page gains a Remote command panel:
one-click presets (uptime, df -h, docker ps, failed units, journal
tail, …) plus a free-form command box. Output appears inline and every run is
logged. The CLI equivalent:
./netsurvey exec pi5 'docker logs --tail 50 pihole'
./netsurvey exec 192.168.1.50 'systemctl restart node_exporter'Because this is effectively remote code execution into your hosts, the web panel is guarded by three factors: a logged-in session, a CSRF token, and an unlocked secret store this session (a deliberate second factor — a stolen/idle session alone can't run commands). Each command runs under a per-host timeout and is recorded with its output.
In the web UI under Settings → UniFi, follow the in-app steps to create a local API key on the UDR, paste it in, and Test & save. netsurvey then pulls the live client list plus the long-memory all-known-clients list — which backfills devices that are powered off or asleep during a scan, and is the authoritative source if your network restricts client-to-client traffic.
On a device's detail page, unlock the secret store with a passphrase (the
first one initializes it), then add label/value pairs. Values are age
scrypt-encrypted; the unlocked key lives in memory only and clears after 15
minutes idle. CLI equivalent: netsurvey secret set --device pi5 web-ui.
netsurvey scan [--full] [--no-nmap] [--subnet CIDR] [--json]
netsurvey list [--filter TEXT] [--all] [--json]
netsurvey serve [--listen ADDR] [--scan-interval DUR] [--subnet CIDR]
netsurvey bootstrap <user@host> [--install-sudoers] [--forget]
netsurvey update --hosts a,b | --all-managed [--dry-run|--apply] [--parallel N]
netsurvey exec <device|ip> <command...>
netsurvey secret set|get|ls|rm [--device NAME] [--user U] <label>
netsurvey version
--data-dir DIR (or $NETSURVEY_DATA_DIR) overrides the data directory
(default ~/.local/share/netsurvey), which holds the SQLite database, the
managed SSH key, and known_hosts.
Devices are keyed on MAC. An observation with only an IP resolves through
recent IP history, or becomes a provisional device that's absorbed once a
MAC is learned for that IP. Randomized (phone) MACs are quarantined into an
"ephemeral" bucket so they don't pollute the inventory. Intermittent devices
(ESP32s, etc.) can be marked intermittent so they stop triggering staleness
alerts.
Scans only ever write disc_* fields; your edits live in separate curated
columns and win on display. Re-scanning a device you've named never loses the
name.
- Binds
127.0.0.1by default. Binding to a LAN address (--listen 0.0.0.0:8472) is refused until a UI password is set. - Single-user password (argon2id), in-memory sessions,
SameSite=Strictcookies, CSRF tokens on mutating requests. - SSH host keys are trust-on-first-use; a changed key hard-fails with a
bootstrap --forgethint. - The
sudoersrule bootstrap installs is scoped to the exact apt/dnf update commands, nothing else. - Secrets are
age-encrypted; plaintext is never written to disk.
The Windows desktop and UniFi gear are inventoried but out of scope for the SSH update runner — they show as "unmanaged".
netsurvey keeps all of your network data and credentials in its data
directory (default ~/.local/share/netsurvey/), which is outside this
repository. Nothing sensitive lives in the source tree, and the
.gitignore is written so that even an accidental in-repo data dir (e.g.
--data-dir .) won't be committed.
What's sensitive, and never belongs in version control:
| Artifact | Holds |
|---|---|
netsurvey.db |
MACs, IPs, hostnames, device names/notes, UI password hash, the UniFi API key (settings table), and the age-encrypted secrets blob |
homelab_ops_ed25519 |
the managed SSH private key — it can log into every host you've bootstrapped |
known_hosts |
SSH host fingerprints for your hosts |
config.toml |
optional headless config; may contain the UniFi API key (kept 0600) |
The repo ships config.toml.example (no real values) to document the
format. The embedded internal/oui/data/oui.txt.gz is the only data file
that is committed — it's the public Wireshark vendor database the build
needs, not anything about your network.
Before pushing, a quick sanity check:
git status --ignored # confirm db/keys/config show as ignored, not staged
git ls-files | grep -Ei '\.(db|age|pem|key)$|ed25519|known_hosts|config\.toml$' # should print nothingSee deploy/netsurvey.service (systemd, e.g. on
the NUC or a ZimaBoard) and
deploy/com.netsurvey.plist (macOS launchd).
make test # unit tests (parsers, merge logic, crypto, package managers)
make vetLive-network behavior (real ping/mDNS/SSH/UniFi) isn't unit-tested; the parsers and merge/identity logic are, with fixtures.





