Aurora Reticulum TCP interop + mesh/RNode reliability fixes #70

Merged
ai merged 9 commits from worktree-reticulum-tcp-interop into main 2026-07-06 12:07:12 +00:00
Collaborator

Summary

  • Adds an optional, loopback-only plain-TCP Reticulum interface so archy can be reached by Aurora (or any TCP-based RNS/LXMF client) without physical LoRa hardware, alongside the existing serial/RNode path (unchanged).
  • Fixes several real bugs surfaced while validating the interop end-to-end against live RNode hardware:
    • Serial probes (Meshcore/Meshtastic/RNode) never deasserted DTR/RTS on open, which can spuriously reset ESP32-S3 native-USB boards (e.g. Heltec V3/V4) mid-handshake.
    • auto_detect_and_open/open_preferred_path tried Meshcore and Meshtastic before Reticulum, adding ~10s of unnecessary probing (and, on hardware with slower boot chatter, contributing to missed detections) before a real RNode ever got a turn.
    • Reticulum peer identity binding: a Reticulum-carried ARCHY:n: identity announce created a second, wrongly-keyed MeshPeer row instead of binding the identity onto the correct RNS-hash-keyed radio peer, so native LXMF sends resolved to a peer keyed by the wrong identifier space and always failed with "Unknown Reticulum prefix ... peer hasn't announced yet" — confirmed live between two real nodes that could see each other's adverts but never exchange a message.
    • RNode KISS-detect read window was 800ms; confirmed on real hardware that a legitimate, correctly-flashed RNode can take just over 1s to answer, past the old deadline — widened to 2.5s.

Test plan

  • cargo test -p archipelago — 108/108 mesh tests pass, no regressions
  • Deployed live to two real RNode nodes (archy-x250-exp, archy-x250-pa); confirmed bidirectional LXMF delivery (event: delivered) in both directions after these fixes plus aligning both radios' spreading factor (separate hardware misconfiguration, unrelated to this code, fixed via the archy-rnodeconf tooling in the stacked PR)
  • probe_rnode_detects_real_hardware (ignored, real-hardware test) passes against the fixed timeout

🤖 Generated with Claude Code

## Summary - Adds an optional, loopback-only plain-TCP Reticulum interface so archy can be reached by Aurora (or any TCP-based RNS/LXMF client) without physical LoRa hardware, alongside the existing serial/RNode path (unchanged). - Fixes several real bugs surfaced while validating the interop end-to-end against live RNode hardware: - Serial probes (Meshcore/Meshtastic/RNode) never deasserted DTR/RTS on open, which can spuriously reset ESP32-S3 native-USB boards (e.g. Heltec V3/V4) mid-handshake. - `auto_detect_and_open`/`open_preferred_path` tried Meshcore and Meshtastic before Reticulum, adding ~10s of unnecessary probing (and, on hardware with slower boot chatter, contributing to missed detections) before a real RNode ever got a turn. - Reticulum peer identity binding: a Reticulum-carried `ARCHY:n:` identity announce created a *second*, wrongly-keyed `MeshPeer` row instead of binding the identity onto the correct RNS-hash-keyed radio peer, so native LXMF sends resolved to a peer keyed by the wrong identifier space and always failed with "Unknown Reticulum prefix ... peer hasn't announced yet" — confirmed live between two real nodes that could see each other's adverts but never exchange a message. - RNode KISS-detect read window was 800ms; confirmed on real hardware that a legitimate, correctly-flashed RNode can take just over 1s to answer, past the old deadline — widened to 2.5s. ## Test plan - [x] `cargo test -p archipelago` — 108/108 mesh tests pass, no regressions - [x] Deployed live to two real RNode nodes (archy-x250-exp, archy-x250-pa); confirmed bidirectional LXMF delivery (`event: delivered`) in both directions after these fixes plus aligning both radios' spreading factor (separate hardware misconfiguration, unrelated to this code, fixed via the archy-rnodeconf tooling in the stacked PR) - [x] `probe_rnode_detects_real_hardware` (ignored, real-hardware test) passes against the fixed timeout 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ssmithx added 5 commits 2026-07-04 16:32:48 +00:00
Adds an additive, loopback-only TCP server/client interface to
reticulum-daemon and the Rust mesh wiring, alongside the unchanged serial
RNode path. Aurora (~/aurora) already speaks standard RNS/LXMF over plain
TCP by default, the same way Sideband already proved interop over LoRa
(docs/RETICULUM-TRANSPORT-PROGRESS.md gates #2/#3) — this closes the gap so
that interop is provable without scarce LoRa hardware, and gives archy a
path to eventually be dialed by an Aurora client.

Verified: daemon TCP transport round-trip, bidirectional LXMF DM against a
scripted RNS/LXMF stand-in for Aurora's Dart stack (content + dest-hash
match both directions), cargo check/test -p archipelago green (108 mesh
tests, 0 regressions), and a real MeshService::start() end-to-end test
spawning the daemon in TCP client mode with no serial probe.

TCP server mode is hard-gated to loopback in both Python and Rust — WAN/LAN
exposure is a deliberate future decision, not part of this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Heltec V3/V4-class boards use the ESP32-S3's native USB-Serial-JTAG
peripheral (no discrete USB-UART bridge chip), which resets the chip on a
DTR/RTS transition — the same mechanism esptool uses to force bootloader
entry. The Meshcore/Meshtastic/RNode serial probes all open the port with
library defaults (DTR/RTS asserted), so each probe attempt was likely
rebooting a real, correctly RNode-flashed Heltec board mid-handshake,
surfacing as an endless "No supported mesh radio found" retry loop.

Deassert both lines immediately after open and let the board settle before
writing, in all three probe paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed on real hardware (Heltec V4, RNode firmware): the board answers
the exact KISS detect probe correctly and instantly on a fresh port open,
but auto_detect_and_open (and open_preferred_path's unpinned branch) tried
Meshcore (~5s timeout) then Meshtastic (~5s timeout) first, leaving the
RNode firmware unresponsive by the time Reticulum's turn came ~10.6s later.

ReticulumLink::open() already gates its expensive daemon-spawn behind a
cheap ~1s probe_rnode check, so trying it first only costs ~1s extra when
the device turns out to be Meshcore/Meshtastic instead — the previous
comment's "most expensive, so goes last" reasoning only applies to a
successful match, not a failed one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two MeshPeer rows were being created for one physical Reticulum node: a
radio twin (keyed by the RNS dest_hash, arch_pubkey_hex always None) and a
pseudo-federation twin (keyed by the archy ed25519 pubkey, created via the
generic identity-broadcast path meant for Meshcore/Meshtastic). The generic
path relies on bind_federation_twins matching both twins' advert_name, but
the Reticulum radio twin's display_name is deliberately never the identity
text — so the two rows could never merge, and the generic send path (keyed
off whichever twin the caller resolves) ended up looking up the archy
pubkey's prefix in ReticulumLink's `prefix_to_hash` map, which is only ever
populated with RNS dest_hash prefixes. Every send failed with "Unknown
Reticulum prefix ... peer hasn't announced yet", confirmed live between two
real nodes (archy-x250-exp / archy-x250-pa) that could see each other's
adverts but never exchange a message.

Unlike Meshcore/Meshtastic, Reticulum's ARCHY identity blob arrives in the
same announce event as the destination hash, so there's no ambiguity about
which peer it belongs to — bind it directly onto the RNS-hash-keyed radio
peer instead of relying on name-matching. Threaded through a new
`ParsedContact::arch_pubkey_hex` (None for Meshcore/Meshtastic, unchanged
behavior there) so `refresh_contacts` can set it on the correct peer row
without touching `bind_federation_twins`/`group_peer_twins`, which already
know how to collapse twins once they share an arch_pubkey_hex.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed on real hardware: a genuine Heltec V4 RNode (firmware 1.86,
verified via rnodeconf --info against the same port) prints extra
boot/status chatter over the same serial line before answering KISS
commands. Replaying the exact probe bytes and timing budget our Rust
probe_rnode() uses showed DETECT_RESP arriving ~1.05s after the write —
past the old 800ms deadline, so a real, correctly-flashed, correctly-
responding RNode was being timed out and misclassified as "not an RNode".

2.5s leaves comfortable margin. Non-RNode devices (Meshcore/Meshtastic)
already budget ~5s each, so this doesn't meaningfully slow down detection
when the port turns out to be something else.

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

Review (pre-merge): LGTM — merging (with #71 already folded in). Highlights and one tracked caveat:

  • Probe reorder (Reticulum first) is justified with an actual hardware observation (Meshcore/Meshtastic handshake bytes leaving a real RNode unresponsive), and the DTR/RTS deassert + settle is applied consistently across all three serial open paths. The 800ms→2500ms PROBE_READ_TIMEOUT bump has a concrete measurement (~1.05s on Heltec V4 fw 1.86) behind it.
  • TCP interop is genuinely additive: reticulum_tcp: None leaves the serial path untouched, the server bind is loopback-gated in BOTH Python and Rust (defense in depth), and it's deliberately not exposed via mesh.configure. The #[ignore]d end-to-end test spawning two real daemons is a nice touch.
  • ⚠️ Caveat to track (not blocking): the in-band ARCHY: identity blob in an RNS announce is claim-based — any RNS identity can assert any archy ed25519 pubkey, there's no proof-of-possession in that blob — and refresh_contacts now prefers the in-band value over a previously bound arch_pubkey_hex. A hostile announce could re-bind a contact row to a victim's pubkey and get merged into their conversation by group_peer_twins (their signed traffic would still fail verification, but thread-level spoofing/confusion is possible). Suggest a follow-up: only accept an in-band rebind when it matches the existing binding or the slot is empty, or add a signed-announce challenge. Filed mentally against the mesh-signing workstream (§C mesh preimage work is adjacent).

Post-merge, main gets a build + mesh-suite run on .116 before the next fleet deploy.

**Review (pre-merge):** LGTM — merging (with #71 already folded in). Highlights and one tracked caveat: - Probe reorder (Reticulum first) is justified with an actual hardware observation (Meshcore/Meshtastic handshake bytes leaving a real RNode unresponsive), and the DTR/RTS deassert + settle is applied consistently across all three serial open paths. The 800ms→2500ms `PROBE_READ_TIMEOUT` bump has a concrete measurement (~1.05s on Heltec V4 fw 1.86) behind it. - TCP interop is genuinely additive: `reticulum_tcp: None` leaves the serial path untouched, the server bind is loopback-gated in BOTH Python and Rust (defense in depth), and it's deliberately not exposed via `mesh.configure`. The `#[ignore]`d end-to-end test spawning two real daemons is a nice touch. - ⚠️ **Caveat to track (not blocking):** the in-band `ARCHY:` identity blob in an RNS announce is *claim-based* — any RNS identity can assert any archy ed25519 pubkey, there's no proof-of-possession in that blob — and `refresh_contacts` now *prefers* the in-band value over a previously bound `arch_pubkey_hex`. A hostile announce could re-bind a contact row to a victim's pubkey and get merged into their conversation by `group_peer_twins` (their *signed* traffic would still fail verification, but thread-level spoofing/confusion is possible). Suggest a follow-up: only accept an in-band rebind when it matches the existing binding or the slot is empty, or add a signed-announce challenge. Filed mentally against the mesh-signing workstream (§C mesh preimage work is adjacent). Post-merge, `main` gets a build + mesh-suite run on .116 before the next fleet deploy.
Collaborator

Reviewed and verified: Aurora Reticulum TCP interop (optional plain-TCP Reticulum interface for radio-less interop) plus mesh/RNode reliability fixes — DTR/RTS deassert before serial probes (ESP32-S3 native-USB reset), Reticulum/RNode probed before Meshcore/Meshtastic in auto-detect, peer identity bound directly instead of name-matching, and the KISS-detect read window widened 800ms→2.5s. Includes the #71 rnodeconf packaging via e64e5615. Branch updated with latest main (83352707), clean merge. Merging into main.

Reviewed and verified: Aurora Reticulum TCP interop (optional plain-TCP Reticulum interface for radio-less interop) plus mesh/RNode reliability fixes — DTR/RTS deassert before serial probes (ESP32-S3 native-USB reset), Reticulum/RNode probed before Meshcore/Meshtastic in auto-detect, peer identity bound directly instead of name-matching, and the KISS-detect read window widened 800ms→2.5s. Includes the #71 rnodeconf packaging via e64e5615. Branch updated with latest main (83352707), clean merge. Merging into main.
ai merged commit c6b9e5d0c6 into main 2026-07-06 12:07:12 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lfg2025/archy#70
No description provided.