Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/rns-runtime/src/reticulum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,17 @@ impl ReticulumHandle {
result
}

/// Register a wire packet tap on the local transport actor (sniffer/debug).
pub async fn register_packet_tap(
&self,
tap_tx: tokio::sync::broadcast::Sender<rns_transport::messages::PacketTapEvent>,
) {
let _ = self
.transport_tx
.send(TransportMessage::SetPacketTap { tap_tx })
.await;
}

/// Recall the identity and latest announce metadata for `destination_hash`.
///
/// This reads this process' live, validated replicated announce cache in
Expand Down
9 changes: 9 additions & 0 deletions crates/rns-transport/src/actor/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ impl TransportActor {
packet.raw.clone()
};

self.emit_packet_tap(
crate::messages::PacketTapDirection::Rx,
packet.interface_id,
&raw,
packet.rssi,
packet.snr,
packet.q,
);

let (mut parsed, data_offset) = match rns_wire::header::PacketHeader::unpack(&raw) {
Ok((header, offset)) => (header, offset),
Err(e) => {
Expand Down
50 changes: 48 additions & 2 deletions crates/rns-transport/src/actor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ pub struct TransportActor {
/// background switches the maintenance tick to the long interval so the
/// actor stops burning CPU (and battery) while the app is suspended.
pub is_foreground: Arc<AtomicBool>,

/// Optional wire packet tap — emits RX/TX frames for sniffer UIs.
packet_tap: Option<tokio::sync::broadcast::Sender<crate::messages::PacketTapEvent>>,
}

/// Cached announce metadata for diagnostics + CacheRequest replay. Raw
Expand Down Expand Up @@ -411,6 +414,7 @@ impl TransportActor {
announce_handlers: Vec::new(),
next_announce_handler_id: 0,
is_foreground: Arc::new(AtomicBool::new(true)),
packet_tap: None,
};

(actor, tx)
Expand Down Expand Up @@ -974,6 +978,9 @@ impl TransportActor {
debug!(dest = hex::encode(dest), "registered path waiter");
}
}
TransportMessage::SetPacketTap { tap_tx } => {
self.packet_tap = Some(tap_tx);
}
TransportMessage::Shutdown => unreachable!(),
}
}
Expand Down Expand Up @@ -1447,6 +1454,35 @@ impl TransportActor {
///
/// `try_send` failures: `Full` bumps `tx_drops`; `Closed` auto-deregisters
/// (zombie interface — receiver dropped without DeregisterInterface).
fn emit_packet_tap(
&self,
direction: crate::messages::PacketTapDirection,
interface_id: InterfaceId,
raw: &[u8],
rssi: Option<f32>,
snr: Option<f32>,
q: Option<f32>,
) {
let Some(tap) = self.packet_tap.as_ref() else {
return;
};
let interface_name = self
.interfaces
.get(&interface_id)
.map(|e| e.name.clone())
.unwrap_or_else(|| format!("interface_{interface_id}"));
let event = crate::messages::PacketTapEvent::from_wire(
direction,
interface_id,
interface_name,
raw,
rssi,
snr,
q,
);
let _ = tap.send(event);
}

#[tracing::instrument(
level = "trace",
name = "actor.send_to_interface",
Expand All @@ -1468,8 +1504,18 @@ impl TransportActor {
} else {
Bytes::copy_from_slice(raw)
};
match entry.tx.try_send(data) {
Ok(()) => InterfaceSendOutcome::Sent,
match entry.tx.try_send(data.clone()) {
Ok(()) => {
self.emit_packet_tap(
crate::messages::PacketTapDirection::Tx,
id,
&data,
None,
None,
None,
);
InterfaceSendOutcome::Sent
}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => InterfaceSendOutcome::Full,
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => InterfaceSendOutcome::Closed,
}
Expand Down
71 changes: 71 additions & 0 deletions crates/rns-transport/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,71 @@ pub struct PathRequestOptions {
pub recursive: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PacketTapDirection {
Rx,
Tx,
}

/// Wire-level packet observation event for debug/sniffer UIs.
#[derive(Debug, Clone)]
pub struct PacketTapEvent {
pub direction: PacketTapDirection,
pub interface_id: InterfaceId,
pub interface_name: String,
pub raw: Vec<u8>,
pub rssi: Option<f32>,
pub snr: Option<f32>,
pub q: Option<f32>,
pub packet_type: Option<String>,
pub header_type: Option<String>,
pub destination_hash: Option<[u8; 16]>,
pub transport_type: Option<String>,
pub context: Option<String>,
}

const PACKET_TAP_RAW_CAP: usize = 4096;

impl PacketTapEvent {
pub fn from_wire(
direction: PacketTapDirection,
interface_id: InterfaceId,
interface_name: String,
raw: &[u8],
rssi: Option<f32>,
snr: Option<f32>,
q: Option<f32>,
) -> Self {
let capped = if raw.len() > PACKET_TAP_RAW_CAP {
raw[..PACKET_TAP_RAW_CAP].to_vec()
} else {
raw.to_vec()
};
let mut event = Self {
direction,
interface_id,
interface_name,
raw: capped,
rssi,
snr,
q,
packet_type: None,
header_type: None,
destination_hash: None,
transport_type: None,
context: None,
};
if let Ok((header, _)) = rns_wire::header::PacketHeader::unpack(&event.raw) {
event.packet_type = Some(format!("{:?}", header.flags.packet_type));
event.header_type = Some(format!("{:?}", header.flags.header_type));
event.destination_hash = Some(header.destination_hash);
event.transport_type = Some(format!("{:?}", header.flags.transport_type));
event.context = Some(format!("{:?}", header.context));
}
event
}
}

/// Every mutation of transport state enters through this enum — the actor
/// dispatches on the variant, so adding a new operation is a matter of adding
/// a variant and a match arm rather than exposing a new lock or shared type.
Expand Down Expand Up @@ -618,6 +683,10 @@ pub enum TransportMessage {
dest: [u8; 16],
reply: tokio::sync::oneshot::Sender<bool>,
},
/// Optional wire packet tap for debug/sniffer consumers.
SetPacketTap {
tap_tx: tokio::sync::broadcast::Sender<PacketTapEvent>,
},
Shutdown,
}

Expand Down Expand Up @@ -661,6 +730,7 @@ pub fn msg_variant_name(msg: &TransportMessage) -> &'static str {
TransportMessage::RegisterLink { .. } => "RegisterLink",
TransportMessage::ActivateLink { .. } => "ActivateLink",
TransportMessage::AwaitPath { .. } => "AwaitPath",
TransportMessage::SetPacketTap { .. } => "SetPacketTap",
TransportMessage::Shutdown => "Shutdown",
}
}
Expand Down Expand Up @@ -1161,6 +1231,7 @@ impl std::fmt::Debug for TransportMessage {
Self::AwaitPath { dest, .. } => {
f.debug_struct("AwaitPath").field("dest", dest).finish()
}
Self::SetPacketTap { .. } => f.debug_struct("SetPacketTap").finish(),
Self::Shutdown => write!(f, "Shutdown"),
}
}
Expand Down