From c2b9167e1ea66d8c4e19a10e0211bf103c0a2130 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Sat, 3 Oct 2026 13:29:03 +0200 Subject: [PATCH] Pushes name the peer by ticket, and an allowance can confirm its push PeerAllowPush and Push take a blob ticket the peer issued as well as a NodeId; the ticket's addresses are remembered, so the push dials directly. A peer allowed to push may observe too: the sender confirms a push by observing it, and without that every allowance-only push reported failure though it had landed. The allowance test now fails instead of skipping on anything but an unreachable peer. Co-Authored-By: Claude Opus 5.5 --- varde-daemon/src/daemon.rs | 28 ++++++++++++++++++++++------ varde-daemon/src/shaped.rs | 7 ++++--- varde-daemon/src/transfer.rs | 9 +++++++++ varde-daemon/tests/lan_trust.rs | 20 ++++++++++++++++---- varde-proto/src/lib.rs | 6 +++++- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/varde-daemon/src/daemon.rs b/varde-daemon/src/daemon.rs index 1fa3217..33507fa 100644 --- a/varde-daemon/src/daemon.rs +++ b/varde-daemon/src/daemon.rs @@ -335,6 +335,26 @@ impl Daemon { } } + /// The peer a request names: a NodeId, or a blob ticket from it (whose + /// addresses are remembered, so dialing it needs no discovery). An app + /// that knows peers by the tickets they hand out - redoal does - can + /// allow and push without unpacking them. + fn peer_of(&self, peer: &str) -> Result { + if let Ok(id) = peer.parse::() { + return Ok(id); + } + match crate::transfer::parse_ticket(peer) { + Ok((_, _, addr)) => { + let id = addr.id; + self.transfer.remember(addr); + Ok(id) + } + Err(_) => Err(OpError::InvalidArgument(format!( + "{peer:?} is neither a node id nor a ticket" + ))), + } + } + /// Trusted peers seen on the LAN recently. fn present_trusted_peers(&self) -> Vec { let peers = self.lan_peers.lock().expect("lan peers lock"); @@ -574,9 +594,7 @@ impl Daemon { Ok(ResponseData::Done {}) } Request::PeerAllowPush { node_id, ttl_secs } => { - let parsed: iroh::EndpointId = node_id - .parse() - .map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?; + let parsed = self.peer_of(&node_id)?; let ttl = ttl_secs.min(varde_proto::MAX_PUSH_TTL_SECS); self.meta .allow_push(&parsed.to_string(), crate::meta::unix_now() + ttl)?; @@ -588,9 +606,7 @@ impl Daemon { }), Request::Push { hash, node_id } => { let parsed = parse_hash(&hash)?; - let peer: iroh::EndpointId = node_id - .parse() - .map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?; + let peer = self.peer_of(&node_id)?; // Consent, sending side: we only push to peers we trust or // hold a push allowance for. (The receiver independently // requires the same of us.) diff --git a/varde-daemon/src/shaped.rs b/varde-daemon/src/shaped.rs index 7700546..bd93d7e 100644 --- a/varde-daemon/src/shaped.rs +++ b/varde-daemon/src/shaped.rs @@ -200,11 +200,12 @@ pub fn provider_events(gate: Gate) -> EventSender { ProviderMessage::ObserveRequestReceived(m) => { // Presence is information too: gate it like data. // Bitfields are metadata, not payload, so trusted - // peers may observe even while serving is off or the - // connection is metered (push confirmation needs it). + // peers - and peers allowed to push, whose push is + // confirmed by observing it - may observe even while + // serving is off or the connection is metered. let trusted = conns .get(&m.inner.connection_id) - .map(|i| i.trusted) + .map(|i| i.trusted || i.may_push) .unwrap_or(false); let allowed = trusted || allow(&gate, &conns, m.inner.connection_id, [m.inner.request.hash]); diff --git a/varde-daemon/src/transfer.rs b/varde-daemon/src/transfer.rs index 9ac3d21..57465ba 100644 --- a/varde-daemon/src/transfer.rs +++ b/varde-daemon/src/transfer.rs @@ -298,6 +298,15 @@ impl Transfer { Ok(ticket.to_string()) } + /// Teach the endpoint where a peer can be reached (the addresses a + /// ticket carries), so a push dials it directly instead of waiting on + /// discovery. + pub fn remember(&self, addr: EndpointAddr) { + if !addr.addrs.is_empty() { + self.known_addrs.add_endpoint_info(addr); + } + } + /// Queue a background fetch of `content` from `providers`. Emits /// progress events and [`Event::PinComplete`] when fully verified. pub fn spawn_fetch(&self, content: HashAndFormat, providers: Vec) { diff --git a/varde-daemon/tests/lan_trust.rs b/varde-daemon/tests/lan_trust.rs index b76ab27..978a115 100644 --- a/varde-daemon/tests/lan_trust.rs +++ b/varde-daemon/tests/lan_trust.rs @@ -437,7 +437,6 @@ fn push_allowance_is_enough_and_is_not_trust() { let mut client_a = support::Client::connect(&a.socket); let mut client_b = support::Client::connect(&b.socket); let id_a = node_id(&mut client_a); - let id_b = node_id(&mut client_b); let dir = tempfile::tempdir().unwrap(); let src = dir.path().join("voice.bin"); @@ -446,7 +445,16 @@ fn push_allowance_is_enough_and_is_not_trust() { let hash = add_file(&mut client_a, &src); pin(&mut client_a, &hash, false); - assert!(client_a.request(&Request::PeerAllowPush { node_id: id_b.clone(), ttl_secs: 600 }).ok); + // A knows B by a ticket B handed out (as redoal peers do), not by + // its bare node id. + let b_src = dir.path().join("b-own.bin"); + std::fs::write(&b_src, vec![1u8; 1_000]).unwrap(); + let b_hash = add_file(&mut client_b, &b_src); + let b_ticket = match client_b.request(&Request::TicketExport { hash: b_hash }).data { + Some(ResponseData::Ticket { ticket }) => ticket, + other => panic!("unexpected ticket reply: {other:?}"), + }; + assert!(client_a.request(&Request::PeerAllowPush { node_id: b_ticket.clone(), ttl_secs: 600 }).ok); assert!(client_b.request(&Request::PeerAllowPush { node_id: id_a, ttl_secs: 600 }).ok); match client_a.request(&Request::PeerList {}).data { Some(ResponseData::Peers { peers }) => assert!(peers.is_empty(), "an allowance is not trust: {peers:?}"), @@ -456,14 +464,18 @@ fn push_allowance_is_enough_and_is_not_trust() { // The peer's address arrives by mdns; retry until it does. let deadline = Instant::now() + Duration::from_secs(30); let reply = loop { - let reply = client_a.request(&Request::Push { hash: hash.clone(), node_id: id_b.clone() }); + let reply = client_a.request(&Request::Push { hash: hash.clone(), node_id: b_ticket.clone() }); if reply.ok || Instant::now() > deadline { break reply; } std::thread::sleep(Duration::from_millis(500)); }; if !reply.ok { - eprintln!("SKIP: push never reached the peer (multicast unavailable?): {:?}", reply.error); + let message = reply.error.map(|e| e.message).unwrap_or_default(); + // Only an unreachable peer is the environment's fault; any other + // refusal is a real failure. + assert!(message.contains("connect"), "push failed: {message}"); + eprintln!("SKIP: push never reached the peer (multicast unavailable?): {message}"); return; } assert!(wait_complete(&b.socket, &hash, Duration::from_secs(60)), "pushed content not complete on receiver"); diff --git a/varde-proto/src/lib.rs b/varde-proto/src/lib.rs index 58e4b47..9f6df55 100644 --- a/varde-proto/src/lib.rs +++ b/varde-proto/src/lib.rs @@ -127,7 +127,8 @@ pub enum Request { /// phones at the same place) and let it lapse. Asking again extends /// it; [`Request::PeerUntrust`] ends it early. Added in varde 0.1.5. PeerAllowPush { - /// The peer's iroh NodeId (z-base-32). + /// The peer's iroh NodeId (z-base-32), or a blob ticket it issued + /// (varde 0.1.6+), whose addresses are then used to reach it. node_id: String, /// How long the allowance lasts, in seconds. ttl_secs: u64, @@ -142,6 +143,9 @@ pub enum Request { /// policy), so the content survives gc and shows up in `List`. Push { /// Hash (hex) of the fully-present blob or HashSeq root to push. + /// + /// The receiver below may be a NodeId or, from varde 0.1.6, a blob + /// ticket it issued. hash: String, /// The receiving peer's iroh NodeId (z-base-32). node_id: String,