{"id":49283410,"url":"https://github.com/kahoon/netmon","last_synced_at":"2026-04-25T20:02:55.683Z","repository":{"id":350495552,"uuid":"1206408022","full_name":"kahoon/netmon","owner":"kahoon","description":"Linux network appliance monitor for DNS and Tailscale health, with change-driven notifications, tracing, and runtime stats.","archived":false,"fork":false,"pushed_at":"2026-04-24T23:23:12.000Z","size":1621,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T01:27:36.336Z","etag":null,"topics":["dns","linux","network","ntfy","pihole","security","tailscale","unbound"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kahoon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-09T22:13:55.000Z","updated_at":"2026-04-24T23:19:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kahoon/netmon","commit_stats":null,"previous_names":["kahoon/netmon"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/kahoon/netmon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kahoon%2Fnetmon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kahoon%2Fnetmon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kahoon%2Fnetmon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kahoon%2Fnetmon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kahoon","download_url":"https://codeload.github.com/kahoon/netmon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kahoon%2Fnetmon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32274987,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"ssl_error","status_checked_at":"2026-04-25T18:29:32.149Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dns","linux","network","ntfy","pihole","security","tailscale","unbound"],"created_at":"2026-04-25T20:02:54.970Z","updated_at":"2026-04-25T20:02:55.672Z","avatar_url":"https://github.com/kahoon.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# netmon\n\nA focused Linux network health monitor for a home network appliance. It watches\none interface, runs collectors for DNS health (upstream resolvers, local Unbound,\nand Pi-hole), Tailscale connectivity, and listener invariants. It notifies only\nwhen something meaningful changes.\n\nThe scope is deliberate. Rather than a general-purpose agent, netmon reconciles a\nspecific set of invariants that matter for a DNS/VPN appliance and stays quiet\nwhen those invariants hold. Temporary address churn, brief resolver hiccups, and\ntransient IPv6 state do not generate noise. Only effective health transitions do.\n\n## Dashboard\n\n`netmonctl top` opens a full-screen live dashboard.\n\n![netmonctl top](assets/top.jpg)\n\nThe header bar shows overall health, observed public IPs, daemon version, and\nuptime. The left column lists all health checks with current severity. The right\ncolumn shows Pi-hole state (DNS probes, latency windows with trend arrows,\nupstream config, gravity freshness, query counters), and Tailscale state \n(connectivity, peers, and exit-node status). Press `r` to refresh, `q` to quit.\n\n## Design\n\n**Narrow alert model.** The monitor evaluates a fixed set of invariants specific\nto this appliance class. It distinguishes CRIT, WARN, and INFO transitions but\nnotifies only on effective health changes, not on every refresh tick or netlink\nevent. This keeps mobile notifications short and actionable rather than a stream\nof churn.\n\n**Debounced, coalesced scheduler.** Collectors run on independent schedules and\ncan also be triggered by netlink events. Link/address/route bursts are debounced\nover 8 seconds into a single settled refresh. Redundant work is coalesced. If a\nrefresh is already pending, a new trigger reschedules rather than stacks. This is\nbacked by [`pending`](https://github.com/kahoon/pending), a purpose-built task\nscheduler with full lifecycle telemetry, written alongside this project.\n\n**Clean API boundary.** The daemon exposes all state through a Connect RPC API\nover a Unix domain socket and HTTP/2 streaming. The CLI is a thin client with no \nshared memory, no files, no side-channel state. This boundary makes the daemon \nindependently introspectable and testable without touching the CLI layer.\n\n**Three-layer observability.** The streaming API is split by audience:\n\n- `watch status`: operator stream, quiet, only emits on effective health changes\n- `watch checks`: check-level transitions, which check moved and from what severity\n- `watch tasks`: full scheduler telemetry for debugging and development\n\nKeeping these separate avoids mixing operator-facing health state with internal\nactivity. You can watch the scheduler make decisions in real time through `watch\ntasks` without polluting the operator stream.\n\n**Bounded, fixed-size data structures.** DNS latency windows use a ring buffer\nfrom [`ring`](https://github.com/kahoon/ring), a companion package, keeping trend\nanalysis fixed-size with no steady-state slice growth. Task history for `watch\ntasks` replay is backed by the same structure.\n\n**Notification design.** Notifications include only changed non-OK checks.\nRecoveries are reported briefly and healthy checks are omitted. The notification\nhost is resolved through a dedicated fallback resolver rather than the local\nsystem resolver, avoiding the circular failure where a DNS outage also breaks\nthe ability to send the alert.\n\n## What It Monitors\n\n**Interface \u0026 listeners** Operational state, expected IPv6 ULA presence, at\nleast one usable GUA (tentative and deprecated addresses excluded), and correct\nTCP/UDP listener coverage on ports 53 and 5335.\n\n**Upstream DNS** Direct `NS .` probes to pinned root servers, recursive\ncorrectness checks against those same targets, and public IPv4/IPv6 observation\nvia DNS-based provider chains with deterministic fallback.\n\n**Local DNS stack** DNSSEC validation directly against Unbound on\n`127.0.0.1:5335` (one positive and one negative query), Pi-hole DNS probes over\nIPv4 and IPv6, upstream configuration match, blocking state, and gravity\nfreshness.\n\n**Tailscale** Backend state, authentication, and tailnet connectivity. Peer\ncounts, exit-node status, and advertised routes are collected for operator\nvisibility.\n\n## Alert Severity\n\n- `CRIT` interface down, ULA missing, port 53 uncovered, Pi-hole DNS failing,\n  Pi-hole disabled or misconfigured, Tailscale disconnected\n- `WARN` no usable IPv6 GUA, external DNS degraded, DNSSEC validation degraded,\n  gravity stale, port 5335 exposed on a non-loopback address\n- `INFO` recovery from a previous degraded condition\n\nIf both IPv4 DNS probes fail, or both IPv6 DNS probes fail, the monitor reports `CRIT`.\n\n## CLI\n\n`netmonctl` talks to the daemon over the Unix domain socket:\n\n```bash\nnetmonctl top                           # live dashboard\nnetmonctl status                        # current health summary\nnetmonctl watch [status|tasks|checks]   # live streams\nnetmonctl trace [-scope all|interface|listeners|upstream|unbound|pihole|tailscale]\nnetmonctl checks [-all]\nnetmonctl state [-json]\nnetmonctl stats [-json]\nnetmonctl info\nnetmonctl refresh [-scope ...]\nnetmonctl set debug-logging on|off\nnetmonctl set runtime-stats-interval 30m\nnetmonctl help [command]\n```\n\nUse `-socket` on any command to override the default socket path\n(`/run/netmon/netmond.sock`).\n\n### Observability streams\n\n**`watch status`** is the operator-facing stream. It sends the current health\nview immediately on connect, then pushes updates only when effective health\nchanges. Quiet on a stable system, immediate when something matters.\n\n**`watch checks`** sits one level deeper. It replays recent check transitions\nthen stays live, showing each check that moved, from what severity to what,\nwith the current summary and detail.\n\n**`watch tasks`** is the scheduler-facing stream. It exposes the full\n[`pending`](https://github.com/kahoon/pending) task lifecycle: scheduled,\nrescheduled, executing, executed, cancelled, and failed, with task IDs, delays, and\ndurations. New subscribers receive a replay of recent history from a bounded\n[`ring`](https://github.com/kahoon/ring) buffer before live events continue.\nThis makes the daemon's internal scheduling decisions inspectable in real time,\nnot just inferred from logs.\n\n**`trace`** runs a bounded traced refresh and streams the causal path end to end.\nUnlike `watch`, it triggers the refresh itself and exits when that work completes.\nUseful for answering: which collectors ran, which probes succeeded or failed, how\nlong each stage took, whether a notification was sent or suppressed.\n\nExample trace output:\n\n```text\n[2026-04-17T16:02:11-04:00] trace_started        trace started scope=upstream\n[2026-04-17T16:02:11-04:00] collector_started    collector started collector=upstream\n[2026-04-17T16:02:11-04:00] probe_result         family=ipv4 latency=12.4ms status=ok target=e.root-servers.net.\n[2026-04-17T16:02:11-04:00] probe_result         family=ipv6 latency=15.1ms status=ok target=j.root-servers.net.\n[2026-04-17T16:02:11-04:00] collector_finished   collector=upstream duration=58.3ms\n[2026-04-17T16:02:11-04:00] notification_skipped reason=no effective changes\n[2026-04-17T16:02:11-04:00] trace_completed      duration=59.0ms scope=upstream\n```\n\n## Configuration\n\nRequired:\n\n- `NTFY_TOPIC` ntfy.sh topic for notifications\n\nRecommended:\n\n- `EXPECTED_ULA` exact static ULA expected on the monitored interface\n  (e.g. `fd8f:bd66:6363:1::15`). If unset, the ULA check is skipped.\n\nOptional:\n\n- `MONITOR_IF` interface to monitor (default: `eno1`)\n- `NTFY_HOST` notification host (default: `ntfy.sh`)\n- `NTFY_RESOLVER` resolver used only for the notification host (default: `9.9.9.9:53`)\n- `RPC_SOCKET_PATH` Unix socket path (default: `/run/netmon/netmond.sock`)\n- `PIHOLE_PASSWORD` Pi-hole API password for session token\n- `DEBUG_EVENTS` log raw netlink events (default: `false`)\n- `RUNTIME_STATS_INTERVAL` Go runtime stats interval, `0` to disable (default: `168h`)\n\n## Build \u0026 Deploy\n\n```bash\n# Build\ngo build ./cmd/netmond\ngo build ./cmd/netmonctl\n\n# Cross-compile from macOS\nGOOS=linux GOARCH=amd64 go build ./cmd/netmond\n\n# Makefile targets\nmake fmt\nmake generate\nmake test\nmake build\nmake build-linux\nmake VERSION=v0.1.0 build-linux\n```\n\nBuilds stamp the binary with version, commit, and build time metadata.\n\n**systemd unit:**\n\n```ini\n[Unit]\nDescription=netmon network health monitor\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nUser=root\nRuntimeDirectory=netmon\nEnvironmentFile=/etc/default/netmon\nExecStart=/usr/sbin/netmond\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n```\n\n**Install:**\n\n```bash\nsudo install -m 0755 ./netmond /usr/sbin/netmond\nsudo install -m 0755 ./netmonctl /usr/sbin/netmonctl\nsudo install -m 0644 ./contrib/netmon.env.example /etc/default/netmon\nsudo install -m 0644 ./contrib/netmon.service /etc/systemd/system/netmon.service\nsudo systemctl daemon-reload\n```\n\nEdit `/etc/default/netmon` for your environment before starting the service, then:\n\n```bash\nsudo systemctl enable --now netmon\n```\n\n## How It Works\n\n1. Look up the monitored interface and subscribe to netlink link, address, and\n   route events.\n2. Debounce interface bursts over 8 seconds using\n   [`pending`](https://github.com/kahoon/pending).\n3. Run collectors on independent schedules: interface (10m), listeners (10m),\n   upstream DNS (5m), Unbound (5m), Pi-hole (5m), Tailscale (5m).\n4. Evaluate a fixed check set against the latest collected state.\n5. Notify only if a check changed or the observed public IP changed.\n\nThe daemon is Linux-specific. It reads listener state from procfs, and Tailscale\nstate via `tailscale status --json` and `tailscale debug prefs`. Protobuf and\nConnect stubs are generated with Buf from `proto/netmon/v1/netmon.proto`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkahoon%2Fnetmon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkahoon%2Fnetmon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkahoon%2Fnetmon/lists"}