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,
|
pub port: u16,
|
||||||
|
|
||||||
// ── global ────────────────────────────────────────────────────────
|
// ── 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)
|
/// Launch the graphical front-end (a window with Host/View controls)
|
||||||
/// instead of the terminal menu. Requires a build with `--features gui`.
|
/// instead of the terminal menu. Requires a build with `--features gui`.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -140,12 +147,16 @@ pub struct HostOpts {
|
|||||||
pub no_hwencode: bool,
|
pub no_hwencode: bool,
|
||||||
pub max_viewers: Option<u32>,
|
pub max_viewers: Option<u32>,
|
||||||
pub interactive: bool,
|
pub interactive: bool,
|
||||||
|
/// Relay override (resolved from `--relay` / `PIXELPASS_RELAY`); None = defaults.
|
||||||
|
pub relay: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ViewerOpts {
|
pub struct ViewerOpts {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub interactive: bool,
|
pub interactive: bool,
|
||||||
|
/// Relay override (resolved from `--relay` / `PIXELPASS_RELAY`); None = defaults.
|
||||||
|
pub relay: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
@@ -163,10 +174,15 @@ impl Cli {
|
|||||||
no_hwencode: self.no_hwencode,
|
no_hwencode: self.no_hwencode,
|
||||||
max_viewers: self.max_viewers,
|
max_viewers: self.max_viewers,
|
||||||
interactive,
|
interactive,
|
||||||
|
relay: crate::common::endpoint::relay_override(self.relay.as_deref()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_viewer_opts(self, interactive: bool) -> ViewerOpts {
|
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 bandwidth;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod deps;
|
pub mod deps;
|
||||||
|
pub mod endpoint;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
|
|||||||
+3
-6
@@ -7,7 +7,7 @@ mod wayland;
|
|||||||
mod x11;
|
mod x11;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use iroh::endpoint::{Connection, presets};
|
use iroh::endpoint::Connection;
|
||||||
use iroh::{Endpoint, EndpointAddr};
|
use iroh::{Endpoint, EndpointAddr};
|
||||||
use iroh_tickets::endpoint::EndpointTicket;
|
use iroh_tickets::endpoint::EndpointTicket;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -17,7 +17,7 @@ use tokio_util::sync::CancellationToken;
|
|||||||
|
|
||||||
use crate::cli::HostOpts;
|
use crate::cli::HostOpts;
|
||||||
use crate::common::{
|
use crate::common::{
|
||||||
alpn::ALPN, bandwidth, config, config::BandwidthStatus, deps, display::DisplayServer, output,
|
bandwidth, config, config::BandwidthStatus, deps, display::DisplayServer, endpoint, output,
|
||||||
signal, tunnel,
|
signal, tunnel,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,10 +74,7 @@ pub async fn run(opts: HostOpts) -> Result<()> {
|
|||||||
|
|
||||||
let cancel = signal::install_ctrl_c();
|
let cancel = signal::install_ctrl_c();
|
||||||
|
|
||||||
let endpoint = Endpoint::builder(presets::N0)
|
let endpoint = endpoint::bind(opts.relay.as_deref()).await?;
|
||||||
.alpns(vec![ALPN.to_vec()])
|
|
||||||
.bind()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Relay-only ticket: wait for the home relay to connect, then keep only
|
// 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
|
// the endpoint id + relay URL and drop the direct IP candidates. The relay
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ mod tests {
|
|||||||
no_hwencode: false,
|
no_hwencode: false,
|
||||||
max_viewers,
|
max_viewers,
|
||||||
interactive: false,
|
interactive: false,
|
||||||
|
relay: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-7
@@ -1,12 +1,10 @@
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use iroh::Endpoint;
|
|
||||||
use iroh::endpoint::presets;
|
|
||||||
use iroh_tickets::endpoint::EndpointTicket;
|
use iroh_tickets::endpoint::EndpointTicket;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::cli::ViewerOpts;
|
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
|
/// Cap on the initial QUIC connect. `endpoint.connect()` has no built-in
|
||||||
/// deadline, so an offline host / stale code / unreachable relay otherwise
|
/// 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<()> {
|
pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
|
||||||
let cancel = signal::install_ctrl_c();
|
let cancel = signal::install_ctrl_c();
|
||||||
|
|
||||||
let endpoint = Endpoint::builder(presets::N0)
|
let endpoint = endpoint::bind(opts.relay.as_deref()).await?;
|
||||||
.alpns(vec![ALPN.to_vec()])
|
|
||||||
.bind()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let addr = ticket.endpoint_addr().clone();
|
let addr = ticket.endpoint_addr().clone();
|
||||||
tracing::info!(remote = %addr.id, "connecting to host");
|
tracing::info!(remote = %addr.id, "connecting to host");
|
||||||
|
|||||||
Reference in New Issue
Block a user