Files
socktop/README.md
jason 0322308896 housekeeping-p2: security, correctness, and performance pass before 1.51 (#39)
* chore: dead-code sweep

- Delete socktop_connector/src/connector.rs: orphaned since 08f248c removed
  'pub mod connector;' during the modularization refactor. Never compiled
  (verified under default, wasm, and workspace feature combos) but shipped
  in the crates.io tarball and contained an outdated copy of the TLS
  verifier — a trap for anyone patching the pinning bug in the dead copy.
- Delete empty socktop/src/ws.rs, tracked editor backup ui/.modal.rs.backup,
  and stray test_thiserror.rs at the repo root.
- Delete the two LEGACY #[allow(dead_code)] process input handlers; the
  header-click render test now exercises the live _with_selection handler
  instead (better coverage of the real path).
- Drop unused sysinfo dependency from the socktop client.
- Replace stale 'temporarily increased for testing' comment on
  COMPRESSION_THRESHOLD (it already held the production value).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(connector): make certificate pinning real; disable Nagle

Security: with --verify-hostname off (the default), the old NoVerify
verifier accepted ANY server certificate — the CA loaded from --tls-ca
was never consulted, so the documented pinning was a no-op and the
connection was trivially MITM-able. Replace it with PinnedCertVerifier:
the presented end-entity cert must be byte-identical to a cert in the
--tls-ca file (any cert in a multi-cert PEM matches, supporting
rotation). Signature validation now uses the ring provider's full
algorithm set instead of a hardcoded 3-scheme list. Empty PEM files
fail fast instead of failing closed per-handshake.

The --verify-hostname path is unchanged (WebPki root-store validation).

Also: the third argument of connect_async_tls_with_config is
tungstenite's disable_nagle flag, not a verification toggle — we were
passing verify_hostname there, leaving Nagle ON for default users. Pass
true unconditionally, and disable Nagle on the plain ws:// path too;
socktop exchanges small request/response frames where Nagle only adds
latency.

Client now consumes the connector via a dual path+version dep so these
fixes are in local builds and CI before the crates.io publish (cargo
strips the path on publish). Connector version -> 1.51.0.

Verified E2E: agent A's cert connects to agent A; agent B's cert
against agent A fails the handshake (the rpi-worker-1 wrong-PEM
scenario); --verify-hostname against a 127.0.0.1 SAN still connects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agent): GPU worker thread, async journalctl, correctness + cache fixes

Lightweight:
- GPU collection moves to a dedicated worker thread that owns the gfxinfo
  handle for the process lifetime. gfxinfo's active_gpu() runs a full NVML
  init/teardown (~20ms, blocking) and we were paying it on the async
  runtime for every collect — measured at ~80% of the agent's entire
  active CPU on a GPU machine. The handle holds Rc<Nvml> (not Send), so a
  thread + mpsc/oneshot channel pair confines it; a zero-total-VRAM reply
  is treated as a dead session (driver reload) and re-probed.
- journalctl now runs via tokio::process instead of blocking one of the
  two runtime workers for the duration of the subprocess.
- TtlCell (state.rs) replaces the four hand-rolled static TTL caches; a
  cached negative result now counts as fresh, so hosts with no matching
  temp sensor or GPU stop rescanning every request. Single lock+clone on
  the GPU cache hit path (was two).

Correctness:
- Process/child CPU times are now microseconds as documented; they were
  milliseconds, rendering 1000x too small next to (correct) thread times.
- Non-Linux per-process CPU%% clamps AFTER dividing by core count; a
  4-cores-busy process on an 8-core box reported 12.5% instead of 50%.
- Journal timestamps are real RFC 3339 UTC plus an additive timestamp_us
  field (sorting is now numeric); the old strings were Debug-formatted
  SystemTime mangled by string replace.
- Partition detection uses /sys/block on Linux: whole-disk filesystems on
  names like nvme0n1 or zram1 are no longer misclassified as partitions.
  One shared parent_disk_name() replaces two inline copies.
- New sampled_at_ms on the metrics payload (additive) records when the
  snapshot was actually collected, so clients can compute exact rates
  across the agent's TTL cache.

Security/robustness:
- key.pem is created 0600 (was umask default 0644, world-readable) and
  pre-1.51 keys are tightened on startup.
- Per-PID detail/journal caches now evict (60s max age, 64 entries max);
  they previously grew without bound under PID-walking clients.
- The two per-PID ws handlers collapse into one generic helper.
- /proc/<pid>/stat parsing unified in one comm-safe module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tui): responsive input, request timeouts, poisoned-stream reconnect

R1 — input latency: the event loop drained input once per iteration, then
slept the whole metrics interval; keys and wheel events queued for up to
500ms (or the full interval at slower rates) and applied in bursts. The
input block is extracted to drain_input() and the tail sleep replaced by
a deadline wait in <=33ms poll slices that handles and repaints input the
moment it arrives. Verified: help modal opens <150ms into a 2000ms tick.

R2 — freeze-proofing: metrics/processes/disks requests had no timeout; a
half-dead connection left ws.next() pending forever and froze the TUI
with no way to quit (raw mode eats Ctrl+C as an unread key event). All
requests now carry a 5s budget.

C3 — desync: replies are matched to requests by order alone, so a timed-
out request's late reply would shift every subsequent reply off by one.
Any timeout now treats the stream as poisoned and goes through the
reconnect flow — a fresh stream is aligned by construction. The modal
endpoints additionally mark process details unsupported (flag resets on
modal close/selection change) so a detail-less agent doesn't cause a
reconnect loop. While disconnected the fetch path idles: recovery belongs
to the manual/auto retry paths instead of 5s-timeout hammering.

C7 — fit::truncate_middle_cols replaces util::truncate_middle: display-
width aware and char-boundary safe; the byte-slicing version panicked the
draw loop on non-ASCII device names.

Verified live: agent kill -9 mid-session -> error modal in <3s, q exits
while disconnected, r reconnects and resumes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: version 1.51.0, path-dep the wasm examples, README notes

- socktop, socktop_agent, socktop_connector -> 1.51.0.
- socktop_wasm_test and zellij_socktop_plugin consume the in-repo
  connector via path deps so wasm-feature API drift is caught at PR time
  instead of after publish. Immediately proved out: the wasm requests
  module needed the new sampled_at_ms field, invisible to native builds.
- zellij plugin gains the standalone [workspace] marker (it could not be
  cargo-checked in-tree at all before). NOTE: its lib.rs has pre-existing
  compile errors unrelated to the connector (static mut STATE conflicts
  with register_plugin!, missing BTreeMap import) — needs its own rework,
  out of scope here.
- README: sampled_at_ms in the example payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): restore Agent Update Required flow, command field, axis alignment

Fixes from Jason's hands-on verification of the branch:

1. Old-agent messaging regression (this branch): a detail-request timeout
   went through the loud poison/reconnect flow, burying the ProcessDetails
   modal's 'Agent Update Required' message under a connection-error modal.
   Old agents IGNORE unknown messages (no late reply, no desync), so the
   optional per-PID endpoints now use quiet_reconnect(): swap the stream
   silently (still safe against merely-slow agents) and let the modal show
   its message. Only a failed reconnect surfaces loudly. Verified against
   a real v1.40.0 agent: message shows, session stays healthy.

2. Draw starvation (this branch): an agent that never answers get_metrics
   put the loop in fetch->timeout->poison->restart cycles that never
   reached the draw call — permanently blank TUI. The iteration now paints
   before fetching, and a second consecutive metrics timeout trips a
   circuit breaker: persistent 'Agent is not responding' error, recovery
   left to the manual/30s retry paths. Verified against a 0.9-era agent.

3. Command & Details pane blank (pre-existing on master): the minimal-
   refresh optimization dropped cmd/exe/cwd from the detail endpoint's
   ProcessRefreshKind, so process.cmd() had nothing to return. Restored
   with UpdateKind::OnlyIfNotSet — immutable values, read once per PID.
   Regression test added; journal E2E re-verified (100 entries render).

4. Scatter-plot axis misalignment: Y labels used a fixed 4-char field from
   the era when CPU times were 1000x too small; honest millisecond values
   (e.g. 136114) blew through it. Labels now right-align to the widest
   value per frame and X labels/titles share the dynamic padding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: journal access notice, 1.60.0, install script, changelog, riscv protoc fallback

- Journal pane now distinguishes 'no entries' from 'no journal access':
  journalctl exits 0 with empty output when the agent's user simply can't
  see the target's entries (demo mode / user-run agents), explaining
  itself only on stderr. The agent forwards that hint as an additive
  JournalResponse.notice and the client renders it with practical advice.
  Verified E2E via a stub journalctl emulating the unprivileged case.
- Version 1.60.0 across all crates (1.51 would read fine, but the repo's
  scheme is 1.40/1.50/…, and a literal 1.6.0 would sort BELOW 1.50.0 in
  semver). All user-facing version strings already come from
  CARGO_PKG_VERSION — a stale binary was the only way to see an old one.
- scripts/install.sh: build-from-source install/upgrade for the test
  fleet (Linux + macOS). Detects in-repo checkouts, installs rustup when
  missing, replaces a systemd socktop-agent service binary in place and
  restarts it, requires system protoc on riscv64.
- build.rs (agent + connector): fall back to $PROTOC / PATH when
  protoc-bin-vendored has no binary for the host (riscv64) — native SBC
  builds previously panicked in the build script.
- CHANGELOG.md covering v1.50.0 -> 1.60.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: add notice field to cache test initializer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: untrack zellij plugin build dir; installer updates all PATH copies

- Remove zellij_socktop_plugin/target from git (3,577 files committed by
  accident in bf6ac87): the root .gitignore anchors /target to the repo
  root, so the standalone plugin's own build dir wasn't covered. Ignore
  target/ at any depth (also fixes the pre-existing
  '/socktop-wasm-test/target' entry, which pointed at a hyphenated path
  that doesn't exist).

- install.sh now updates EVERY copy of socktop/socktop_agent on PATH,
  not just $PREFIX: a stale 'cargo install' in ~/.cargo/bin shadows
  /usr/local/bin on most PATHs, so an install could 'succeed' while
  'socktop --version' kept reporting the old release. Extra copies that
  can't be written are warned about, not fatal, and the script now
  prints which binary is actually active on PATH at the end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(installer): manage the socktop-agent systemd service

Upgrade path (unit already present): NEVER touch the unit file — it is
the operator's config (SSL, tokens, ports live there as Environment=
lines). Only the binary at the unit's own ExecStart path is replaced,
then the service restarts. Flags/args preserved by construction.

Fresh path (no unit): full first-time setup mirroring the deb postinst
and the agent-service docs — create the socktop system user/group and
/var/lib/socktop, install docs/socktop-agent.service (ExecStart rewritten
to wherever this run installed the agent; embedded fallback for old
refs), daemon-reload, enable --now, and print how to turn on TLS/token.

Also: system-level operations get their own sudo decision (SYS_SUDO) —
previously they inherited the PREFIX sudo flag, so a writable --prefix
made the service section run groupadd/systemctl unprivileged and die.
No sudo at all now skips service management with a warning instead of
failing the install.

Both branches dry-run verified with stubbed systemctl/sudo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(installer): don't bind fresh agent services onto occupied ports

The Orange Pi install put the new service straight into a crash-restart
loop: the unit's default --port 3000 collided with a Docker service
already publishing 3000 (Umami; Gitea and friends default there too).
Fresh installs now scan 3000/3001/3010/3231/3232 via ss and configure
the unit on the first free port, warning loudly when 3000 was taken and
printing the resulting ws:// URL. Upgrades still never touch the unit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agent): detect NVIDIA GPUs on distros without the unversioned NVML soname

On Debian and derivatives the NVIDIA driver ships only libnvidia-ml.so.1
(the unversioned symlink belongs to the dev package), and nvml-wrapper's
default init dlopens the unversioned name — so gfxinfo reported 'No GPU
found' on a fully functional RTX A2000 host while nvidia-smi worked
fine. Arch-family distros ship the symlink, which is why the desktop
never showed this.

The GPU worker now falls back to initializing NVML directly with the
versioned soname when gfxinfo's probe fails, collecting name/util/vram
through the same handle-caching path. nvml-wrapper was already in the
tree via gfxinfo — same version, no new build cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agent): box the NVML handle variant (clippy large_enum_variant)

CI clippy runs with -D warnings; Nvml is a large struct next to the
16-byte Box<dyn Gpu> variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(installer): survive self-modification mid-run; sturdier unit detection

Root cause of the mixed-up second install on the A2000 host: when run
from the clone it manages, the script's own git checkout/merge REPLACES
scripts/install.sh while bash is still executing it. Bash reads scripts
lazily by byte offset, so it resumed parsing the NEW file at the OLD
offset and executed an arbitrary tail of it — observed as the fresh-
service path running on a host whose unit already existed: the port scan
saw the still-running old service on 3000 and silently wrote a new unit
on 3001, while enable --now on the already-active service changed
nothing until a manual daemon-reload.

Fix: the whole script now runs inside main(), invoked as
'main "$@"; exit $?' so bash parses everything up front and never
reads the file again after main returns (the exit lives in the same
parse unit — demonstrated necessary: with a bare 'main "$@"' ending,
bash still executed the swapped file's trailing content after main
returned).

Also: unit existence is now checked with 'systemctl cat' instead of
grepping the full list-unit-files output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(installer): use a durable ref in the usage example

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 12:45:10 -07:00

18 KiB
Raw Permalink Blame History

socktop

socktop is a remote system monitor with a rich TUI, inspired by top/btop, talking to a lightweight agent over WebSockets.

  • Linux agent: near-zero CPU when idle (request-driven, no always-on sampler)
  • TUI: smooth graphs, sortable process table, scrollbars, readable colors

socktop.io


Features

  • Remote monitoring via WebSocket (JSON over WS)
  • Optional WSS (TLS): agent autogenerates a selfsigned cert on first run; client pins the cert via --tls-ca/-t
  • TUI built with ratatui
  • CPU
    • Overall sparkline + per-core mini bars
    • Accurate per-process CPU% (Linux /proc deltas), normalized to 0100%
  • Memory/Swap gauges with human units
  • Disks: per-device usage
  • Network: per-interface throughput with sparklines and peak markers
  • Temperatures: CPU (optional)
  • Top processes (top 50)
    • PID, name, CPU%, memory, and memory%
    • Click-to-sort by CPU% or Mem (descending)
    • Scrollbar and mouse/keyboard scrolling
    • Total process count shown in the header
    • Only top-level processes listed (threads hidden) — matches btop/top
  • Optional GPU metrics (can be disabled)
  • Optional auth token for the agent
  • Compact layout for small windows: automatically drops the panes that no longer fit so the CPU graph and per-core bars stay visible (see Compact mode)

Prerequisites: Install Rust (rustup)

Rust is fast, safe, and crossplatform. Installing it will make your machine better. Consider yourself privileged.

Linux/macOS:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# load cargo for this shell
source "$HOME/.cargo/env"
# ensure stable is up to date
rustup update stable
rustc --version
cargo --version
# after install you may need to reload your shell, e.g.:
exec bash   # or: exec zsh / exec fish

Windows (for the brave): install from https://rustup.rs with the MSVC toolchain. Yes, youll need Visual Studio Build Tools. You chose Windows — enjoy the ride.

Raspberry Pi / Ubuntu / PopOS (required for GPU support)

Note: GPU monitoring is only supported on x86_64 and aarch64 (64-bit ARM) platforms. ARMv7 (32-bit) and RISC-V builds do not include GPU support.

For 64-bit systems with GPU support:

sudo apt-get update
sudo apt-get install libdrm-dev libdrm-amdgpu1

For ARMv7 (32-bit Raspberry Pi), build with --no-default-features to disable GPU support:

cargo build --release -p socktop_agent --no-default-features

Additional note for Raspberry Pi users. Please update your system to use the newest kernel available through app, kernel version 6.6+ will use considerably less overall CPU to run the agent. For example on a rpi4 the kernel < 6.6 the agent will consume .8 cpu but on the same hardware on > 6.6 the agent will consume only .2 cpu. (these numbers indicate continuous polling at web socket endpoints, when not in use the usage is 0)


Architecture

Two components:

  1. Agent (remote): small Rust WS server using sysinfo + /proc. It collects metrics only when the client requests them over the WebSocket (request-driven). No background sampling loop.

  2. Client (local): TUI that connects to ws://HOST:PORT/ws (or wss://HOST:PORT/ws when TLS is enabled) and renders updates.


Quick start

  • Build both binaries:
git clone https://github.com/jasonwitty/socktop.git
cd socktop
cargo build --release
  • Start the agent on the target machine (default port 3000):
./target/release/socktop_agent --port 3000
  • Connect with the TUI from your local machine:
./target/release/socktop ws://REMOTE_HOST:3000/ws

Cross-compiling for Raspberry Pi

For Raspberry Pi and other ARM devices, you can cross-compile the agent from a more powerful machine:

Quick demo (no agent setup)

Spin up a temporary local agent on port 3231 and connect automatically:

socktop --demo

Or just run socktop with no arguments and pick the builtin demo entry from the interactive profile list (if you have saved profiles, demo is appended). The demo agent:

  • Runs locally (ws://127.0.0.1:3231/ws)
  • Stops automatically (you'll see "Stopped demo agent on port 3231") when you quit the TUI or press Ctrl-C

Install (from crates.io)

You dont need to clone this repo to use socktop. Install the published binaries with cargo:

# TUI (client)
cargo install socktop
# Agent (server)
cargo install socktop_agent

This drops socktop and socktop_agent into ~/.cargo/bin (add it to PATH).

Notes:

  • After installing Rust via rustup, reload your shell (e.g., exec bash) so cargo is on PATH.
  • Windows: you can also grab prebuilt EXEs from GitHub Actions artifacts if rustup scares you. It shouldnt. Be brave.

System-wide agent (Linux)

# If you installed with cargo, binaries are in ~/.cargo/bin
sudo install -o root -g root -m 0755 "$HOME/.cargo/bin/socktop_agent" /usr/local/bin/socktop_agent

# Install and enable the systemd service (example unit in docs/)
sudo install -o root -g root -m 0644 docs/socktop-agent.service /etc/systemd/system/socktop-agent.service
sudo systemctl daemon-reload
sudo systemctl enable --now socktop-agent

# Enable SSL

# Stop service
sudo systemctl stop socktop-agent

# Edit service to append SSL option and port
sudo micro /etc/systemd/system/socktop-agent.service

--
ExecStart=/usr/local/bin/socktop_agent --enableSSL --port 8443
--

# Reload
sudo systemctl daemon-reload

# Restart
sudo systemctl start socktop-agent

# check logs for certificate location
sudo journalctl -u socktop-agent -f

--
Aug 22 22:25:26 rpi-master socktop_agent[2913998]: socktop_agent: generated self-signed TLS certificate at /var/lib/socktop/.config/socktop_agent/tls/cert.pem
--


Usage

Agent (server):

socktop_agent --port 3000
# or env: SOCKTOP_PORT=3000 socktop_agent
# optional auth: SOCKTOP_TOKEN=changeme socktop_agent
# enable TLS (selfsigned cert, default port 8443; you can also use -p):
socktop_agent --enableSSL --port 8443

Client (TUI):

socktop ws://HOST:3000/ws
# with token:
socktop "ws://HOST:3000/ws?token=changeme"
# TLS with pinned server certificate (recommended over the internet):
socktop --tls-ca /path/to/cert.pem wss://HOST:8443/ws
# (By default hostname/SAN verification is skipped for ease on home networks. To enforce it add --verify-hostname)
socktop --verify-hostname --tls-ca /path/to/cert.pem wss://HOST:8443/ws
# shorthand:
socktop -t /path/to/cert.pem wss://HOST:8443/ws
# Note: providing --tls-ca/-t automatically upgrades ws:// to wss:// if you forget
# force the small-window layout at any terminal size (normally automatic):
socktop --compact ws://HOST:3000/ws

Intervals (client-driven):

  • Fast metrics: ~500 ms
  • Processes: ~2 s
  • Disks: ~5 s

The agent stays idle unless queried. When queried, it collects just whats needed.


Compact mode

In a short terminal the fixed layout runs out of rows and the CPU graph and per-core bars are the first things to collapse — exactly the panes you are most likely watching. Once the window is too short for the Disks pane to show even one disk, socktop switches to a compact layout:

  • Disks is dropped. It is the pane that degrades worst when partially drawn.
  • Memory and Swap move side by side into the row Disks vacated.
  • GPU shrinks to a single line — utilisation and VRAM only, no device name. On a host with no GPU the pane disappears entirely.
  • Everything reclaimed goes to the CPU graph and per-core bars, which stay usable well below the size where they used to vanish.

The switch is automatic and needs no configuration. Pass --compact to pin the compact layout at any window size:

socktop --compact ws://HOST:3000/ws

Connection Profiles (Named)

You can save frequently used connection settings (URL + optional TLS CA path) under a short name and reuse them later.

Config file location:

  • Linux (XDG): $XDG_CONFIG_HOME/socktop/profiles.json
  • Fallback (when XDG not set): ~/.config/socktop/profiles.json

Creating a profile

First time you specify a new --profile/-P name together with a URL (and optional --tls-ca), it is saved automatically:

socktop --profile prod ws://prod-host:3000/ws
# With TLS pinning:
socktop --profile prod-tls --tls-ca /path/to/cert.pem wss://prod-host:8443/ws

You can also set custom intervals (milliseconds):

```bash
socktop --profile prod --metrics-interval-ms 750 --processes-interval-ms 3000 ws://prod-host:3000/ws

If a profile already exists you will be prompted before overwriting:

$ socktop --profile prod ws://new-host:3000/ws Overwrite existing profile 'prod'? [y/N]: y


To overwrite without an interactive prompt pass `--save`:

```bash
socktop --profile prod --save ws://new-host:3000/ws

Using a saved profile

Just pass the profile name (no URL needed):

socktop --profile prod
socktop -P prod-tls      # short flag

The stored URL (and TLS CA path, if any) plus any saved intervals will be used. TLS auto-upgrade still applies if a CA path is stored alongside a ws:// URL.

Interactive selection (no args)

If you run socktop with no arguments and at least one profile exists, you will be shown a numbered list to pick from:

$ socktop
Select profile:
  1. prod
  2. prod-tls
Enter number (or blank to abort): 2

Choosing a number starts the TUI with that profile. A builtin demo option is always appended; selecting it launches a local agent on port 3231 (no TLS) and connects to ws://127.0.0.1:3231/ws. Pressing Enter on blank aborts without connecting.

JSON format

An example profiles.json (prettyprinted):

{
  "profiles": {
    "prod": { "url": "ws://prod-host:3000/ws" },
    "prod-tls": {
      "url": "wss://prod-host:8443/ws",
      "tls_ca": "/home/user/certs/prod-cert.pem",
      "metrics_interval_ms": 500,
      "processes_interval_ms": 2000
    }
  },
  "version": 0
}

Notes:

  • The tls_ca path is stored as given; if you move or rotate the certificate update the profile by re-running with --profile NAME --save.
  • Deleting a profile: edit the JSON file and remove the entry (TUI does not yet have an in-app delete command).
  • Profiles are client-side convenience only; they do not affect the agent.
  • Intervals: metrics_interval_ms controls the fast metrics poll (default 500 ms). processes_interval_ms controls process list polling (default 2000 ms). Values below 100 ms (metrics) or 200 ms (processes) are clamped.

Updating

Update the agent (systemd):

# on the server running the agent
cargo install socktop_agent --force
sudo systemctl stop socktop-agent
sudo install -o root -g root -m 0755 "$HOME/.cargo/bin/socktop_agent" /usr/local/bin/socktop_agent
# if you changed the unit file:
# sudo install -o root -g root -m 0644 docs/socktop-agent.service /etc/systemd/system/socktop-agent.service
# sudo systemctl daemon-reload
sudo systemctl start socktop-agent
sudo systemctl status socktop-agent --no-pager
# logs:
# journalctl -u socktop-agent -f

Update the TUI (client):

cargo install socktop --force
socktop ws://HOST:3000/ws

Tip: If only the binary changed, restart is enough. If the unit file changed, run sudo systemctl daemon-reload.


Configuration (agent)

  • Port:
    • Flag: --port 8080 or -p 8080
    • Positional: socktop_agent 8080
    • Env: SOCKTOP_PORT=8080
  • TLS (selfsigned):
    • Enable: --enableSSL
    • Default TLS port: 8443 (override with --port/-p)
    • Certificate/Key location (created on first TLS run):
      • Linux (XDG): $XDG_CONFIG_HOME/socktop_agent/tls/{cert.pem,key.pem} (defaults to ~/.config)
      • The agent prints these paths on creation.
    • You can set XDG_CONFIG_HOME before first run to control where certs are written.
    • Additional SANs: set SOCKTOP_AGENT_EXTRA_SANS (commaseparated) before first TLS start to include extra IPs/DNS names in the cert. Example:
      SOCKTOP_AGENT_EXTRA_SANS="192.168.1.101,myhost.internal" socktop_agent --enableSSL
      
      This prevents client errors like NotValidForName when connecting via an IP not present in the default cert SAN list.
    • Expiry / rotation: the generated cert is valid for ~397 days from creation. If the agent fails to start with an "ExpiredCertificate" error (or your client reports expiry), simply delete the existing cert and key:
      rm ~/.config/socktop_agent/tls/cert.pem ~/.config/socktop_agent/tls/key.pem
      # (adjust path if XDG_CONFIG_HOME is set or different user)
      systemctl restart socktop-agent   # if running under systemd
      
      On next TLS start the agent will generate a fresh pair. Only distribute the new cert.pem to clients (never the key).
  • Auth token (optional): SOCKTOP_TOKEN=changeme
  • Disable GPU metrics: SOCKTOP_AGENT_GPU=0
  • Disable CPU temperature: SOCKTOP_AGENT_TEMP=0

Keyboard & Mouse

  • Quit: q or Esc
  • Processes pane:
    • Click “CPU %” to sort by CPU descending
    • Click “Mem” to sort by memory descending
    • Mouse wheel: scroll
    • Drag scrollbar: scroll
    • Arrow/PageUp/PageDown/Home/End: scroll

Example agent JSON

{
  "sampled_at_ms": 1786752000123,
  "cpu_total": 12.4,
  "cpu_per_core": [11.2, 15.7],
  "mem_total": 33554432,
  "mem_used": 18321408,
  "swap_total": 0,
  "swap_used": 0,
  "process_count": 127,
  "hostname": "myserver",
  "cpu_temp_c": 42.5,
  "disks": [{"name":"nvme0n1p2","total":512000000000,"available":320000000000}],
  "networks": [{"name":"eth0","received":12345678,"transmitted":87654321}],
  "top_processes": [
    {"pid":1234,"name":"nginx","cpu_usage":1.2,"mem_bytes":12345678}
  ],
  "gpus": null
}

Notes:

  • process_count is merged into the main metrics on the client when processes are polled.
  • top_processes are the current top 50 (sorting in the TUI is client-side).

Security

Set a token on the agent and pass it as a query param from the client:

Server:

SOCKTOP_TOKEN=changeme socktop_agent --port 3000

Client:

socktop "ws://HOST:3000/ws?token=changeme"

TLS / WSS

For encrypted connections, enable TLS on the agent and pin the server certificate on the client.

Server (generates selfsigned cert and key on first run):

socktop_agent --enableSSL --port 8443

Client (trust/pin the server cert; copy cert.pem from the agent):

socktop --tls-ca /path/to/agent/cert.pem wss://HOST:8443/ws

Notes:

  • Do not copy the private key off the server; only the cert.pem is needed by clients.
  • When --tls-ca/-t is supplied, the client autoupgrades ws:// to wss:// to avoid protocol mismatch.
  • Hostname (SAN) verification is DISABLED by default; instead the client PINS the certificate: the agent must present a cert byte-identical to one in your --tls-ca file (expiry is ignored in this mode — you pinned that exact cert). Use --verify-hostname to switch to strict chain + SAN validation instead.
  • You can run multiple clients with different cert paths by passing --tls-ca per invocation.

Using tmux to monitor multiple hosts

You can use tmux to show multiple socktop instances in a single terminal.

socktop screenshot monitoring 4 Raspberry Pis using Tmux

Prerequisites:

  • Install tmux (Ubuntu/Debian: sudo apt-get install tmux)

Key bindings (defaults):

  • Split left/right: Ctrl-b %
  • Split top/bottom: Ctrl-b "
  • Move between panes: Ctrl-b + Arrow keys
  • Show pane numbers: Ctrl-b q
  • Close a pane: Ctrl-b x
  • Detach from session: Ctrl-b d

Two panes (left/right)

  • This creates a session named "socktop", splits it horizontally, and starts two socktops.
tmux new-session -d -s socktop 'socktop ws://HOST1:3000/ws' \; \
  split-window -h 'socktop ws://HOST2:3000/ws' \; \
  select-layout even-horizontal \; \
  attach

Four panes (top-left, top-right, bottom-left, bottom-right)

  • This creates a 2x2 grid with one socktop per pane.
tmux new-session -d -s socktop 'socktop ws://HOST1:3000/ws' \; \
  split-window -h 'socktop ws://HOST2:3000/ws' \; \
  select-pane -t 0 \; split-window -v 'socktop ws://HOST3:3000/ws' \; \
  select-pane -t 1 \; split-window -v 'socktop ws://HOST4:3000/ws' \; \
  select-layout tiled \; \
  attach

Tips:

  • Replace HOST1..HOST4 (and ports) with your targets.
  • Reattach later: tmux attach -t socktop

Platform notes

  • Linux: fully supported (agent and client).
  • Raspberry Pi:
    • 64-bit: aarch64-unknown-linux-gnu
    • 32-bit: armv7-unknown-linux-gnueabihf
  • Windows:
    • TUI + agent can build with stable Rust; bring your own MSVC. Youre on Windows; you know the drill.
    • CPU temperature may be unavailable.
    • binary exe for both available in build artifacts under actions.
  • macOS:
    • TUI works; agent is primarily targeted at Linux. Agent will run just fine on macos for debugging but I have not documented how to run as a service, I may not given the "security" feautures with applications on macos. We will see.

Development

cargo fmt
cargo clippy --all-targets --all-features
cargo run -p socktop -- ws://127.0.0.1:3000/ws
# TLS (dev): first run will create certs under ~/.config/socktop_agent/tls/
cargo run -p socktop_agent -- --enableSSL --port 8443

Auto-format on commit

A sample pre-commit hook that runs cargo fmt --all is provided in .githooks/pre-commit. Enable it (one-time):

git config core.hooksPath .githooks
chmod +x .githooks/pre-commit

Every commit will then format Rust sources and restage them automatically.


Roadmap

  • Agent authentication (token)
  • Hide per-thread entries; only show processes
  • Sort top processes in the TUI
  • Configurable refresh intervals (client)
  • Export metrics to file
  • TLS / WSS support (selfsigned server cert + client pinning)
  • Split processes/disks to separate WS calls with independent cadences (already logical on client; formalize API)
  • Outage notifications and reconnect.
  • Per process detailed statistics pane
  • cleanup of Disks section, properly display physical disks / partitions, remove duplicate entries

License

MIT — see LICENSE.


Acknowledgements

  • ratatui for the TUI
  • sysinfo for system metrics
  • tokio-tungstenite for WebSockets