Stand up the friends control plane: a persistent-identity iroh endpoint that's online for the whole GUI session, separate from the ephemeral video sessions, ready to carry friend requests and pushed share-codes. Identity split by plane (common/endpoint.rs): the video plane (host/ viewer) goes back to ephemeral per-session keypairs, while the new bind_control() binds with the machine's persistent identity. They must differ — the GUI's control endpoint and a host's video endpoint can be live at once, and iroh routes by EndpointId, so a shared id would make relay delivery ambiguous. Bonus: a screen-share now leaks no stable id. common/control.rs — the protocol: a ControlMsg enum (Hello / Friend Request / FriendAccept / FriendDecline / ShareCode) with one-message- per-connection framing (EOF-delimited JSON) and a one-byte ACK the receiver returns only after a successful parse, so send() gets a real delivered/failed signal (the basis for the later code-push queue). The sender id is taken from the connection's verified remote key, never the payload. send() takes impl Into<EndpointAddr> so production dials a bare EndpointId (discovery resolves it) while tests use a full addr. gui/presence.rs — the service: a dedicated thread + current-thread tokio runtime (mirroring the tray) binds the control endpoint and runs the accept loop, bridging inbound messages to a std mpsc the UI drains each tick and pinging the Waker so they land even while hidden to the tray. The whole friends stack (identity, control, CONTROL_ALPN, bind_control) is gated behind the `gui` feature — a headless CLI host runs no presence service — keeping the headless build lean and warning-free. Verified: loopback test delivers a FriendRequest across two real iroh endpoints with the correct authenticated sender id; the live GUI binds its control endpoint on launch under the persistent identity. fmt + clippy clean on both feature sets; headless and gui test suites pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
3.3 KiB
Rust
85 lines
3.3 KiB
Rust
//! Shared iroh endpoint construction.
|
|
//!
|
|
//! Two planes, two identities:
|
|
//!
|
|
//! * The **video** plane (host/viewer sessions) binds with an *ephemeral*
|
|
//! keypair — a fresh `EndpointId` per run. Each session is a throwaway tunnel,
|
|
//! and keeping its id ephemeral means a screen-share leaks no stable
|
|
//! fingerprint.
|
|
//! * The **control** plane (the always-on friends presence service) binds with
|
|
//! the machine's *persistent* identity (see [`identity`]), so peers can find
|
|
//! and recognise each other across launches.
|
|
//!
|
|
//! They must use different identities because both can be live at once on the
|
|
//! same machine (the GUI's control endpoint while a host session runs), and
|
|
//! iroh routes by `EndpointId` — two live endpoints sharing one id would make
|
|
//! relay delivery ambiguous.
|
|
|
|
use std::str::FromStr;
|
|
|
|
use anyhow::{Context, Result};
|
|
use iroh::endpoint::presets;
|
|
use iroh::{Endpoint, RelayMap, RelayMode, RelayUrl};
|
|
|
|
use super::alpn::ALPN;
|
|
|
|
/// Environment variable consulted when `--relay` isn't passed. Lets the GUI's
|
|
/// child processes and scripted runs inherit a relay choice without a flag.
|
|
pub const RELAY_ENV: &str = "PIXELPASS_RELAY";
|
|
|
|
/// Resolve the relay override: explicit `--relay` wins, else `PIXELPASS_RELAY`,
|
|
/// else `None` (use the bundled defaults).
|
|
pub fn relay_override(flag: Option<&str>) -> Option<String> {
|
|
flag.map(str::to_owned).or_else(|| {
|
|
std::env::var(RELAY_ENV)
|
|
.ok()
|
|
.filter(|s| !s.trim().is_empty())
|
|
})
|
|
}
|
|
|
|
/// Bind a **video-plane** endpoint (host/viewer) with an ephemeral identity.
|
|
///
|
|
/// With no `relay` override we use [`presets::N0`] — n0 DNS discovery, the
|
|
/// library's default relays, and the chosen crypto provider. With an override
|
|
/// we keep all of that but swap in a single custom relay via
|
|
/// [`RelayMode::Custom`]; this is how a user gets off the rc's bundled
|
|
/// (canary-grade) relays or points at a self-hosted one. Discovery is
|
|
/// unchanged, so peers still resolve each other by endpoint id.
|
|
pub async fn bind(relay: Option<&str>) -> Result<Endpoint> {
|
|
// No `secret_key` set → iroh mints a fresh ephemeral keypair for this run.
|
|
bind_with(relay, None, ALPN).await
|
|
}
|
|
|
|
/// Bind the **control-plane** endpoint with the machine's persistent identity
|
|
/// (see [`super::identity`]) and the friends [`super::alpn::CONTROL_ALPN`]. Its
|
|
/// `EndpointId` is the stable id friends know you by.
|
|
#[cfg(feature = "gui")]
|
|
pub async fn bind_control(relay: Option<&str>) -> Result<Endpoint> {
|
|
let secret_key = super::identity::load_or_create()?;
|
|
bind_with(relay, Some(secret_key), super::alpn::CONTROL_ALPN).await
|
|
}
|
|
|
|
/// Shared builder: optional persistent key (None → ephemeral) + the plane's ALPN.
|
|
async fn bind_with(
|
|
relay: Option<&str>,
|
|
key: Option<iroh::SecretKey>,
|
|
alpn: &[u8],
|
|
) -> Result<Endpoint> {
|
|
let mut builder = Endpoint::builder(presets::N0).alpns(vec![alpn.to_vec()]);
|
|
if let Some(key) = key {
|
|
builder = builder.secret_key(key);
|
|
}
|
|
|
|
if let Some(url) = relay {
|
|
let url = RelayUrl::from_str(url).with_context(|| {
|
|
format!("invalid relay URL {url:?} (expected e.g. https://relay.example/)")
|
|
})?;
|
|
builder = builder.relay_mode(RelayMode::Custom(RelayMap::from(url)));
|
|
}
|
|
|
|
builder
|
|
.bind()
|
|
.await
|
|
.context("failed to bind the iroh endpoint")
|
|
}
|