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 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-10-03 13:29:03 +02:00
co-authored by Claude Opus 5.5
parent 4d9985c9cb
commit c2b9167e1e
5 changed files with 56 additions and 14 deletions
+22 -6
View File
@@ -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<iroh::EndpointId, OpError> {
if let Ok(id) = peer.parse::<iroh::EndpointId>() {
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. /// Trusted peers seen on the LAN recently.
fn present_trusted_peers(&self) -> Vec<PeerInfo> { fn present_trusted_peers(&self) -> Vec<PeerInfo> {
let peers = self.lan_peers.lock().expect("lan peers lock"); let peers = self.lan_peers.lock().expect("lan peers lock");
@@ -574,9 +594,7 @@ impl Daemon {
Ok(ResponseData::Done {}) Ok(ResponseData::Done {})
} }
Request::PeerAllowPush { node_id, ttl_secs } => { Request::PeerAllowPush { node_id, ttl_secs } => {
let parsed: iroh::EndpointId = node_id let parsed = self.peer_of(&node_id)?;
.parse()
.map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?;
let ttl = ttl_secs.min(varde_proto::MAX_PUSH_TTL_SECS); let ttl = ttl_secs.min(varde_proto::MAX_PUSH_TTL_SECS);
self.meta self.meta
.allow_push(&parsed.to_string(), crate::meta::unix_now() + ttl)?; .allow_push(&parsed.to_string(), crate::meta::unix_now() + ttl)?;
@@ -588,9 +606,7 @@ impl Daemon {
}), }),
Request::Push { hash, node_id } => { Request::Push { hash, node_id } => {
let parsed = parse_hash(&hash)?; let parsed = parse_hash(&hash)?;
let peer: iroh::EndpointId = node_id let peer = self.peer_of(&node_id)?;
.parse()
.map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?;
// Consent, sending side: we only push to peers we trust or // Consent, sending side: we only push to peers we trust or
// hold a push allowance for. (The receiver independently // hold a push allowance for. (The receiver independently
// requires the same of us.) // requires the same of us.)
+4 -3
View File
@@ -200,11 +200,12 @@ pub fn provider_events(gate: Gate) -> EventSender {
ProviderMessage::ObserveRequestReceived(m) => { ProviderMessage::ObserveRequestReceived(m) => {
// Presence is information too: gate it like data. // Presence is information too: gate it like data.
// Bitfields are metadata, not payload, so trusted // Bitfields are metadata, not payload, so trusted
// peers may observe even while serving is off or the // peers - and peers allowed to push, whose push is
// connection is metered (push confirmation needs it). // confirmed by observing it - may observe even while
// serving is off or the connection is metered.
let trusted = conns let trusted = conns
.get(&m.inner.connection_id) .get(&m.inner.connection_id)
.map(|i| i.trusted) .map(|i| i.trusted || i.may_push)
.unwrap_or(false); .unwrap_or(false);
let allowed = trusted let allowed = trusted
|| allow(&gate, &conns, m.inner.connection_id, [m.inner.request.hash]); || allow(&gate, &conns, m.inner.connection_id, [m.inner.request.hash]);
+9
View File
@@ -298,6 +298,15 @@ impl Transfer {
Ok(ticket.to_string()) 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 /// Queue a background fetch of `content` from `providers`. Emits
/// progress events and [`Event::PinComplete`] when fully verified. /// progress events and [`Event::PinComplete`] when fully verified.
pub fn spawn_fetch(&self, content: HashAndFormat, providers: Vec<EndpointAddr>) { pub fn spawn_fetch(&self, content: HashAndFormat, providers: Vec<EndpointAddr>) {
+16 -4
View File
@@ -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_a = support::Client::connect(&a.socket);
let mut client_b = support::Client::connect(&b.socket); let mut client_b = support::Client::connect(&b.socket);
let id_a = node_id(&mut client_a); let id_a = node_id(&mut client_a);
let id_b = node_id(&mut client_b);
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("voice.bin"); 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); let hash = add_file(&mut client_a, &src);
pin(&mut client_a, &hash, false); 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); assert!(client_b.request(&Request::PeerAllowPush { node_id: id_a, ttl_secs: 600 }).ok);
match client_a.request(&Request::PeerList {}).data { match client_a.request(&Request::PeerList {}).data {
Some(ResponseData::Peers { peers }) => assert!(peers.is_empty(), "an allowance is not trust: {peers:?}"), 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. // The peer's address arrives by mdns; retry until it does.
let deadline = Instant::now() + Duration::from_secs(30); let deadline = Instant::now() + Duration::from_secs(30);
let reply = loop { 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 { if reply.ok || Instant::now() > deadline {
break reply; break reply;
} }
std::thread::sleep(Duration::from_millis(500)); std::thread::sleep(Duration::from_millis(500));
}; };
if !reply.ok { 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; return;
} }
assert!(wait_complete(&b.socket, &hash, Duration::from_secs(60)), "pushed content not complete on receiver"); assert!(wait_complete(&b.socket, &hash, Duration::from_secs(60)), "pushed content not complete on receiver");
+5 -1
View File
@@ -127,7 +127,8 @@ pub enum Request {
/// phones at the same place) and let it lapse. Asking again extends /// phones at the same place) and let it lapse. Asking again extends
/// it; [`Request::PeerUntrust`] ends it early. Added in varde 0.1.5. /// it; [`Request::PeerUntrust`] ends it early. Added in varde 0.1.5.
PeerAllowPush { 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, node_id: String,
/// How long the allowance lasts, in seconds. /// How long the allowance lasts, in seconds.
ttl_secs: u64, ttl_secs: u64,
@@ -142,6 +143,9 @@ pub enum Request {
/// policy), so the content survives gc and shows up in `List`. /// policy), so the content survives gc and shows up in `List`.
Push { Push {
/// Hash (hex) of the fully-present blob or HashSeq root to 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, hash: String,
/// The receiving peer's iroh NodeId (z-base-32). /// The receiving peer's iroh NodeId (z-base-32).
node_id: String, node_id: String,