fix(mesh): route ContentRef over federation when >160B
mesh.send-content was failing with "Message too large for LoRa: 624
bytes (max 160)" because a single ContentRef envelope (cid + onion +
cap_token + thumb) dwarfs a LoRa frame. Add a federation Tor fallback:
- New POST /archipelago/mesh-typed endpoint accepts
{from_pubkey, typed_envelope_b64, signature}, verifies ed25519 over
the raw wire bytes, and injects the decoded envelope into MeshState
via a new MeshService::inject_typed_from_federation helper. This
shares the same dispatch match as LoRa receives via a new pub(crate)
handle_typed_envelope_direct extracted from handle_typed_message.
- MeshService::send_typed_wire_via_federation POSTs the signed wire to
a peer's onion over TOR_SOCKS_PROXY and records a local Sent record.
- handle_mesh_send_content looks up the peer's onion in federation
storage and routes via federation when available, falling back to
LoRa only when no federation presence is known (still fails on
oversized — chunking is Phase 4).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8d868a1d12
commit
06584a3821
@@ -527,6 +527,112 @@ impl MeshService {
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Send a typed envelope to a peer via the federation (Tor) path rather
|
||||
/// than LoRa. Used when the envelope exceeds LoRa's per-frame budget —
|
||||
/// ContentRef is the canonical example, at 400+ bytes with thumb + cap.
|
||||
/// The peer's onion must already be in federation storage. Records a
|
||||
/// Sent MeshMessage locally on success so the UI gets the rich card.
|
||||
///
|
||||
/// This does NOT use chunking and does NOT go through the mesh radio —
|
||||
/// it is a straight HTTP POST over Tor to the peer's
|
||||
/// `/archipelago/mesh-typed` endpoint.
|
||||
pub async fn send_typed_wire_via_federation(
|
||||
&self,
|
||||
contact_id: u32,
|
||||
peer_onion: &str,
|
||||
wire: Vec<u8>,
|
||||
type_label: &str,
|
||||
display_text: &str,
|
||||
typed_payload: Option<serde_json::Value>,
|
||||
sender_seq: u64,
|
||||
) -> Result<MeshMessage> {
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use ed25519_dalek::Signer;
|
||||
|
||||
let host = if peer_onion.ends_with(".onion") {
|
||||
peer_onion.to_string()
|
||||
} else {
|
||||
format!("{}.onion", peer_onion.trim_end_matches('/'))
|
||||
};
|
||||
let url = format!("http://{}/archipelago/mesh-typed", host);
|
||||
|
||||
// Sign the raw wire bytes so the receiver can attribute the envelope
|
||||
// to our pubkey even when it arrives over federation/Tor rather than
|
||||
// the radio. Signature covers the wire only — the receiver re-hashes.
|
||||
let signature = hex::encode(self.signing_key.sign(&wire).to_bytes());
|
||||
let wire_b64 = BASE64.encode(&wire);
|
||||
let body = serde_json::json!({
|
||||
"from_pubkey": self.our_ed_pubkey_hex,
|
||||
"from_name": self.our_did,
|
||||
"typed_envelope_b64": wire_b64,
|
||||
"signature": signature,
|
||||
});
|
||||
|
||||
let proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid Tor proxy: {}", e))?;
|
||||
let client = reqwest::Client::builder()
|
||||
.proxy(proxy)
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| anyhow::anyhow!("HTTP client build failed: {}", e))?;
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Federation POST failed: {}", e))?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Peer rejected typed envelope: HTTP {}", resp.status());
|
||||
}
|
||||
Ok(self
|
||||
.record_sent_typed(contact_id, type_label, display_text, typed_payload, sender_seq)
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Inject a typed envelope received over federation (Tor) into MeshState
|
||||
/// as if it had arrived over the mesh radio. Looks up contact_id by
|
||||
/// matching pubkey_hex against known mesh peers; falls back to a
|
||||
/// synthetic id derived from the pubkey bytes so the UI can still
|
||||
/// address the chat (will render as a new "peer" if not in the list).
|
||||
pub async fn inject_typed_from_federation(
|
||||
&self,
|
||||
from_pubkey_hex: &str,
|
||||
from_name: Option<&str>,
|
||||
wire: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let envelope = crate::mesh::message_types::TypedEnvelope::from_wire(&wire)?;
|
||||
// Find the contact_id by pubkey match; fall back to synthetic.
|
||||
let contact_id = {
|
||||
let peers = self.state.peers.read().await;
|
||||
peers
|
||||
.iter()
|
||||
.find_map(|(cid, p)| {
|
||||
if p.pubkey_hex.as_deref() == Some(from_pubkey_hex) {
|
||||
Some(*cid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let bytes = hex::decode(from_pubkey_hex).unwrap_or_default();
|
||||
if bytes.len() >= 4 {
|
||||
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
};
|
||||
let display_name = from_name.unwrap_or("federation peer").to_string();
|
||||
listener::dispatch::handle_typed_envelope_direct(
|
||||
&self.state,
|
||||
contact_id,
|
||||
&display_name,
|
||||
envelope,
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allocate the next outbound seq for a target. Convenience passthrough
|
||||
/// to MeshState::next_send_seq; used by RPC handlers before encoding a
|
||||
/// TypedEnvelope so the seq on the wire matches the Sent record.
|
||||
|
||||
Reference in New Issue
Block a user