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
Open
Conversation
…ER/SETT handshake so it can run as a Ski…
catalyst-cloud-connector
Bot
force-pushed
the
MAN-86
branch
from
September 7, 2026 05:42
8897c24 to
3ba938d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 barelogin: \r\n, silently accepted any login string, and dropped bothSKIMMER/SETTandBYEintoCommand::Unknownwith 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::SettandCommand::Byeare new variants;parse()recognizesSKIMMER/SETT, bareSETT, andBYE(case-insensitively) above theUnknownfallback. The tokenizer already normalizedSKIMMER/SETTinto["SKIMMER","SETT"], so this was match-arm-only. The old test assertingparse("bye") == Command::Unknown— which literally encoded the bug — was replaced with one assertingCommand::Bye, with a comment recording why.SETT reply model (new
sett.rs) —SettSettings/ValidationLevelrender the wire formatSETT: vlNormal <lo>-<hi>[,<lo>-<hi>...](kHz, one decimal, comma-separated, no spaces), with an optionalCQtoken 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.rsgained a smallallocations()accessor exposing the existingBANDS_HZtable 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_qthare rejected at TOML-deserialize time if empty or containing control characters (same line-injection concernstation_callsignalready documents — these are interpolated into a banner every connecting client sees).operator_gridis validated as a 4- or 6-character Maidenhead locator.Telnet wiring (
telnet.rs) — the singlestation_call: Stringparameter toserve()/handle_client()becameArc<StationProfile>(identity +SettSettings), following the existingJsonStreamConfigprecedent 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 thanmanta_spot::grammar::is_plausible, which MAN-45 already found rejects real callsigns likeJW/LB2PG; it's a shape check, not authentication (this listener has none, by design —ARCHITECTURE.md§7).Command::Settreplies with the formattedSettSettings;Command::ByerepliesCU AGN!\r\nand closes the connection, matching every other clean-disconnect path in the loop.CLI wiring (
main.rs) —start_spot_servernow takes the RF centre frequency (taken after any--dial-freq-hzoverride) and builds theStationProfile, warning at startup if operator QTH/grid weren't configured.Docs — new
docs/DECISIONS/2026-09-07-man86-aggregator-sett-handshake.mdrecords the primary-source recovery this plan required (see below) plus all resulting design decisions;docs/DECISIONS/2026-09-01-legacy-capability-matrix.mdgains 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
SETTshould 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 theSETTreply 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 aSETTline 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:
de <call>-# >; no source available shows Aggregator parsing it — follow-up candidate).SKIMMER/QSY//AUDIOIF//LO_FREQ//START//STOP(already dispositioned Non-goal) or Control-D disconnect.ARCHITECTURE.mdcommits normatively to none on this listener.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 realtelnet::serve).connect_and_loginhelper, 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 warningsandcargo fmt --all --checkboth clean;--features hpsdrclippy also clean. (--features soapycan't build in this container — missing system SoapySDR library, unrelated to this diff, which touches no soapy-gated code.)/code-reviewat 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 anOk(0)EOF and would hot-spin if the server ever closed early instead of failing fast; and the exact terminator onPlease 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)
SKIMMER/SETTCQ-filter token (once/if manta gains a CQ-only mode) remain open, pre-existing follow-up candidates noted in the plan.