feat(relay): add --relay / PIXELPASS_RELAY override
Both host and viewer hardcoded presets::N0, pinning every session to the
bundled relays (which on iroh rc.0 are the canary-grade defaults). Add a
shared common::endpoint::bind() that keeps N0's DNS discovery + crypto but
swaps in a RelayMode::Custom single-relay map when --relay (or the
PIXELPASS_RELAY env var, so GUI children inherit it) is set.
Lets users point at a self-hosted relay or staging today; the production
relays (*.relay.iroh.network) speak a newer protocol that rc.0 rejects
("invalid iroh-relay version header"), so they only become usable — and
the default — after an iroh GA bump. Verified: override connects cleanly
through staging; bad URLs are rejected before any network work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+17
-1
@@ -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<String>,
|
||||
|
||||
/// 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<u32>,
|
||||
pub interactive: bool,
|
||||
/// Relay override (resolved from `--relay` / `PIXELPASS_RELAY`); None = defaults.
|
||||
pub relay: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ViewerOpts {
|
||||
pub port: u16,
|
||||
pub interactive: bool,
|
||||
/// Relay override (resolved from `--relay` / `PIXELPASS_RELAY`); None = defaults.
|
||||
pub relay: Option<String>,
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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<Endpoint> {
|
||||
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")
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+3
-6
@@ -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
|
||||
|
||||
@@ -189,6 +189,7 @@ mod tests {
|
||||
no_hwencode: false,
|
||||
max_viewers,
|
||||
interactive: false,
|
||||
relay: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-7
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user