Skip to content

feat(manta-server,manta-cli): MAN-86 — answer Aggregator's SKIMMER/SETT handshake, add greeting banner and BYE - #128

Open
catalyst-cloud-connector[bot] wants to merge 1 commit into
mainfrom
MAN-86
Open

feat(manta-server,manta-cli): MAN-86 — answer Aggregator's SKIMMER/SETT handshake, add greeting banner and BYE#128
catalyst-cloud-connector[bot] wants to merge 1 commit into
mainfrom
MAN-86

Conversation

@catalyst-cloud-connector

@catalyst-cloud-connector catalyst-cloud-connector Bot commented Sep 7, 2026

Copy link
Copy Markdown

Why

RBN's Aggregator manual v6.0 §9.2 is explicit: "If Aggregator does not receive a response to a SETT command to a Skimmer, it will not forward spots from that source." Per decision D1 in docs/DECISIONS/2026-09-06-broad-review-decisions.md, Aggregator-compatible operation is manta's primary near-term RBN admission path — it's the only publicly documented one. Before this change, manta's telnet server (crates/manta-server/src/telnet.rs) opened every connection with a bare login: \r\n, silently accepted any login string, and dropped both SKIMMER/SETT and BYE into Command::Unknown with zero reply bytes and no disconnect — reproduced live against a running build before any code was touched (see the research doc's §1§3). A stock Aggregator pointed at manta would give up on it after 5 minutes, exactly as the ticket describes.

What changed

Command grammar (command.rs) — Command::Sett and Command::Bye are new variants; parse() recognizes SKIMMER/SETT, bare SETT, and BYE (case-insensitively) above the Unknown fallback. The tokenizer already normalized SKIMMER/SETT into ["SKIMMER","SETT"], so this was match-arm-only. The old test asserting parse("bye") == Command::Unknown — which literally encoded the bug — was replaced with one asserting Command::Bye, with a comment recording why.

SETT reply model (new sett.rs) — SettSettings/ValidationLevel render the wire format SETT: vlNormal <lo>-<hi>[,<lo>-<hi>...] (kHz, one decimal, comma-separated, no spaces), with an optional CQ token manta never currently emits (no CQ-only mode). segments_for_passband() derives the reported segments from the live passband (centre ± rate/2) clipped to whichever amateur allocation(s) it overlaps, falling back to the raw passband if it overlaps none, and returning no segment for a non-finite/non-positive centre frequency. band.rs gained a small allocations() accessor exposing the existing BANDS_HZ table for this purpose.

Operator-identity config (config.rs) — three new optional [server] keys (operator_name, operator_qth, operator_grid) feed the greeting banner. All optional, so no existing config breaks. operator_name/operator_qth are rejected at TOML-deserialize time if empty or containing control characters (same line-injection concern station_callsign already documents — these are interpolated into a banner every connecting client sees). operator_grid is validated as a 4- or 6-character Maidenhead locator.

Telnet wiring (telnet.rs) — the single station_call: String parameter to serve()/handle_client() became Arc<StationProfile> (identity + SettSettings), following the existing JsonStreamConfig precedent for collapsing long parameter lists. On connect, the server now sends a three-line CW-Skimmer-shaped banner (Welcome to the manta Telnet cluster port! / manta <ver> is operated by <name>, <call> in <qth> (<grid>), each optional operator field cleanly omitted when absent / Please enter your callsign: ) before the login read. The login value is now sanitized (sanitize_login): trailing CR/NUL/whitespace — RFC 854's NVT encoding of Enter, observed verbatim from macOS telnet — is stripped rather than rejected, then the remainder must look like a plausible callsign shape or the connection is closed with an explicit message instead of silently passing through. This check is deliberately wider than manta_spot::grammar::is_plausible, which MAN-45 already found rejects real callsigns like JW/LB2PG; it's a shape check, not authentication (this listener has none, by design — ARCHITECTURE.md §7). Command::Sett replies with the formatted SettSettings; Command::Bye replies CU AGN!\r\n and closes the connection, matching every other clean-disconnect path in the loop.

CLI wiring (main.rs) — start_spot_server now takes the RF centre frequency (taken after any --dial-freq-hz override) and builds the StationProfile, warning at startup if operator QTH/grid weren't configured.

Docs — new docs/DECISIONS/2026-09-07-man86-aggregator-sett-handshake.md records the primary-source recovery this plan required (see below) plus all resulting design decisions; docs/DECISIONS/2026-09-01-legacy-capability-matrix.md gains rows for the newly-handled commands; ARCHITECTURE.md §7 no longer claims a "standard login prompt".

A documented, deliberate deviation from the ticket's literal Gherkin

The ticket's scenario 1 says SETT should reply with "the operator's callsign, location, and passband settings." The real CW Skimmer wire protocol doesn't work that way: primary-source recovery in this session (re-fetching and re-extracting the CW Skimmer manual, the Aggregator v6.0 manual, and a real two-SkimServ SETT capture from a 2015 skimmertalk archive — all cited with URLs and byte sizes in the new decision doc) shows the SETT reply carries only validation level, an optional CQ flag, and decodable segments; operator name/callsign/QTH/grid live in the greeting banner instead. Implementing the ticket's literal reading would produce a SETT line no Aggregator can actually parse. Both halves of the required information are still delivered — just split the way the real protocol splits them — so no acceptance criterion is dropped; this plan implements the real protocol shape and documents the reasoning.

What this deliberately does not do

Scoped to the ticket's three scenarios plus the login-validation technical note:

  • Doesn't change the post-login prompt shape (CW Skimmer's differs from manta's de <call>-# >; no source available shows Aggregator parsing it — follow-up candidate).
  • Doesn't implement SKIMMER/QSY//AUDIOIF//LO_FREQ//START//STOP (already dispositioned Non-goal) or Control-D disconnect.
  • Doesn't add telnet authentication — ARCHITECTURE.md commits normatively to none on this listener.
  • Doesn't add a CQ-only filter (manta has none today; the reply format already has a one-line path to add the token later).
  • Doesn't change the spot line format (separate ticket's scope).
  • Wasn't verified against a live Aggregator install — no Windows/Wine host was reachable from this environment. This is the single biggest residual risk (see below) and is recorded as such in the new decision doc.

Testing

Full validation pass (validation.md, PASS) against the real branch diff (11 files, +1064/−25):

  • cargo test --workspace --offline — 39 suites, 0 failed, including 18 new tests (3 command grammar, 8 SETT formatting/segment-derivation, 5 config validation, 4 telnet unit, 7 new telnet acceptance/integration tests exercising a real socket against the real telnet::serve).
  • All 14 pre-existing telnet acceptance tests still pass after fixing their shared connect_and_login helper, which previously assumed the greeting was exactly one line — this is the concrete regression signal for the banner change.
  • cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --all --check both clean; --features hpsdr clippy also clean. (--features soapy can't build in this container — missing system SoapySDR library, unrelated to this diff, which touches no soapy-gated code.)
  • /code-review at high effort surfaced 4 findings, none categorized correctness or security. Two are worth follow-up rather than blocking: the new acceptance-test "read until callsign prompt" loops don't handle an Ok(0) EOF and would hot-spin if the server ever closed early instead of failing fast; and the exact terminator on Please enter your callsign: (currently CRLF) is genuinely ambiguous from the manual's rendering and should be the first thing checked once a real Aggregator is reachable.

Residual risk

Nothing here has been exercised against a real Aggregator or CW Skimmer client — no Windows/Wine host was reachable in this environment. The wire format is built from four independently re-fetched primary sources (CW Skimmer manual, RBN Aggregator manual v6.0, RTTY Skimmer Server manual, and a real 2015 two-SkimServ SETT capture), not guessed, but the only way to fully retire this risk is a live-Aggregator test, which is already tracked as a follow-up (the same gap MAN-90 exists to close for the outbound uplink side). The Please enter your callsign: line terminator (finding 3 above) is the first thing to check there.

Follow-ups (not fixed inline, per this repo's review-convergence policy)

  • Make the new acceptance-test banner-read loops EOF-safe instead of looping forever on an unexpected early close.
  • Verify the handshake end-to-end against a real Aggregator/CW Skimmer install once one is reachable, starting with the callsign-prompt terminator.
  • CW Skimmer's differing post-login prompt shape, Control-D disconnect support, and the SKIMMER/SETT CQ-filter token (once/if manta gains a CQ-only mode) remain open, pre-existing follow-up candidates noted in the plan.

…ER/SETT handshake so it can run as a Ski…
@catalyst-cloud-connector catalyst-cloud-connector Bot changed the title feat: MAN-86 — manta's telnet server should answer Aggregator's SKIMMER/SETT handshake so it can run as a Ski… feat(manta-server,manta-cli): MAN-86 — answer Aggregator's SKIMMER/SETT handshake, add greeting banner and BYE Sep 7, 2026
@catalyst-cloud-connector
catalyst-cloud-connector Bot marked this pull request as ready for review September 7, 2026 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants