diff --git a/src/cli.rs b/src/cli.rs index 57d48ab..7103324 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -68,6 +68,13 @@ pub struct Cli { pub port: u16, // ── global ──────────────────────────────────────────────────────── + /// Relay server URL to use instead of the bundled defaults, e.g. + /// `https://relay.example/`. Applies to both host and viewer. Falls back + /// to the `PIXELPASS_RELAY` environment variable. Use this to get off the + /// pre-release default relays or to point at a self-hosted relay. + #[arg(long, value_name = "URL")] + pub relay: Option, + /// Launch the graphical front-end (a window with Host/View controls) /// instead of the terminal menu. Requires a build with `--features gui`. #[arg(long)] @@ -140,12 +147,16 @@ pub struct HostOpts { pub no_hwencode: bool, pub max_viewers: Option, pub interactive: bool, + /// Relay override (resolved from `--relay` / `PIXELPASS_RELAY`); None = defaults. + pub relay: Option, } #[derive(Debug, Clone)] pub struct ViewerOpts { pub port: u16, pub interactive: bool, + /// Relay override (resolved from `--relay` / `PIXELPASS_RELAY`); None = defaults. + pub relay: Option, } impl Cli { @@ -163,10 +174,15 @@ impl Cli { no_hwencode: self.no_hwencode, max_viewers: self.max_viewers, interactive, + relay: crate::common::endpoint::relay_override(self.relay.as_deref()), } } pub fn into_viewer_opts(self, interactive: bool) -> ViewerOpts { - ViewerOpts { port: self.port, interactive } + ViewerOpts { + port: self.port, + interactive, + relay: crate::common::endpoint::relay_override(self.relay.as_deref()), + } } } diff --git a/src/common/endpoint.rs b/src/common/endpoint.rs new file mode 100644 index 0000000..b78ecee --- /dev/null +++ b/src/common/endpoint.rs @@ -0,0 +1,42 @@ +//! Shared iroh endpoint construction for the host and viewer. +//! +//! Both sides bind an endpoint with the same ALPN; the only knob is the relay. + +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 { + flag.map(str::to_owned) + .or_else(|| std::env::var(RELAY_ENV).ok().filter(|s| !s.trim().is_empty())) +} + +/// Bind the iroh endpoint with our ALPN. +/// +/// 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 { + let mut builder = Endpoint::builder(presets::N0).alpns(vec![ALPN.to_vec()]); + + 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") +} diff --git a/src/common/mod.rs b/src/common/mod.rs index af1fd15..af5a575 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -2,6 +2,7 @@ pub mod alpn; pub mod bandwidth; pub mod config; pub mod deps; +pub mod endpoint; pub mod display; pub mod output; pub mod process; diff --git a/src/host/mod.rs b/src/host/mod.rs index 44f2eb4..0a26135 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -7,7 +7,7 @@ mod wayland; mod x11; use anyhow::{Result, bail}; -use iroh::endpoint::{Connection, presets}; +use iroh::endpoint::Connection; use iroh::{Endpoint, EndpointAddr}; use iroh_tickets::endpoint::EndpointTicket; use std::collections::HashMap; @@ -17,7 +17,7 @@ use tokio_util::sync::CancellationToken; use crate::cli::HostOpts; use crate::common::{ - alpn::ALPN, bandwidth, config, config::BandwidthStatus, deps, display::DisplayServer, output, + bandwidth, config, config::BandwidthStatus, deps, display::DisplayServer, endpoint, output, signal, tunnel, }; @@ -74,10 +74,7 @@ pub async fn run(opts: HostOpts) -> Result<()> { let cancel = signal::install_ctrl_c(); - let endpoint = Endpoint::builder(presets::N0) - .alpns(vec![ALPN.to_vec()]) - .bind() - .await?; + let endpoint = endpoint::bind(opts.relay.as_deref()).await?; // Relay-only ticket: wait for the home relay to connect, then keep only // the endpoint id + relay URL and drop the direct IP candidates. The relay diff --git a/src/host/quality.rs b/src/host/quality.rs index d85bf3c..294c348 100644 --- a/src/host/quality.rs +++ b/src/host/quality.rs @@ -189,6 +189,7 @@ mod tests { no_hwencode: false, max_viewers, interactive: false, + relay: None, } } diff --git a/src/viewer/mod.rs b/src/viewer/mod.rs index 2bc211d..f6fe7d8 100644 --- a/src/viewer/mod.rs +++ b/src/viewer/mod.rs @@ -1,12 +1,10 @@ use anyhow::{Context, Result, bail}; -use iroh::Endpoint; -use iroh::endpoint::presets; use iroh_tickets::endpoint::EndpointTicket; use std::time::Duration; use tokio::net::TcpListener; use crate::cli::ViewerOpts; -use crate::common::{alpn::ALPN, output, signal}; +use crate::common::{alpn::ALPN, endpoint, output, signal}; /// Cap on the initial QUIC connect. `endpoint.connect()` has no built-in /// deadline, so an offline host / stale code / unreachable relay otherwise @@ -17,10 +15,7 @@ const CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> { let cancel = signal::install_ctrl_c(); - let endpoint = Endpoint::builder(presets::N0) - .alpns(vec![ALPN.to_vec()]) - .bind() - .await?; + let endpoint = endpoint::bind(opts.relay.as_deref()).await?; let addr = ticket.endpoint_addr().clone(); tracing::info!(remote = %addr.id, "connecting to host");