Skip to content

Releases: javimosch/remotecmd-cli

v2.4.1

Choose a tag to compare

@github-actions github-actions released this 10 Aug 18:28

Summary

Adds a relay secret exempt list (RELAY_SECRET_EXEMPT env var) that allows specific target names to connect without the relay secret. Also adds static (musl/Alpine-compatible) binaries to the release.

Changes

RELAY_SECRET_EXEMPT env var

  • Comma-separated target names allowed to connect without the Bearer token
  • When set, unauthenticated WebSocket connections are allowed to upgrade, but rejected on register unless the target name is in the exempt list
  • When not set (empty), behavior is unchanged: all unauthenticated connections are rejected at the WebSocket upgrade level
  • Enables gradual migration: legacy nodes can keep connecting while new nodes adopt the secret

Static binaries (Alpine/musl compatible)

  • remotecmd-cli-linux-amd64-static (CGO_ENABLED=0)
  • remotecmd-cli-linux-arm64-static (CGO_ENABLED=0)
  • Needed for Alpine Linux containers (supergato, etc.) that use musl instead of glibc

Sidecar updates (remotecmd-sidecar repo)

  • Both node and go sidecars now accept relaySecret in the pair request payload
  • Pass --secret to set-relay when provided
  • Falls back to RCMD_RELAY_SECRET env var

Deployment status

  • dk1 relay: running with RELAY_SECRET + RELAY_SECRET_EXEMPT=supergato,74ac167fc6df,PRINTER-BOT-V1,coolify,dk31
  • 6/7 targets alive: rbm21, dk2, vps1, pve2, rbm20, supergato (all with secret)
  • rfs_dev_ecobox (74ac167fc6df): offline (daemon killed, no direct SSH) — in exempt list, will connect when daemon restarts
  • dk31, PRINTER-BOT-V1, coolify: connected via exempt list (legacy nodes)

Test plan

  • 8 unit tests for relay secret (reject/accept/wrong/no-secret/exempt-allow/exempt-reject/helpers/splitCSV)
  • Full test suite passes (21s)
  • CI green
  • Deployed to dk1 with secret + exempt list
  • 6/7 targets verified alive through enforced relay
  • supergato re-paired via sidecar with relay secret

Generated with Devin

v2.4.0

Choose a tag to compare

@github-actions github-actions released this 10 Aug 17:08

Summary

First security layer for remotecmd-cli: relay shared secret + TLS env vars. Both are opt-in and backward compatible — existing deployments continue to work without any config changes.

Changes

Relay shared secret (RELAY_SECRET)

  • If RELAY_SECRET env var is set on the relay, all WebSocket connections must include Authorization: Bearer <secret> header
  • If not set, relay accepts all connections (backward compatible)
  • Clients/daemons send the header via dialRelay() — reads from config.json (relay.secret) or RELAY_SECRET env var
  • Configure with remotecmd-cli set-relay --url <u> --name <n> --secret <s>

TLS env vars

  • Relay: RCMD_TLS_CERT and RCMD_TLS_KEY env vars as fallback for --tls-cert/--tls-key flags
  • Client: RCMD_TLS_SKIP_VERIFY=1 to skip cert verification (self-signed certs)
  • wss:// URLs already worked via wsURL() — no client changes needed

Centralized relay dialing

  • All 10 relay connection sites now use dialRelay() which adds auth headers automatically
  • relayAuthHeaders() returns nil when no secret configured (no overhead)

Migration path

  1. Deploy v2.4.0 to relay and all clients/daemons (self-update works)
  2. When ready, set RELAY_SECRET on the relay and restart it
  3. Add --secret <value> to set-relay on each client/daemon (or set RELAY_SECRET env var)
  4. Until all peers are updated, the relay runs without the secret — no disruption

Test plan

  • Unit tests: 5 new tests for relay secret (reject/accept/wrong/no-secret/helpers)
  • Full test suite passes (27s)
  • CI passes (tests + coverage gate)
  • Deployed to dk1 relay — all 10 targets reconnected without config changes
  • Exec verified on rbm21, dk2, vps1 through new relay

Generated with Devin

v2.3.2

Choose a tag to compare

@github-actions github-actions released this 10 Aug 11:05

Summary

The update command now uses gh api (authenticated, 5000 req/hr) when available, falling back to unauthenticated HTTP (60 req/hr) if gh is not installed.

Why

Updating a fleet of 6+ machines would exhaust the unauthenticated GitHub API rate limit (60 req/hr) after a few updates. With gh authenticated, the limit is 5000/hr — enough for any fleet size.

How it works

latestReleaseInfo() now tries two paths:

  1. gh api repos/javimosch/remotecmd-cli/releases/latest (if gh is in PATH)
  2. Unauthenticated HTTP to api.github.com (fallback)

This also fixes the passive nudge (maybeNudge) which uses the same function.

Test plan

  • Unit tests pass
  • CI passes (tests + coverage gate)
  • Local: update --check works while unauthenticated API is rate-limited (gh fallback)
  • rbm21: self-updated via gh-authenticated path
  • dk1: updated via SSH (no gh installed, used HTTP fallback)

Generated with Devin

v2.3.1

Choose a tag to compare

@github-actions github-actions released this 10 Aug 10:52

Summary

Fixes the stale pair code spam bug: a daemon with an unpaired pair_code file was retrying every 15 seconds forever. In production, this ran for 2 months (~5,760 retries/day).

Changes

Tiered backoff based on pair_code file age:

  • 0-5 min: 15s intervals (normal pairing — common case)
  • 5-60 min: 1 min intervals (slow colleague / relay restart)
  • 1-24 hours: 5 min intervals (provisioning, "I'll do it tomorrow")
  • After 24h: stop, delete pair code, log "expired"

Uses the pair_code file's mtime as start, so a daemon restart doesn't reset the 24h expiry clock.

Total retries in 24h: ~290 (vs 5,760 with flat 15s).

Test plan

  • Unit tests pass
  • CI passes (tests + coverage gate)
  • Deployed to dk1, rbm21, local — all on v2.3.1
  • No pair code spam in relay log

Generated with Devin

v2.3.0 — Auto-tuned Parallel Streams + Gzip Writer Pooling

Choose a tag to compare

@javimosch javimosch released this 09 Aug 17:54

Summary

  • Auto-tuned parallel streams based on file size (CERN/GridFTP research)
  • Pooled gzip.Writer objects to reduce GC pressure on compressible data
  • Larger relay write queue (128 frames) for better parallel stream pipelining

Auto-tuned parallel streams

File size → stream count mapping based on GridFTP/CERN research (2-10 streams optimal):

  • 5-20 MiB → 2 streams
  • 20-100 MiB → 3 streams
  • 100+ MiB → 4 streams

Override with RCMD_PARALLEL_STREAMS=N (0 = disabled).

Gzip writer pooling

gzip.Writer is ~40KB per allocation. Previously created a new one per chunk. Now pooled with sync.Pool + Reset() for reuse. Helps most with compressible data.

Benchmark (50MB random → rbm21, 77ms RTT)

Method Time vs scp
scp 75.6s 1.0x
rcmd auto-3 streams 69.1s 1.10x

10MB zeros (compressible): 465ms (22 MB/s)
All transfers verified with sha256sum.

Test plan

  • Unit tests pass
  • 10MB random — integrity verified
  • 10MB zeros — integrity verified
  • 50MB random — integrity verified
  • Auto-parallel selects correct stream count by file size

Generated with Devin

v2.2.0 — Parallel TCP Streams

Choose a tag to compare

@javimosch javimosch released this 09 Aug 08:16

Summary

  • Parallel TCP streams for file transfers — auto-enabled for files > 5 MiB
  • 1.74x speedup on 50MB files over high-RTT relay links (77ms RTT)
  • 1.21x faster than scp for 50MB files

What changed

  • Client opens N WebSocket connections (sendFileParallel), each sending interleaved chunks
  • Daemon uses seek-based writes to place each chunk at the correct offset
  • Relay uses atomic header+binary forwarding to prevent interleaving
  • Auto-parallel: files > 5 MiB get 2 streams by default
  • Override with RCMD_PARALLEL_STREAMS=N (0 = disabled)

Benchmark (50MB random → rbm21, 77ms RTT relay)

Method Time Throughput vs scp
scp 87.0s 600 KB/s 1.0x
rcmd 1 stream 124.9s 421 KB/s 0.70x
rcmd 2 streams 86.6s 608 KB/s 1.00x
rcmd 3 streams 72.0s 734 KB/s 1.21x

All transfers verified with sha256sum.

Test plan

  • Unit tests pass
  • 10MB transfer with 2 streams — integrity verified
  • 50MB transfer with 3 streams — integrity verified
  • Small file (1MB) uses single stream
  • Auto-parallel for files > 5MB

Generated with Devin

v2.1.0

Choose a tag to compare

@javimosch javimosch released this 09 Aug 02:13

What's new

Two optimizations that together bring 10MB transfer to rbm21 from 14s to 9.5s — 1.45x faster than scp.

Async relay write queue

When a chunked file transfer begins, the relay starts a background goroutine to drain a buffered channel of frames to the target. This decouples the relay's read loop from the target's write speed: the relay can read chunk N+1 from the client while chunk N is still being written to the target.

Optimal chunk size: 2 MiB

Benchmarked on the real relay link (local → dk1 → rbm21, 77ms RTT). 2 MiB chunks keep the async pipeline full: 5 chunks for 10 MiB vs 2 chunks with 8 MiB, giving better overlap of read and write on the relay.

Benchmark (10MB random, 5 runs)

Method Time Throughput vs scp
scp 13.8s 761 KB/s 1.0x
rcmd v2.0.0 (8MB, sync) 11.0s 953 KB/s 1.26x
rcmd v2.1.0 (2MB, async) 9.5s 1104 KB/s 1.45x

Chunk size sweep (10MB random, before async queue)

Chunk size Time
1 MB 11.9s
2 MB 9.6s
4 MB 12.2s
8 MB 14.2s
16 MB 11.7s

Generated with Devin

v2.0.0

Choose a tag to compare

@javimosch javimosch released this 08 Aug 19:59

What's new

Optimization pass on top of v1.9.0 adaptive compression.

Sample-based compression skip

Compresses a 4KB sample first; if it doesn't compress by 5%, skips the full chunk entirely. Eliminates the CPU waste on incompressible data that v1.9.0 introduced.

Data type v1.9.0 v2.0.0 Improvement
10MB random 14.5s + wasted CPU 14.5s, no wasted CPU CPU saved
10MB zeros 0.3s 0.7s same (compressed)
10MB text 13s 13s same (compressed)

TCP_NODELAY

Disables Nagle's algorithm on all WebSocket connections (client, relay, daemon). Reduces latency for the small JSON headers that precede each binary chunk.

Buffer pooling

sync.Pool reuses bytes.Buffer across compression calls to reduce GC pressure on large transfers.

Shared dialer

Centralized wsDialer() helper with 1 MiB read/write buffers, TCP_NODELAY, and 1 MiB TCP socket buffers.

Chunk size

Tested 16 MiB chunks — caused throughput regression due to head-of-line blocking on high-RTT links. Kept at 8 MiB.

Generated with Devin

v1.9.0

Choose a tag to compare

@javimosch javimosch released this 08 Aug 19:32

What's new

Adaptive gzip compression for cp

Each 8 MiB chunk is gzip-compressed (BestSpeed level) before sending. If the compressed version is not at least 10% smaller, the raw bytes are sent instead — zero overhead for incompressible data.

Data type v1.8.0 v1.9.0 Speedup Wire savings
10MB zeros 16s 0.3s 53x 99.9%
10MB text (base64) 16.5s 13s 1.3x 24%
10MB source tar 11.4MB 6.7MB 41%
10MB random binary 21s 21s same 0% (skipped)

How it works

  1. Client compresses each chunk with gzip.BestSpeed
  2. If compressed_len < original_len * 0.9, sends compressed + sets Compressed: true in header
  3. Relay forwards the Compressed flag as-is (no re-serialization)
  4. Daemon decompresses with gzip.NewReader before writing to disk

Backwards compatibility

Old daemons that don't understand Compressed will ignore the flag and write compressed bytes to disk — but old clients never set it, so this only affects new-to-new transfers.

Issues

This further improves on #7 (cp transfer performance).

Generated with Devin

v1.8.0

Choose a tag to compare

@javimosch javimosch released this 08 Aug 19:20

What's new

Binary WebSocket frames for cp (3-6x faster)

File chunks are now sent as raw binary WebSocket frames instead of base64-encoded JSON. Eliminates 33% base64 overhead and JSON re-serialization on the relay.

File size v1.7.0 v1.8.0 Speedup
1 MB 16.5s 2.6s 6.3x
10 MB 67s 16s 4.2x
50 MB 257s 79s 3.3x

stdin forwarding for exec (issue #6)

Piped stdin is now forwarded to the remote command:

cat file.bin | remotecmd-cli exec --target node --cmd 'cat > /remote/file.bin'

Binary-safe (base64-encoded in transit).

Streaming from/to disk

  • Client reads files in 8 MiB chunks instead of loading the entire file into memory
  • Daemon writes chunks directly to disk instead of buffering in RAM
  • 50 MB transfer: client RSS 17 MB (was 50+ MB)

Larger socket buffers

1 MiB read/write buffers on client, relay, and daemon (default ~212 KB). Helps on high-RTT links.

Backwards compatibility

Legacy base64 chunk protocol preserved — old clients/daemons still work with new relays.

Issues fixed

  • #6: stdin pipe creates 0-byte files
  • #7: cp file transfer is ~150-200 KB/s

Generated with Devin