Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions node/src/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ pub struct Mesh {
/// the fleet-conscription takeover (AMS-01). Refreshed on ownership changes
/// and on a periodic tick.
fleet_authorized: Mutex<std::collections::HashSet<String>>,
/// Canonical pubkeys of devices THIS node has sent an ownership `Claim` to
/// and is awaiting a `Claimed` confirmation from. An inbound
/// `OwnershipControl::Claimed` is honoured only when its authenticated
/// sender is in this set — and the entry is consumed on use — so an
/// *unsolicited* `Claimed` from an arbitrary peer can't drive itself into
/// this device's fleet member list and signed roster (which
/// [`Mesh::sender_may_control`] trusts), i.e. can't hand itself control of
/// this machine. The outbound-claim mirror of the per-sender guards the
/// other ownership arms already apply. In-memory only: a claim interrupted
/// by a restart simply needs re-issuing.
pending_claims: Mutex<std::collections::HashSet<String>>,
/// Latest passive clock-skew sample per peer (ms; positive = the peer's
/// wall clock reads ahead of ours) with when it landed, from the
/// `sent_at` stamp presence adverts carry. Fed to the network verdict in
Expand Down Expand Up @@ -1025,6 +1036,7 @@ impl Mesh {
}),
ownership: Arc::new(Ownership::load()),
fleet_authorized: Mutex::new(std::collections::HashSet::new()),
pending_claims: Mutex::new(std::collections::HashSet::new()),
peer_clock_skew: Mutex::new(HashMap::new()),
clock_skew_warned: std::sync::atomic::AtomicBool::new(false),
offer_first_seen: Mutex::new(HashMap::new()),
Expand Down Expand Up @@ -2899,6 +2911,44 @@ impl Mesh {
.await;
return;
}
// Media-source gate: a Display/Video/Audio offer whose
// source endpoint is a capability on THIS machine makes
// us capture our own screen/camera/microphone and stream
// it to the offerer — every bit as sensitive as letting
// them drive us, but `route_drive_plane` never classified
// it, so the `authorized` computed above is
// unconditionally true for it. Require the same
// owner/fleet-or-explicit-grant authority here. (A known
// CEC technician without live consent was already refused
// just above; `sender_may_source_media` honours a
// technician's live ScreenView grant and person-to-person
// screen/camera/mic shares.)
if hosts_here
&& matches!(
route.media,
MediaKind::Display | MediaKind::Video | MediaKind::Audio
)
&& !self.sender_may_source_media(&from, route)
{
tracing::warn!(
"media source offer {} from {} refused: not owner/fleet/share",
route.id,
short_id(&from)
);
let _ = self
.send_control(
&from,
&ControlMessage::Route(RouteControl::Reject {
route_id: route.id.clone(),
reason:
"not authorized: capturing this device's screen, \
camera, or microphone needs owner/fleet or a share"
.into(),
}),
)
.await;
return;
}
if let Some(reason) =
privileged_offer_refusal(route, hosts_here, authorized)
{
Expand Down Expand Up @@ -5939,6 +5989,27 @@ impl Mesh {
}
}
OwnershipControl::Claimed { owner } => {
// Honour a claim confirmation only from a device THIS node
// actually sent a `Claim` to. Without this guard any
// authenticated peer could send an *unsolicited* `Claimed` and
// drive itself into our fleet member list *and* signed roster —
// both of which `sender_may_control` trusts — i.e. hand itself
// full control of this machine (input, shell, disk, clipboard).
// This is the outbound-claim mirror of the per-sender guards the
// sibling arms already apply (`Release`/`FleetKey` check the
// recorded owner). Consumed on use, so a replayed or duplicate
// confirmation is ignored.
if !self
.pending_claims
.lock()
.remove(pubkey_part(from.as_str()))
{
tracing::warn!(
"ignoring unsolicited claim confirmation from {} — this device never claimed it",
short_id(from.as_str())
);
return;
}
// The device we claimed (`from`) accepted us as its owner.
// Make the claim *do* something durable: mint our fleet key on
// the first adoption, record ourselves and the new device in
Expand Down Expand Up @@ -6526,6 +6597,14 @@ impl Mesh {
);
}
}
// Record that we're now awaiting this device's `Claimed` confirmation,
// so the inbound handler honours only a confirmation we actually
// solicited (see `pending_claims` / the `Claimed` arm). Recorded before
// the send; if the send fails the peer never answers, so the leftover
// entry is harmless.
self.pending_claims
.lock()
.insert(pubkey_part(&node).to_string());
tracing::info!("claiming {} (sending ownership claim)", short_id(&node));
let msg = ControlMessage::Ownership(OwnershipControl::Claim { owner: me.into() });
self.send_control(&node, &msg).await
Expand Down Expand Up @@ -11088,6 +11167,49 @@ impl Mesh {
.is_allowed(sender, allmystuff_cec_consent::Capability::ScreenView)
}

/// Whether `sender` may make THIS machine *source* (capture and stream) the
/// media `route` asks for — its screen, camera, or microphone. This is the
/// capture-side twin of [`Self::sender_may_drive`]: [`route_drive_plane`]
/// only ever classified the drive planes (input/terminal/files/sites/
/// clipboard), so a `Display`/`Video`/`Audio` *source* offer sailed past the
/// offer gate and reached `start_media` with no authorization at all — the
/// gap that let any authenticated peer pull this device's screen, webcam, or
/// mic. Owner and fleet may source anything; a dialed CEC technician's live
/// `ScreenView` consent covers the screen kinds (`Display`/`Video`; audio is
/// not part of the CEC consent model); otherwise it takes an explicit
/// person-to-person share this device extended that lets that person
/// *consume* this media on this capability (the "Receive your screen"
/// grant). Mirrors the owner/fleet → CEC → share layering of
/// [`Self::may_drive`]; screen viewing stays gated here at the offer (and
/// torn down by [`Self::spawn_cec_consent_sweep`]), never per frame.
fn sender_may_source_media(&self, sender: &str, route: &Route) -> bool {
if self.sender_may_control(sender) {
return true;
}
// A dialed CEC technician holds no fleet membership; their authority to
// view the screen is the customer's live ScreenView consent grant.
if matches!(route.media, MediaKind::Display | MediaKind::Video)
&& self
.cec
.is_allowed(sender, allmystuff_cec_consent::Capability::ScreenView)
{
return true;
}
// An explicit share this device extended letting that person consume
// exactly this media on this capability (honours capability pinning and
// the media/role scope via the canonical `Grant::permits`).
let Some(person) = self.shares.person_for_node(pubkey_part(sender)) else {
return false;
};
self.shares.out_grants_for(&person.id).iter().any(|g| {
g.permits(
route.media,
allmystuff_graph::GrantRole::Consume,
&route.from,
)
})
}

/// Media keeps arriving for a route this side doesn't hold live — our
/// app restarted (fresh session, old routes gone), or the route tore
/// down here while the sender missed it. Tell the sender, rate-limited:
Expand Down
Loading