PeerAllowPush: consent to exchange pushes, for a while, and nothing else
PeerTrust syncs a peer's pins and serves it everything - too much to grant a stranger. An app that wants two peers sharing something (two phones at the same place) to hand each other new content allows pushes for ttl_secs (at most a day): both sides may push, the receiver pins, and the allowance lapses on its own. Asking again extends it; PeerUntrust ends it. varde-ctl peer allow-push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0006a188e3
commit
2063d11890
@@ -566,11 +566,23 @@ impl Daemon {
|
||||
let parsed: iroh::EndpointId = node_id
|
||||
.parse()
|
||||
.map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?;
|
||||
if !self.meta.untrust_peer(&parsed.to_string())? {
|
||||
let was_trusted = self.meta.untrust_peer(&parsed.to_string())?;
|
||||
let was_allowed = self.meta.disallow_push(&parsed.to_string())?;
|
||||
if !was_trusted && !was_allowed {
|
||||
return Err(OpError::NotFound(format!("{parsed} was not trusted")));
|
||||
}
|
||||
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 ttl = ttl_secs.min(varde_proto::MAX_PUSH_TTL_SECS);
|
||||
self.meta
|
||||
.allow_push(&parsed.to_string(), crate::meta::unix_now() + ttl)?;
|
||||
debug!(peer = %parsed, ttl, "pushes allowed");
|
||||
Ok(ResponseData::Done {})
|
||||
}
|
||||
Request::PeerList {} => Ok(ResponseData::Peers {
|
||||
peers: self.present_trusted_peers(),
|
||||
}),
|
||||
@@ -579,11 +591,12 @@ impl Daemon {
|
||||
let peer: iroh::EndpointId = node_id
|
||||
.parse()
|
||||
.map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?;
|
||||
// Consent, sending side: we only push to peers we trust.
|
||||
// (The receiver independently requires that it trusts us.)
|
||||
if !self.meta.is_trusted(&peer.to_string()) {
|
||||
// Consent, sending side: we only push to peers we trust or
|
||||
// hold a push allowance for. (The receiver independently
|
||||
// requires the same of us.)
|
||||
if !self.meta.may_push(&peer.to_string()) {
|
||||
return Err(OpError::InvalidArgument(format!(
|
||||
"{peer} is not a trusted peer; trust it before pushing"
|
||||
"{peer} is neither trusted nor allowed to exchange pushes; trust it or allow pushes first"
|
||||
)));
|
||||
}
|
||||
let (_, _, complete) = self.store.presence(parsed).await?;
|
||||
|
||||
@@ -40,6 +40,10 @@ struct MetaState {
|
||||
/// Trusted peer NodeIds (z-base-32).
|
||||
#[serde(default)]
|
||||
trusted_peers: BTreeSet<String>,
|
||||
/// Peers allowed to push to us and be pushed to, until a unix time
|
||||
/// (`PeerAllowPush`): consent for that one thing, lapsing on its own.
|
||||
#[serde(default)]
|
||||
push_peers: BTreeMap<String, u64>,
|
||||
/// Hashes (hex) the user shared via ticket export: standing consent
|
||||
/// to serve them to anyone, like an `open_lan` pin.
|
||||
#[serde(default)]
|
||||
@@ -164,6 +168,31 @@ impl Meta {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Allow pushes with `node_id` until `until` (unix seconds). Extends,
|
||||
/// never shortens, an allowance already held; expired ones are
|
||||
/// dropped on the way.
|
||||
pub fn allow_push(&self, node_id: &str, until: u64) -> Result<()> {
|
||||
let now = unix_now();
|
||||
self.mutate(|s| {
|
||||
s.push_peers.retain(|_, u| *u > now);
|
||||
let entry = s.push_peers.entry(node_id.to_string()).or_insert(0);
|
||||
*entry = (*entry).max(until);
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether pushes with `node_id` are allowed now: a trusted peer, or
|
||||
/// one holding an unexpired push allowance.
|
||||
pub fn may_push(&self, node_id: &str) -> bool {
|
||||
let state = self.state.lock().expect("meta lock poisoned");
|
||||
state.trusted_peers.contains(node_id)
|
||||
|| state.push_peers.get(node_id).is_some_and(|u| *u > unix_now())
|
||||
}
|
||||
|
||||
/// End a push allowance. Returns whether there was one.
|
||||
pub fn disallow_push(&self, node_id: &str) -> Result<bool> {
|
||||
self.mutate(|s| s.push_peers.remove(node_id).is_some())
|
||||
}
|
||||
|
||||
pub fn is_trusted(&self, node_id: &str) -> bool {
|
||||
self.state
|
||||
.lock()
|
||||
@@ -173,6 +202,14 @@ impl Meta {
|
||||
}
|
||||
}
|
||||
|
||||
/// Seconds since the unix epoch.
|
||||
pub fn unix_now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -201,4 +238,27 @@ mod tests {
|
||||
assert!(meta.unpin("h").unwrap());
|
||||
assert!(!meta.is_pinned("h"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_push_allowance_lapses_and_never_shortens() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let meta = Meta::load(dir.path()).unwrap();
|
||||
let now = unix_now();
|
||||
assert!(!meta.may_push("p"));
|
||||
meta.allow_push("p", now + 600).unwrap();
|
||||
assert!(meta.may_push("p"));
|
||||
assert!(!meta.is_trusted("p"), "a push allowance is not trust");
|
||||
meta.allow_push("p", now + 10).unwrap();
|
||||
assert!(meta.may_push("p"), "a shorter ask does not cut it short");
|
||||
meta.allow_push("q", now.saturating_sub(1)).unwrap();
|
||||
assert!(!meta.may_push("q"), "expired");
|
||||
// Survives a reload.
|
||||
let again = Meta::load(dir.path()).unwrap();
|
||||
assert!(again.may_push("p"));
|
||||
assert!(again.disallow_push("p").unwrap());
|
||||
assert!(!again.may_push("p"));
|
||||
// Full trust allows pushes too.
|
||||
again.trust_peer("t").unwrap();
|
||||
assert!(again.may_push("t"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ pub struct Gate {
|
||||
#[derive(Debug, Clone)]
|
||||
struct ConnInfo {
|
||||
trusted: bool,
|
||||
/// Trusted, or holding a push allowance (`PeerAllowPush`).
|
||||
may_push: bool,
|
||||
endpoint_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -142,6 +144,11 @@ pub fn provider_events(gate: Gate) -> EventSender {
|
||||
.endpoint_id
|
||||
.map(|id| gate.meta.is_trusted(&id.to_string()))
|
||||
.unwrap_or(false);
|
||||
let may_push = m
|
||||
.inner
|
||||
.endpoint_id
|
||||
.map(|id| gate.meta.may_push(&id.to_string()))
|
||||
.unwrap_or(false);
|
||||
let ClientConnected {
|
||||
connection_id,
|
||||
endpoint_id,
|
||||
@@ -151,6 +158,7 @@ pub fn provider_events(gate: Gate) -> EventSender {
|
||||
connection_id,
|
||||
ConnInfo {
|
||||
trusted,
|
||||
may_push,
|
||||
endpoint_id: endpoint_id.map(|id| id.to_string()),
|
||||
},
|
||||
);
|
||||
@@ -208,12 +216,13 @@ pub fn provider_events(gate: Gate) -> EventSender {
|
||||
m.tx.send(verdict).await.ok();
|
||||
}
|
||||
ProviderMessage::PushRequestReceived(m) => {
|
||||
// Inbound writes: trusted peers only, deferred while
|
||||
// metered (RateLimited invites a retry later).
|
||||
// Inbound writes: trusted peers and push allowances
|
||||
// only, deferred while metered (RateLimited invites a
|
||||
// retry later).
|
||||
let info = conns.get(&m.inner.connection_id);
|
||||
let trusted = info.map(|i| i.trusted).unwrap_or(false);
|
||||
debug!(hash = %m.inner.request.hash.to_hex(), trusted, "push request received");
|
||||
let verdict = if !trusted {
|
||||
let may_push = info.map(|i| i.may_push).unwrap_or(false);
|
||||
debug!(hash = %m.inner.request.hash.to_hex(), may_push, "push request received");
|
||||
let verdict = if !may_push {
|
||||
Err(AbortReason::Permission)
|
||||
} else if gate.metered.is_metered() {
|
||||
Err(AbortReason::RateLimited)
|
||||
|
||||
Reference in New Issue
Block a user