feat: wire FAPP message handling into mesh router
When a MeshEnvelope is delivered locally and its payload starts with a known FAPP wire tag (0x01-0x05), MeshNode.process_incoming now delegates to FappRouter instead of returning a raw Deliver action. Nodes without FAPP capabilities still receive FAPP-tagged payloads as normal Deliver actions, preserving backward compatibility. Adds IncomingAction::Fapp variant, is_fapp_payload() helper, and three integration tests covering the routing, passthrough, and no-router cases.
This commit is contained in:
@@ -35,6 +35,21 @@ pub const FAPP_WIRE_RESERVE: u8 = 0x04;
|
||||
/// [`SlotConfirm`](crate::fapp::SlotConfirm) frame (handled later).
|
||||
pub const FAPP_WIRE_CONFIRM: u8 = 0x05;
|
||||
|
||||
/// Check whether a raw payload starts with a known FAPP wire tag.
|
||||
///
|
||||
/// Useful for the mesh router to decide whether a delivered envelope should be
|
||||
/// routed through the [`FappRouter`] rather than the application layer.
|
||||
pub fn is_fapp_payload(payload: &[u8]) -> bool {
|
||||
matches!(
|
||||
payload.first(),
|
||||
Some(&FAPP_WIRE_ANNOUNCE)
|
||||
| Some(&FAPP_WIRE_QUERY)
|
||||
| Some(&FAPP_WIRE_RESPONSE)
|
||||
| Some(&FAPP_WIRE_RESERVE)
|
||||
| Some(&FAPP_WIRE_CONFIRM)
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FappAction — what to do after handling an incoming FAPP frame
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -455,6 +470,24 @@ mod tests {
|
||||
use crate::fapp::{Fachrichtung, Kostentraeger, Modalitaet, SlotType, TimeSlot};
|
||||
use crate::identity::MeshIdentity;
|
||||
|
||||
#[test]
|
||||
fn is_fapp_payload_recognizes_all_tags() {
|
||||
assert!(is_fapp_payload(&[FAPP_WIRE_ANNOUNCE, 0x01]));
|
||||
assert!(is_fapp_payload(&[FAPP_WIRE_QUERY, 0x01]));
|
||||
assert!(is_fapp_payload(&[FAPP_WIRE_RESPONSE, 0x01]));
|
||||
assert!(is_fapp_payload(&[FAPP_WIRE_RESERVE, 0x01]));
|
||||
assert!(is_fapp_payload(&[FAPP_WIRE_CONFIRM, 0x01]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_fapp_payload_rejects_non_fapp() {
|
||||
assert!(!is_fapp_payload(&[]));
|
||||
assert!(!is_fapp_payload(&[0x00]));
|
||||
assert!(!is_fapp_payload(&[0x06]));
|
||||
assert!(!is_fapp_payload(&[0x10])); // KeyPackageRequest tag
|
||||
assert!(!is_fapp_payload(&[0xFF]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_incoming_unknown_tag_dropped() {
|
||||
let routes = Arc::new(RwLock::new(RoutingTable::new(Duration::from_secs(300))));
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::config::MeshConfig;
|
||||
use crate::envelope::MeshEnvelope;
|
||||
use crate::error::{MeshError, MeshResult};
|
||||
use crate::fapp::{FappStore, CAP_FAPP_PATIENT, CAP_FAPP_RELAY, CAP_FAPP_THERAPIST};
|
||||
use crate::fapp_router::FappRouter;
|
||||
use crate::fapp_router::{is_fapp_payload, FappRouter};
|
||||
use crate::identity::MeshIdentity;
|
||||
use crate::mesh_router::{IncomingAction, MeshRouter};
|
||||
use crate::metrics::{self, MeshMetrics};
|
||||
@@ -336,6 +336,17 @@ impl MeshNode {
|
||||
let action = self.mesh_router.handle_incoming(envelope)
|
||||
.map_err(|e| MeshError::Internal(e.to_string()))?;
|
||||
|
||||
// If the envelope is delivered locally and its payload is a FAPP frame,
|
||||
// delegate to the FappRouter instead of returning a raw Deliver.
|
||||
let action = match action {
|
||||
IncomingAction::Deliver(ref env) if self.fapp_router.is_some() && is_fapp_payload(&env.payload) => {
|
||||
let fapp_router = self.fapp_router.as_ref().unwrap();
|
||||
let fapp_action = fapp_router.handle_incoming(&env.payload);
|
||||
IncomingAction::Fapp(fapp_action)
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
|
||||
// Update routing metrics based on action
|
||||
match &action {
|
||||
IncomingAction::Deliver(_) => {
|
||||
@@ -350,6 +361,9 @@ impl MeshNode {
|
||||
IncomingAction::Dropped(_) => {
|
||||
self.metrics.protocol.parse_errors.inc();
|
||||
}
|
||||
IncomingAction::Fapp(_) => {
|
||||
self.metrics.store.messages_delivered.inc();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(action)
|
||||
@@ -455,6 +469,8 @@ pub struct GcStats {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::envelope::MeshEnvelope;
|
||||
use crate::fapp_router::{FappAction, FAPP_WIRE_QUERY, FAPP_WIRE_ANNOUNCE};
|
||||
|
||||
#[tokio::test]
|
||||
async fn mesh_node_starts() {
|
||||
@@ -526,4 +542,112 @@ mod tests {
|
||||
|
||||
node.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fapp_payload_routed_to_fapp_router() {
|
||||
let identity = MeshIdentity::generate();
|
||||
let node_pk = identity.public_key();
|
||||
|
||||
let node = MeshNodeBuilder::new()
|
||||
.identity(identity)
|
||||
.fapp_relay()
|
||||
.build()
|
||||
.await
|
||||
.expect("build fapp node");
|
||||
|
||||
// Build a FAPP query payload (tag 0x02 + CBOR body).
|
||||
let query = crate::fapp::SlotQuery {
|
||||
query_id: [0xAA; 16],
|
||||
fachrichtung: None,
|
||||
modalitaet: None,
|
||||
kostentraeger: None,
|
||||
plz_prefix: None,
|
||||
earliest: None,
|
||||
latest: None,
|
||||
slot_type: None,
|
||||
max_results: 5,
|
||||
};
|
||||
let mut fapp_payload = vec![FAPP_WIRE_QUERY];
|
||||
ciborium::into_writer(&query, &mut fapp_payload).expect("CBOR encode");
|
||||
|
||||
// Wrap in a MeshEnvelope addressed to this node.
|
||||
let sender = MeshIdentity::generate();
|
||||
let envelope = MeshEnvelope::new(&sender, &node_pk, fapp_payload, 3600, 5);
|
||||
|
||||
let sender_addr = MeshAddress::from_public_key(&sender.public_key());
|
||||
let action = node.process_incoming(&sender_addr, envelope).expect("process");
|
||||
|
||||
match action {
|
||||
IncomingAction::Fapp(FappAction::QueryResponse(resp)) => {
|
||||
// Relay answers from its (empty) store — expect zero matches.
|
||||
assert!(resp.matches.is_empty());
|
||||
}
|
||||
other => panic!("expected Fapp(QueryResponse), got {:?}", std::mem::discriminant(&other)),
|
||||
}
|
||||
|
||||
node.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_fapp_payload_delivered_normally() {
|
||||
let identity = MeshIdentity::generate();
|
||||
let node_pk = identity.public_key();
|
||||
|
||||
let node = MeshNodeBuilder::new()
|
||||
.identity(identity)
|
||||
.fapp_relay()
|
||||
.build()
|
||||
.await
|
||||
.expect("build fapp node");
|
||||
|
||||
// A regular (non-FAPP) payload — first byte 0xFF is not a FAPP tag.
|
||||
let regular_payload = vec![0xFF, 0x01, 0x02, 0x03];
|
||||
let sender = MeshIdentity::generate();
|
||||
let envelope = MeshEnvelope::new(&sender, &node_pk, regular_payload.clone(), 3600, 5);
|
||||
|
||||
let sender_addr = MeshAddress::from_public_key(&sender.public_key());
|
||||
let action = node.process_incoming(&sender_addr, envelope).expect("process");
|
||||
|
||||
match action {
|
||||
IncomingAction::Deliver(env) => {
|
||||
assert_eq!(env.payload, regular_payload);
|
||||
}
|
||||
other => panic!("expected Deliver, got {:?}", std::mem::discriminant(&other)),
|
||||
}
|
||||
|
||||
node.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fapp_payload_without_fapp_router_delivered_normally() {
|
||||
let identity = MeshIdentity::generate();
|
||||
let node_pk = identity.public_key();
|
||||
|
||||
// No FAPP capabilities — fapp_router is None.
|
||||
let node = MeshNodeBuilder::new()
|
||||
.identity(identity)
|
||||
.build()
|
||||
.await
|
||||
.expect("build node");
|
||||
|
||||
assert!(node.fapp_router().is_none());
|
||||
|
||||
// Even though the payload has a FAPP tag, without a FappRouter it should
|
||||
// be delivered as a normal message.
|
||||
let fapp_payload = vec![FAPP_WIRE_ANNOUNCE, 0x01, 0x02];
|
||||
let sender = MeshIdentity::generate();
|
||||
let envelope = MeshEnvelope::new(&sender, &node_pk, fapp_payload.clone(), 3600, 5);
|
||||
|
||||
let sender_addr = MeshAddress::from_public_key(&sender.public_key());
|
||||
let action = node.process_incoming(&sender_addr, envelope).expect("process");
|
||||
|
||||
match action {
|
||||
IncomingAction::Deliver(env) => {
|
||||
assert_eq!(env.payload, fapp_payload);
|
||||
}
|
||||
other => panic!("expected Deliver, got {:?}", std::mem::discriminant(&other)),
|
||||
}
|
||||
|
||||
node.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use anyhow::{bail, Result};
|
||||
|
||||
use crate::announce::compute_address;
|
||||
use crate::envelope::MeshEnvelope;
|
||||
use crate::fapp_router::FappAction;
|
||||
use crate::identity::MeshIdentity;
|
||||
use crate::routing_table::RoutingTable;
|
||||
use crate::store::MeshStore;
|
||||
@@ -54,6 +55,8 @@ pub enum IncomingAction {
|
||||
Store(MeshEnvelope),
|
||||
/// Message was dropped (expired, max hops, invalid).
|
||||
Dropped(String),
|
||||
/// FAPP protocol message — handled by [`FappRouter`](crate::fapp_router::FappRouter).
|
||||
Fapp(FappAction),
|
||||
}
|
||||
|
||||
/// Per-destination delivery statistics.
|
||||
|
||||
Reference in New Issue
Block a user