diff --git a/reticulum-sidecar/patches/README.md b/reticulum-sidecar/patches/README.md index 5624c0f7d..8de4330e6 100644 --- a/reticulum-sidecar/patches/README.md +++ b/reticulum-sidecar/patches/README.md @@ -519,6 +519,29 @@ Listed in `scripts/lib/ratspeak-overlay-apply-list.sh` and `RATSPEAK_PATCH_ENTRI When ratspeak/rsReticulum exposes equivalent live TX queue depth on interface stats, remove this patch and the apply step. +## rsReticulum-pathless-link-exclude-rf.patch + +Python `Transport.py` only transmits pathless `Destination.LINK` packets on `destination.attached_interface`. rsReticulum was pathless-broadcasting Link hashes (Keepalive / Lrrtt / Resource\*) onto every OUT iface, including flow-controlled RNodes, which filled the host TX queue during PN Sync. Prefer `link_table` when present; otherwise broadcast only to non-RF sinks (RNode name or bitrate under 100 kbps). + +| Field | Value | +| ----- | ----- | +| **Base commit** | floated `origin/main` (regenerate; record short SHA in PR) | +| **Upstream PR** | none yet (mesh-client-local) | + +**Touches:** `crates/rns-transport/src/actor/mod.rs`, `crates/rns-transport/src/actor/outbound.rs` + +### Apply locally + +```bash +./scripts/apply-rsReticulum-pathless-link-exclude-rf.sh +``` + +Listed in `scripts/lib/ratspeak-overlay-apply-list.sh` and `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh`. + +### Sunset + +When ratspeak/rsReticulum matches Python pathless-LINK attached-only (or equivalent) on floated `origin/main`, remove this patch and the apply step. + ## rsLXMF-propagation-client-abort-transfer.patch Adds `PropagationClient::abort_transfer` so Cancel / mid-transfer abort leaves the client **Idle**. Without it, a cancelled Sync can leave `/get` stuck busy and the next Sync returns `PROPAGATION_RETRIEVE_BUSY` forever (or Auto falsely concludes there are no PNs). diff --git a/reticulum-sidecar/patches/rsReticulum-pathless-link-exclude-rf.patch b/reticulum-sidecar/patches/rsReticulum-pathless-link-exclude-rf.patch new file mode 100644 index 000000000..426e6b298 --- /dev/null +++ b/reticulum-sidecar/patches/rsReticulum-pathless-link-exclude-rf.patch @@ -0,0 +1,108 @@ +From: mesh-client +Subject: Exclude RF sinks from pathless Link broadcasts + +Python Transport.py only transmits pathless LINK on attached_interface. +rsReticulum was flooding Keepalive/Resource onto flow-controlled RNodes. + +--- a/crates/rns-transport/src/actor/mod.rs ++++ b/crates/rns-transport/src/actor/mod.rs +@@ -38,7 +38,18 @@ + // duplex route. They never appear on the wire or in the interface registry. + const LOCAL_LINK_INITIATOR_INTERFACE: InterfaceId = InterfaceId::MAX; + const LOCAL_LINK_RESPONDER_INTERFACE: InterfaceId = InterfaceId::MAX - 1; ++ ++/// LoRa / KISS-class sinks must not receive pathless Link floods (no path for ++/// link hashes). Bitrate ceiling ~100 kbps; also match common RNode names. ++fn iface_is_pathless_link_rf_sink(entry: &InterfaceEntry) -> bool { ++ const RF_BITRATE_CEILING_BPS: u64 = 100_000; ++ if entry.name.to_ascii_lowercase().contains("rnode") { ++ return true; ++ } ++ entry.bitrate > 0 && entry.bitrate < RF_BITRATE_CEILING_BPS ++} + ++ + #[derive(Clone)] + struct LocalLinkRoute { + initiator_tx: mpsc::Sender, +@@ -1526,6 +1537,18 @@ + fields(raw_len = raw.len()), + )] + fn broadcast_on_interfaces(&mut self, raw: &[u8], except: Option) -> bool { ++ self.broadcast_on_interfaces_filtered(raw, except, false) ++ } ++ ++ /// Pathless Link egress must not flood flow-controlled RF (RNode / KISS-class ++ /// bitrates). Unattached senders still reach TCP hubs; RF only via Attached / ++ /// link_table / a real path. ++ fn broadcast_on_interfaces_filtered( ++ &mut self, ++ raw: &[u8], ++ except: Option, ++ exclude_rf_sinks: bool, ++ ) -> bool { + // Collect ids first so the borrow on self.interfaces ends before + // send_to_interface (which may mutate it via auto-deregister) runs. + let ids: Vec = self +@@ -1534,6 +1557,8 @@ + .filter_map(|(&id, entry)| { + if except == Some(id) || !entry.direction.outbound { + None ++ } else if exclude_rf_sinks && iface_is_pathless_link_rf_sink(entry) { ++ None + } else { + Some(id) + } +--- a/crates/rns-transport/src/actor/outbound.rs ++++ b/crates/rns-transport/src/actor/outbound.rs +@@ -184,6 +184,50 @@ + } + sent + } else { ++ // No path_table entry. Python Transport.py pathless LINK only ++ // transmits on destination.attached_interface — never floods ++ // every OUT iface. rsReticulum previously broadcast Link hashes ++ // (Keepalive / Lrrtt / Resource*) onto flow-controlled RNodes and ++ // filled the host TX queue. Prefer link_table; otherwise broadcast ++ // only to non-RF ifaces so TCP hubs still see unattached senders. ++ if parsed.flags.destination_type == rns_wire::flags::DestinationType::Link { ++ if let Some(target_interface) = self ++ .link_table ++ .get(&request.destination_hash) ++ .map(|e| e.interface_id) ++ { ++ let iface_name = self ++ .interfaces ++ .get(&target_interface) ++ .map(|e| e.name.as_str()) ++ .unwrap_or("unknown"); ++ tracing::info!( ++ dest = %hex::encode(request.destination_hash), ++ interface_id = target_interface, ++ interface_name = %iface_name, ++ "outbound: pathless Link routed via link_table" ++ ); ++ return self.send_to_interface(target_interface, &request.raw); ++ } ++ ++ let iface_names: Vec<&str> = self ++ .interfaces ++ .values() ++ .filter(|e| e.direction.outbound && !super::iface_is_pathless_link_rf_sink(e)) ++ .map(|e| e.name.as_str()) ++ .collect(); ++ tracing::info!( ++ dest = %hex::encode(request.destination_hash), ++ broadcast_to = ?iface_names, ++ "outbound: pathless Link — excluding RF sinks (Python attached-only parity)" ++ ); ++ return self.broadcast_on_interfaces_filtered( ++ &request.raw, ++ None, ++ /* exclude_rf_sinks */ true, ++ ); ++ } ++ + let iface_names: Vec<&str> = self + .interfaces + .values() diff --git a/scripts/apply-rsReticulum-pathless-link-exclude-rf.sh b/scripts/apply-rsReticulum-pathless-link-exclude-rf.sh new file mode 100755 index 000000000..ef5d1ce0a --- /dev/null +++ b/scripts/apply-rsReticulum-pathless-link-exclude-rf.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Apply mesh-client rsReticulum pathless-Link RF exclusion overlay. +# Pathless LINK hashes must not flood flow-controlled RNodes (Python attached-only). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" +PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-pathless-link-exclude-rf.patch" +RNS_DIR="${RS_RETICULUM_DIR:-${REPO_ROOT}/.rsstack/rsReticulum}" +MOD_RS="${RNS_DIR}/crates/rns-transport/src/actor/mod.rs" + +if ! git -C "${RNS_DIR}" rev-parse --is-inside-work-tree > /dev/null 2>&1; then + echo "error: rsReticulum not found at ${RNS_DIR}" >&2 + echo "Clone: git clone https://github.com/ratspeak/rsReticulum.git ${RNS_DIR}" >&2 + exit 1 +fi + +if [[ ! -f "${PATCH_FILE}" ]]; then + echo "error: patch not found at ${PATCH_FILE}" >&2 + exit 1 +fi + +if [[ -f "${MOD_RS}" ]] && grep -q 'iface_is_pathless_link_rf_sink' "${MOD_RS}"; then + echo "pathless-link-exclude-rf overlay already applied on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" + exit 0 +fi + +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "pathless-link-exclude-rf"; then + exit 0 +fi +exit 1 diff --git a/scripts/lib/ratspeak-overlay-apply-list.sh b/scripts/lib/ratspeak-overlay-apply-list.sh index f109d45ba..a68079fe2 100644 --- a/scripts/lib/ratspeak-overlay-apply-list.sh +++ b/scripts/lib/ratspeak-overlay-apply-list.sh @@ -14,6 +14,7 @@ RS_RETICULUM_APPLY_SCRIPTS=( apply-rsReticulum-path-medium-slots.sh apply-rsReticulum-inbound-raw-saturation-log.sh apply-rsReticulum-interface-tx-queue-stats.sh + apply-rsReticulum-pathless-link-exclude-rf.sh ) RS_LXMF_APPLY_SCRIPTS=( diff --git a/scripts/update.sh b/scripts/update.sh index eed18a04e..7c1508dde 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -220,6 +220,7 @@ check_ratspeak_patches() { 'rsReticulum-discovery-announce-egress.patch|ratspeak/rsReticulum|19|rsReticulum discovery announce egress|https://github.com/ratspeak/rsReticulum/pull/19' 'rsReticulum-inbound-raw-saturation-log.patch|ratspeak/rsReticulum||rsReticulum inbound-raw saturation log|' 'rsReticulum-interface-tx-queue-stats.patch|ratspeak/rsReticulum||rsReticulum interface TX queue stats|' + 'rsReticulum-pathless-link-exclude-rf.patch|ratspeak/rsReticulum||rsReticulum pathless Link exclude RF sinks|' 'rsLXMF-propagation-sync-peering.patch|ratspeak/rsLXMF|4|rsLXMF propagation sync peering|https://github.com/ratspeak/rsLXMF/pull/4' 'rsLXMF-propagation-node-policy-setters.patch|ratspeak/rsLXMF|6|rsLXMF PropagationNode policy setters|https://github.com/ratspeak/rsLXMF/pull/6' 'rsLXMF-propagation-node-deferred-messagestore-load.patch|ratspeak/rsLXMF||rsLXMF PropagationNode deferred messagestore load|'