clawbot 51d2bd24df
check / check (push) Waiting to run
Stamp the git tag or short commit into the binary (closes #10)
rtnetmon had no version. main.Version is now set at link time and logged
in the first startup line. The Dockerfile takes it from the VERSION build
argument when one is given, otherwise from `git describe --tags --always`
of the .git the build context now carries, and fails the build if the
context carries .git and no version comes out. .dockerignore follows the
canonical copy: .git is sent, .git/config, which can hold a credential,
is not.

Model: opus-5-5
2026-10-02 06:24:22 +00:00
2025-05-16 05:28:39 -07:00

rtnetmon

rtnetmon is a WTFPL-licensed Go terminal (CLI/TUI) network monitor by @sneak that shows real-time network health, packet loss, and latency across one or two network interfaces on Linux and macOS.

Overview

rtnetmon is a terminal-based network monitoring tool that provides real-time visibility into network health, packet loss, and latency across one or two network interfaces simultaneously. It runs on Linux and macOS and uses a terminal dashboard interface.

Features

  • Interface Monitoring: Monitor one or two network interfaces simultaneously
  • Real-time Updates: Live dashboard with sub-second updates
  • Comprehensive Metrics:
    • ICMP reachability tests
    • Packet loss percentage
    • TCP connection latency
    • Interface health status
  • Visual Indicators: Color-coded status, spinners, and meters for quick status assessment
    • Packet loss meter uses reverse coloring (empty/green = good, full/red = bad)
  • Detailed Logging: Optional logging to file for debugging and analysis

Requirements

  • Linux or macOS
  • Go 1.21 or later
  • Root/sudo access (for raw ICMP packets)
  • ping command available in PATH
  • On Linux: ip (with /proc/net/route as a fallback)
  • On macOS: netstat

Platform support and interface detection

rtnetmon monitors either one or two interfaces, chosen automatically for the platform it runs on. When only one interface is detected, the dashboard shows a single pane.

Linux. The two named interfaces (--ifaceA/--ifaceB, default gu0/backhaul0) are used when both exist — this is the original dual-bridge setup, unchanged. When neither exists, the single default-route interface is monitored instead.

macOS. The physical internet interface is found from the default route. When a VPN client is running (Mullvad and similar clients create a utun tunnel that carries a default route or holds a routable address), that tunnel is monitored as the primary pane alongside the physical interface. With no VPN running, only the physical interface is monitored. Interface names are detected on macOS; the --ifaceA/--ifaceB flags are not used there, but --labelA/--labelB still set the pane labels.

Supported matrix

OS Interfaces monitored
Linux gu0 + backhaul0 when both exist (two panes)
Linux the single default-route interface otherwise (one pane)
macOS VPN tunnel + physical default-route interface (two panes)
macOS the physical default-route interface with no VPN (one pane)

Anything outside this matrix — on Linux, only one of the named pair present, or no/multiple default routes when neither is present; on macOS, no default route or more than one physical default route — exits with a clear error.

When the non-VPN physical gateway is a Starlink dish, two extra lines appear under that pane: dish state, uptime, obstruction and alert count on the first, and pop-ping latency and drop rate with downlink/uplink throughput on the second. They turn red when the dish is not connected or an alert is active.

Detection is a TCP connect to the dish's fixed local endpoint, 192.168.100.1:9200, made over the physical interface (the same binding the latency probes use). The dish sits behind the Starlink router, so this is not a gateway-address check. rtnetmon probes once a minute until a dish answers and then reads its status every few seconds; when no dish answers, nothing is drawn and no status is fetched. The status comes from the dish's local get_status gRPC call.

Installation

git clone https://git.eeqj.de/sneak/rtnetmon.git
cd rtnetmon
make build   # produces ./bin/rtnetmon

Usage

sudo ./bin/rtnetmon --ifaceA eth0 --labelA "Primary WAN" --ifaceB wlan0 --labelB "Backup WiFi"

Command Line Options

  • --ifaceA: Primary network interface (default: "gu0")
  • --labelA: Label for primary interface (default: "gu LAN - VPN outbound")
  • --ifaceB: Secondary network interface (default: "backhaul0")
  • --labelB: Label for secondary interface (default: "Cox cable direct")
  • --hosts: Comma-separated list of hosts to monitor
  • --logfile: Path to log file (default: "/tmp/rtnetmon.log")

Keyboard Controls

  • q or Ctrl+C: Quit the application

Project Structure

rtnetmon/
├── cmd/rtnetmon/       # main entry point (thin: calls internal/cli)
│   └── main.go
├── internal/
│   ├── cli/            # command-line interface using Cobra
│   │   └── root.go
│   ├── netdetect/      # per-platform interface/route detection and selection
│   │   ├── netdetect.go     # selection logic and route parsers (pure)
│   │   ├── routes_linux.go  # Linux default-route query (build-tagged)
│   │   └── routes_darwin.go # macOS default-route query (build-tagged)
│   ├── starlink/       # Starlink dish detection and status
│   │   ├── starlink.go      # Status, Client interface, pure Render
│   │   ├── client.go        # real gRPC client (get_status)
│   │   └── pb/              # generated bindings for the get_status RPC
│   └── monitor/        # core monitoring functionality
│       ├── monitor.go       # monitor types, probes, logging
│       ├── loops.go         # monitoring loops (reachability, loss, TCP)
│       ├── styles.go        # terminal color styles
│       ├── ui.go            # user interface rendering
│       ├── dial_linux.go    # TCP source binding (build-tagged)
│       └── dial_darwin.go   # TCP IP_BOUND_IF binding (build-tagged)
├── script/             # Scripts to Rule Them All entrypoints
├── .gitea/workflows/   # CI (runs script/cibuild)
├── Dockerfile          # lint + test gate phases and the build
├── Makefile            # thin shims over script/
├── .golangci.yml       # vendored linter config
├── go.mod / go.sum
└── README.md

Development

make check       # run test, lint, and fmt-check (the default target)
make build       # build ./bin/rtnetmon
make run         # build, then run ./bin/rtnetmon locally
make dev         # go run ./cmd/rtnetmon
make test        # run the test suite (test phase of the Dockerfile)
make lint        # run golangci-lint (lint phase of the Dockerfile)
make fmt         # format Go code (writes)
make fmt-check   # verify formatting (read-only)
make deps        # go mod download + go mod tidy
make docker      # build the Docker image
make cibuild     # bootstrap, check, and build the image (run by CI)
make bootstrap   # install dependencies idempotently (git, make, go)
make setup       # bootstrap plus install the git pre-commit hook
make hooks       # install the git pre-commit hook
make clean       # remove build artifacts

Linting and testing run in Docker so results do not depend on host tooling; make lint, make test, make docker, and make cibuild therefore require a Docker daemon. make check must be green before committing, and the pre-commit hook (make hooks) runs it.

Entrypoints

This repository adheres to the Scripts to Rule Them All standard: normalized scripts in script/ are the entrypoints for the development workflow, and the Makefile targets are thin shims that call them.

  • script/bootstrap — install dependencies idempotently (git, make, go).
  • script/setup — bootstrap plus install the git pre-commit hook.
  • script/projectname — output the project name.
  • script/test — run the test suite as the Dockerfile test phase.
  • script/lint — run golangci-lint as the Dockerfile lint phase.
  • script/fmt — format Go code (host).
  • script/fmt-check — verify formatting (host, read-only).
  • script/check — run test, lint, and fmt-check.
  • script/docker — build the Docker image.
  • script/cibuild — bootstrap, check, and build the image; run by CI.
  • script/precommit — run by the pre-commit hook (go mod tidy check + check).
  • script/install-precommit — install the pre-commit hook.

Using the Monitor API

The monitor package provides an object-oriented API for programmatic use:

import "git.eeqj.de/sneak/rtnetmon/internal/monitor"

// Create a new monitor for one or two interfaces
mon := monitor.NewMonitor([]monitor.IfaceSpec{
    {Name: "eth0", Label: "Primary"},
    {Name: "wlan0", Label: "Backup"},
}, "/tmp/monitor.log")

// Configure timing parameters (optional - defaults are sensible)
mon.ICMPTimeout = 1 * time.Second
mon.PacketLossPings = 10
mon.PacketLossPeriod = 10 * time.Second

// Add hosts to monitor
mon.AddReachabilityHost("8.8.8.8")
mon.AddReachabilityHost("google.com")

mon.AddPacketLossHost("github.com")
mon.AddPacketLossHost("8.8.8.8")

mon.AddTCPHost("google.com:443")
mon.AddTCPHost("github.com:443")

// Run the monitor
ctx := context.Background()
if err := mon.Run(ctx); err != nil {
    log.Fatal(err)
}

License

WTFPL - Do What The Fuck You Want To Public License

Author

sneak@sneak.berlin

S
Description
No description provided
Readme
193 KiB
Languages
Go 98.1%
Makefile 1.9%