lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 6fe7f07b6e6c11b5a1520f0c651decac5a09030e
parent ea0d5df8de604fbd1a946cd8a264c2acdeb145c0
Author: triesap <tyson@radroots.org>
Date:   Thu,  9 Jul 2026 20:22:59 +0000

transport: harden reticulum mesh validation

Diffstat:
MCargo.lock | 1+
Mcrates/mesh/Cargo.toml | 1+
Mcrates/mesh/src/error.rs | 2++
Mcrates/mesh/src/model.rs | 16+++++++++-------
Mcrates/mesh/tests/mesh.rs | 50+++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/transport_reticulum/src/lib.rs | 20++++++++++----------
Mcrates/transport_reticulum/tests/reticulum_preview.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 172 insertions(+), 22 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4391,6 +4391,7 @@ dependencies = [ name = "radroots_mesh" version = "0.1.0-alpha.2" dependencies = [ + "radroots_transport", "serde", ] diff --git a/crates/mesh/Cargo.toml b/crates/mesh/Cargo.toml @@ -16,6 +16,7 @@ default = ["serde"] serde = ["dep:serde"] [dependencies] +radroots_transport = { workspace = true, default-features = false } serde = { workspace = true, optional = true } [lints.rust] diff --git a/crates/mesh/src/error.rs b/crates/mesh/src/error.rs @@ -3,6 +3,7 @@ use core::fmt; #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsMeshError { EmptyCustomScope, + InvalidCustomScope, EmptyMessageId, InvalidTtl, PayloadTransmissionForbidden, @@ -17,6 +18,7 @@ impl fmt::Display for RadrootsMeshError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::EmptyCustomScope => f.write_str("mesh custom scope is empty"), + Self::InvalidCustomScope => f.write_str("mesh custom scope is invalid"), Self::EmptyMessageId => f.write_str("mesh message id is empty"), Self::InvalidTtl => f.write_str("mesh frame TTL is invalid"), Self::PayloadTransmissionForbidden => { diff --git a/crates/mesh/src/model.rs b/crates/mesh/src/model.rs @@ -1,6 +1,7 @@ use crate::RadrootsMeshError; use alloc::string::{String, ToString}; use alloc::vec::Vec; +use radroots_transport::{RadrootsTransportError, RadrootsTransportMeshScopeId}; pub const RADROOTS_MESH_FRAME_VERSION: u16 = 1; @@ -60,13 +61,14 @@ pub enum RadrootsMeshScope { } impl RadrootsMeshScope { - pub fn custom(value: impl Into<String>) -> Result<Self, RadrootsMeshError> { - let value = value.into(); - let canonical = value.trim().to_ascii_lowercase(); - if canonical.is_empty() { - return Err(RadrootsMeshError::EmptyCustomScope); - } - Ok(Self::Custom(canonical)) + pub fn custom(value: impl AsRef<str>) -> Result<Self, RadrootsMeshError> { + let scope = + RadrootsTransportMeshScopeId::parse(value.as_ref()).map_err(|err| match err { + RadrootsTransportError::EmptyTargetScope => RadrootsMeshError::EmptyCustomScope, + RadrootsTransportError::InvalidTargetScope => RadrootsMeshError::InvalidCustomScope, + _ => RadrootsMeshError::InvalidCustomScope, + })?; + Ok(Self::Custom(scope.as_str().to_string())) } pub fn label(&self) -> &str { diff --git a/crates/mesh/tests/mesh.rs b/crates/mesh/tests/mesh.rs @@ -77,8 +77,8 @@ fn all_frame_types_round_trip_with_stable_codes_and_labels() { #[test] fn custom_scope_has_explicit_namespace() { - let scope = RadrootsMeshScope::custom(" Farm-North ").expect("custom scope"); - assert_eq!(scope.label(), "farm-north"); + let scope = RadrootsMeshScope::custom("farm-north.preview_1").expect("custom scope"); + assert_eq!(scope.label(), "farm-north.preview_1"); let frame = RadrootsMeshFrame::new( RadrootsMeshFrameType::RouteProbe, scope, @@ -89,15 +89,38 @@ fn custom_scope_has_explicit_namespace() { let encoded = encode_mesh_frame_cbor(&frame).expect("encode custom scope"); let decoded = decode_mesh_frame_cbor(&encoded).expect("decode custom scope"); - assert_eq!(decoded.scope_id.cbor_label(), "custom:farm-north"); + assert_eq!(decoded.scope_id.cbor_label(), "custom:farm-north.preview_1"); + assert_eq!(encode_mesh_frame_cbor(&decoded).expect("reencode"), encoded); } #[test] -fn mesh_parsers_and_validation_reject_unknown_or_empty_values() { +fn mesh_parsers_and_validation_reject_unknown_empty_or_invalid_values() { assert_eq!( - RadrootsMeshScope::custom(" ").expect_err("empty custom scope"), + RadrootsMeshScope::custom("").expect_err("empty custom scope"), RadrootsMeshError::EmptyCustomScope ); + for invalid in [ + " ", + " farm-north", + "farm-north ", + "farm north", + "farm/north", + "farm:north", + "farm\nnorth", + ] { + assert_eq!( + RadrootsMeshScope::custom(invalid).expect_err("invalid custom scope"), + RadrootsMeshError::InvalidCustomScope + ); + } + assert_eq!( + RadrootsMeshScope::parse("custom:").expect_err("empty parsed custom scope"), + RadrootsMeshError::EmptyCustomScope + ); + assert_eq!( + RadrootsMeshScope::parse("custom:farm north").expect_err("invalid parsed custom scope"), + RadrootsMeshError::InvalidCustomScope + ); assert_eq!( RadrootsMeshScope::parse("unscoped").expect_err("unknown scope"), RadrootsMeshError::UnknownScope @@ -142,6 +165,10 @@ fn mesh_errors_have_stable_display_strings() { "mesh custom scope is empty", ), ( + RadrootsMeshError::InvalidCustomScope, + "mesh custom scope is invalid", + ), + ( RadrootsMeshError::EmptyMessageId, "mesh message id is empty", ), @@ -272,6 +299,19 @@ fn decoder_rejects_previous_five_field_frame_shape() { #[test] fn decoder_rejects_malformed_cbor_shapes() { let encoded = encode_mesh_frame_cbor(&default_frame()).expect("encode default"); + let invalid_custom_scope = default_encoded_with_replacement( + 6, + 12, + [ + 0x70, b'c', b'u', b's', b't', b'o', b'm', b':', b'f', b'a', b'r', b'm', b' ', b'n', + b'o', b'r', b't', + ], + ); + assert_eq!( + decode_mesh_frame_cbor(&invalid_custom_scope).expect_err("invalid custom scope"), + RadrootsMeshError::InvalidCustomScope + ); + let mut unsupported_version = encoded.clone(); unsupported_version[2] = 2; assert_eq!( diff --git a/crates/transport_reticulum/src/lib.rs b/crates/transport_reticulum/src/lib.rs @@ -8,15 +8,16 @@ use alloc::string::String; use alloc::vec::Vec; use core::fmt; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, - RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportDeliveryReceipt, - RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, - RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportMeshScopeId, - RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportStatus, - RadrootsTransportTarget, RadrootsTransportTargetReceipt, + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, + RadrootsTransportDeliveryTargetStatus, RadrootsTransportImplementationState, + RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, + RadrootsTransportOutcomeKind, RadrootsTransportStatus, RadrootsTransportTarget, + RadrootsTransportTargetReceipt, }; const DEFAULT_PROFILE_ID: &str = "transport.reticulum.preview"; +const RETICULUM_AGENT_ENDPOINT_PREFIX: &str = "reticulum-agent:"; const UNAVAILABLE_CODE: &str = "transport_unavailable"; const DEFERRED_CODE: &str = "deferred_until_implemented"; const DEFERRED_MESSAGE: &str = "Reticulum preview delivery is deferred until implementation"; @@ -87,7 +88,8 @@ impl RadrootsReticulumPreviewAgentEndpoint { || uri .chars() .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) - || uri.find(':').is_none() + || !uri.starts_with(RETICULUM_AGENT_ENDPOINT_PREFIX) + || uri.len() == RETICULUM_AGENT_ENDPOINT_PREFIX.len() { return Err(RadrootsReticulumPreviewError::InvalidAgentEndpoint); } @@ -347,9 +349,7 @@ fn ensure_reticulum_targets( if target.uri.as_str() != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI { return Err(RadrootsReticulumPreviewError::InvalidEndpoint); } - if target.scope.as_ref().map(|scope| scope.as_str()) - != Some(RADROOTS_RETICULUM_PREVIEW_SCOPE_ID) - { + if target.scope.is_none() { return Err(RadrootsReticulumPreviewError::InvalidEndpoint); } } diff --git a/crates/transport_reticulum/tests/reticulum_preview.rs b/crates/transport_reticulum/tests/reticulum_preview.rs @@ -16,6 +16,16 @@ fn reticulum_target(uri: &str) -> RadrootsTransportTarget { RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, uri).expect("reticulum target") } +fn scoped_reticulum_target(scope: &str) -> RadrootsTransportTarget { + RadrootsTransportTarget::new_with_metadata( + RadrootsTransportKind::Reticulum, + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, + Some(RadrootsTransportMeshScopeId::parse(scope).expect("scope")), + None, + ) + .expect("scoped reticulum target") +} + fn nostr_target() -> RadrootsTransportTarget { RadrootsTransportTarget::new(RadrootsTransportKind::Nostr, "wss://relay.example") .expect("nostr target") @@ -142,11 +152,23 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { agent_endpoint.clone().into_string(), "reticulum-agent://localhost:19999" ); + let local_agent_endpoint = + RadrootsReticulumPreviewAgentEndpoint::parse("reticulum-agent:local-controller") + .expect("local agent endpoint"); + assert_eq!( + local_agent_endpoint.as_str(), + "reticulum-agent:local-controller" + ); for invalid_agent in [ "", " reticulum-agent://localhost", + "reticulum-agent:", "reticulum agent", "agent", + "https://localhost:19999", + "ws://localhost:19999", + "reticulum://localhost:19999", + "RETICULUM-AGENT://localhost:19999", ] { assert_eq!( RadrootsReticulumPreviewAgentEndpoint::parse(invalid_agent) @@ -200,6 +222,57 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { } #[test] +fn direct_preview_delivery_accepts_any_typed_reticulum_scope_as_inert_metadata() { + let transport = RadrootsReticulumPreviewTransport::default(); + let request = delivery_request(vec![scoped_reticulum_target("farm-north.preview_1")]); + let receipt = transport.deliver(request).expect("delivery receipt"); + + assert_eq!(receipt.target_receipts.len(), 1); + assert_eq!( + receipt.target_receipts[0] + .target + .scope + .as_ref() + .map(|scope| scope.as_str()), + Some("farm-north.preview_1") + ); + assert_eq!( + receipt.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::PreviewUnavailable + ); + assert_eq!( + receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), + 0 + ); + + let deferred_transport = RadrootsReticulumPreviewTransport::new( + RadrootsReticulumPreviewProfile::default() + .with_behavior(RadrootsReticulumPreviewBehavior::DeferDeliveryPlans), + ); + let deferred = deferred_transport + .deliver(delivery_request(vec![scoped_reticulum_target( + "farm-south.preview_2", + )])) + .expect("deferred delivery receipt"); + assert_eq!( + deferred.target_receipts[0] + .target + .scope + .as_ref() + .map(|scope| scope.as_str()), + Some("farm-south.preview_2") + ); + assert_eq!( + deferred.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + assert_eq!( + deferred.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), + 0 + ); +} + +#[test] fn reject_delivery_attempts_returns_unavailable_without_success_or_nostr_routing() { let transport = RadrootsReticulumPreviewTransport::default(); let request = delivery_request(vec![reticulum_target( @@ -293,6 +366,18 @@ fn non_reticulum_targets_are_rejected_without_nostr_routing() { } #[test] +fn malformed_reticulum_target_without_typed_scope_is_rejected() { + let transport = RadrootsReticulumPreviewTransport::default(); + let mut target = reticulum_target(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); + target.scope = None; + let err = transport + .deliver(delivery_request(vec![target])) + .expect_err("missing typed scope"); + + assert_eq!(err, RadrootsReticulumPreviewError::InvalidEndpoint); +} + +#[test] fn fetch_reports_preview_unavailable_without_observed_events() { let transport = RadrootsReticulumPreviewTransport::default(); assert_eq!( @@ -409,6 +494,25 @@ fn public_models_round_trip_through_serde() { } #[test] +fn preview_source_remains_inert_without_runtime_delivery_hooks() { + let source = include_str!("../src/lib.rs").to_ascii_lowercase(); + for forbidden in [ + "socket", + "rnsd", + "python", + "identity", + "send_mesh", + "fallback", + "nostr", + ] { + assert!( + !source.contains(forbidden), + "Reticulum preview source contains forbidden runtime hook {forbidden}" + ); + } +} + +#[test] fn reticulum_preview_errors_and_defaults_are_stable() { assert_eq!( RadrootsReticulumPreviewBehavior::default(),