Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ rust-version = "1.91"

[dependencies]
anyhow = "1.0.95"
data-encoding = "2.9.0"
Comment thread
b5 marked this conversation as resolved.
derive_more = { version = "2.0.1", features = ["display", "from"] }
ed25519-dalek = { version = "=3.0.0-pre.7" }
irpc = { version = "0.15", default-features = false, features = ["derive", "stream", "spans"] }
Expand Down
63 changes: 63 additions & 0 deletions examples/relays.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! This example shows common methods for configuring custom relays provided by iroh services.
//! All of these leverage the [`iroh_services::preset`] builder to configure the endpoint,
//! which itself builds an [`iroh::presets::Preset`] to pass to an [`iroh::Endpoint`] on
//! construction.
//!
//! To run these, set IROH_SERVICES_API_SECRET in your environment, using an API secret
//! from your iroh services project, and then run with `cargo run --example relays`.
//!
//! Your app will use only one of these methods, depending on your use case. To test this
//! example with custom relay URLS, you will need to comment out the secret key preset
//! example and use the `relays` method instead, pasting in your own relay URLs.
use iroh::{Endpoint, SecretKey};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// minimal preset that works with a free iroh services project. This will
// give a bandwidth bump when using the public relays, and surface your
// relay traffic on your project:
let _preset = iroh_services::preset().api_secret_from_env()?.build()?;

// pro & enterprise projects have access to custom relays, which are set
// with the `relays` method on the builder.
// You'll need to replace these strings with the relay urls for your project.
let _preset = iroh_services::preset()
Copy link
Copy Markdown
Contributor

@okdistribute okdistribute May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this example actually runnable? It seems like all of the other examples in this repository can be run by setting the API SECRET in env without modifying the code.

I'd prefer to keep it that way, and instead put this information in the documentation (https://docs.iroh.computer) and link to it from the README of this repository as well as the relays page on iroh services.

Copy link
Copy Markdown
Member Author

@b5 b5 May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep it's runnable! Or at least, it is on my dev branch & should be generally. I added a note about setting the IROH_SERVICES_API_SECRET in the example description in c124d31

.relays([
"https://use1-1.relay.username.project.iroh.link",
"https://usw1-1.relay.username.project.iroh.link",
"https://euc1-1.relay.username.project.iroh.link",
])?
.api_secret_from_env()?
.build()?;

// if you are using a secret key from disk, or generally want control over
// the ID your endpoint uses, the secret key must be given to the preset
// before passing it to the endpoint. The access token that the preset creates
// to access relays is scoped only to the given key.
//
// If no key is provided, a random one is generated and passed to the
// endpoint.
let secret_key = SecretKey::generate();
let preset = iroh_services::preset()
.secret_key(secret_key)
.api_secret_from_env()?
.build()?;

// once a preset is built, we'll pass it to the endpoint for binding.
// we clone the preset so we can reuse to get a client builder below
let endpoint = Endpoint::bind(preset.clone()).await?;

// wait for the endpoint to be online, to prove we have an authorized
// connection to a relay
endpoint.online().await;

// a client is not required to use, but the preset has a convenience method
// for creating a client builder that uses the same access token as the
// endpoint, so you don't need to pass the secret key separately:
let client = preset.client_builder(&endpoint).build().await?;

// we can also ping the service just to confirm everything is working
client.ping().await?;

Ok(())
}
3 changes: 3 additions & 0 deletions src/api_secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use iroh::{EndpointAddr, EndpointId, SecretKey, TransportAddr};
use iroh_tickets::{ParseError, Ticket};
use serde::{Deserialize, Serialize};

/// The environment variable name this crate checks in builders for an API secret.
pub const API_SECRET_ENV_VAR_NAME: &str = "IROH_SERVICES_API_SECRET";

/// The secret material used to connect your services.iroh.computer project. The
/// value of these should be treated like any other API key: guard them carefully.
#[derive(Debug, Clone)]
Expand Down
3 changes: 3 additions & 0 deletions src/caps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use n0_future::time::Duration;
use rcan::{Capability, Expires, Rcan};
use serde::{Deserialize, Serialize};

/// How long tokens are valid for by default
pub(crate) const DEFAULT_CAP_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 1 month
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use Duration::from_days?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no such method I'm afraid, but I can bump up to Duration::from_hours


macro_rules! cap_enum(
($enum:item) => {
#[derive(
Expand Down
7 changes: 2 additions & 5 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use tracing::{debug, trace, warn};
use uuid::Uuid;

use crate::{
api_secret::ApiSecret,
caps::Caps,
api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
caps::{Caps, DEFAULT_CAP_EXPIRY},
net_diagnostics::{DiagnosticsReport, checks::run_diagnostics},
protocol::{
ALPN, Auth, IrohServicesClient, NameEndpoint, Ping, Pong, PutMetrics,
Expand Down Expand Up @@ -69,9 +69,6 @@ pub struct ClientBuilder {
registry: Registry,
}

const DEFAULT_CAP_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 1 month
pub const API_SECRET_ENV_VAR_NAME: &str = "IROH_SERVICES_API_SECRET";

impl ClientBuilder {
pub fn new(endpoint: &Endpoint) -> Self {
let mut registry = Registry::default();
Expand Down
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod openssh;
pub mod api_secret;
pub mod caps;
pub mod net_diagnostics;
pub mod preset;
pub mod protocol;

mod built_info {
Expand All @@ -59,8 +60,9 @@ pub use client_host::{CLIENT_HOST_ALPN, ClientHost, ClientHostClient};
pub use iroh_metrics::Registry;

pub use self::{
api_secret::ApiSecret,
client::{API_SECRET_ENV_VAR_NAME, Client, ClientBuilder},
api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
client::{Client, ClientBuilder},
net_diagnostics::{DiagnosticsReport, checks::run_diagnostics},
preset::{IrohServicesPreset, PresetBuilder, preset},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we have the bad precedent, but having a pub mod and pub use is really confusing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this fully addresses it, but I've made preset private, and just exported the main types. Is that better?

protocol::ALPN,
};
187 changes: 187 additions & 0 deletions src/preset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//! An [`iroh::endpoint`] preset tailored for use with iroh-services.
//!
//! [`IrohServicesPreset`] starts from the n0 stock preset (production crypto
//! provider + n0 DNS-based address lookup) and overlays the bits that
//! iroh-services callers usually want to configure together: the relay map
//! the endpoint should use, an optional explicit [`SecretKey`], and an
//! optional [`ApiSecret`] that downstream code can retrieve to wire up a
//! [`crate::Client`].
//!
//! # Example
//! ```no_run
//! use iroh::Endpoint;
//!
//! async fn run() -> anyhow::Result<()> {
//! let preset = iroh_services::preset()
//! .relays(["https://us-east1.project_username.iroh.link"])?
//! .api_secret_from_env()?
//! .build()?;
//! let endpoint = Endpoint::builder(preset).bind().await?;
//! Ok(())
//! }
//! ```
use std::{str::FromStr, time::Duration};

use anyhow::{Context, Result, anyhow};
use iroh::{Endpoint, RelayMap, RelayMode, RelayUrl, SecretKey, endpoint::presets::Preset};

use crate::{
ClientBuilder,
api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
caps::{Cap, Caps, DEFAULT_CAP_EXPIRY},
};

/// An iroh endpoint preset configured for iroh-services. Build one with
/// [`preset`] or [`IrohServicesPreset::builder`], then pass it to
/// [`iroh::Endpoint::builder`].
#[derive(Debug, Clone)]
pub struct IrohServicesPreset {
secret_key: SecretKey,
relays: RelayMap,
// not used by the preset, only for creating a client builder
api_secret: ApiSecret,
}

impl IrohServicesPreset {
/// Start a new builder seeded with iroh-services defaults. Equivalent to
/// the free-standing [`preset`] function.
pub fn builder() -> PresetBuilder {
preset()
}

/// Returns the [`ApiSecret`] stashed on this preset, if one was set.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stashed is very odd wording

/// Useful for handing the same secret to a [`crate::Client`] without
/// plumbing it through twice.
pub fn api_secret(&self) -> &ApiSecret {
&self.api_secret
}

/// Returns a [`ClientBuilder`] pre-configured with this preset's API secret.
pub fn client_builder(&self, endpoint: &Endpoint) -> ClientBuilder {
ClientBuilder::new(endpoint)
.api_secret(self.api_secret.clone())
.unwrap()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why unwrap?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you dig into the builder, the api_secret method on the builder has been factored to the point that it can no longer fail. I'll add a comment as such at a bare minimum, but can also either make this bubble up a Result, or we can break the API one last time, your call

}
}

impl Preset for IrohServicesPreset {
fn apply(self, builder: iroh::endpoint::Builder) -> iroh::endpoint::Builder {
// Inherit n0 defaults (crypto provider + DNS address lookup), then
// overlay our relay map and (optionally) an explicit secret key
let mut builder = iroh::endpoint::presets::N0.apply(builder);
builder = builder.relay_mode(RelayMode::Custom(self.relays));
builder = builder.secret_key(self.secret_key);
builder
}
}

/// Fluent builder for [`IrohServicesPreset`]. Construct one through
/// [`preset`] or [`IrohServicesPreset::builder`].
#[derive(Debug, Clone)]
pub struct PresetBuilder {
cap_expiry: Duration,
secret_key: Option<SecretKey>,
relays: RelayMap,
api_secret: Option<ApiSecret>,
}

/// Start a new [`IrohServicesPreset`] builder seeded with iroh-services
/// defaults: the n0 production relay map and no explicit secret key (the
/// endpoint will generate one at bind time).
pub fn preset() -> PresetBuilder {
PresetBuilder {
cap_expiry: DEFAULT_CAP_EXPIRY,
secret_key: None,
relays: iroh::endpoint::default_relay_mode().relay_map(),
api_secret: None,
}
}

impl PresetBuilder {
/// Set the endpoint's long-lived [`SecretKey`]. If left unset the
/// endpoint will generate a fresh random key at bind time.
pub fn secret_key(mut self, secret_key: SecretKey) -> Self {
self.secret_key = Some(secret_key);
self
}

pub fn relays<I, S>(mut self, relays: I) -> Result<Self>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing docs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added an example to help make the generic argument more understandable

where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let parsed = relays
.into_iter()
.map(|s| {
let s = s.as_ref();
s.parse::<RelayUrl>()
.with_context(|| format!("invalid relay url {s:?}"))
})
.collect::<anyhow::Result<Vec<_>>>()?;

self.relays = RelayMap::from_iter(parsed);
Ok(self)
}

/// Pick relays via a [`RelayMode`] (e.g. `RelayMode::Staging` or a
/// pre-built `RelayMode::Custom(RelayMap)`).
pub fn relay_mode(mut self, mode: RelayMode) -> Self {
self.relays = mode.relay_map();
self
}

/// Pass in a [`RelayMap`] directly, bypassing URL parsing.
pub fn relay_map(mut self, map: RelayMap) -> Self {
self.relays = map;
self
}

/// Check IROH_SERVICES_API_SECRET environment variable for a valid API secret
pub fn api_secret_from_env(self) -> Result<Self> {
let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
Ok(self.api_secret(ticket))
}

/// set client API secret from an encoded string
pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
Ok(self.api_secret(key))
}

/// Stash an [`ApiSecret`] on the preset so callers can retrieve it later
/// via [`IrohServicesPreset::api_secret`] when constructing a client.
pub fn api_secret(mut self, api_secret: ApiSecret) -> Self {
self.api_secret = Some(api_secret);
self
}

/// Finalize the configuration into an [`IrohServicesPreset`].
pub fn build(self) -> Result<IrohServicesPreset> {
let secret_key = self.secret_key.unwrap_or_else(SecretKey::generate);

let Some(api_secret) = self.api_secret else {
return Err(anyhow!(
"api secret is required to use iroh_services relay preset"
));
};

// build our token to interact with relays. This is only scoped to relay use.
let rcan = crate::caps::create_api_token_from_secret_key(
api_secret.secret.clone(),
secret_key.public(),
self.cap_expiry,
Caps::new([Cap::Relay(crate::caps::RelayCap::Use)]),
)?;

let mut token = data_encoding::BASE32_NOPAD.encode(&rcan.encode());
token.make_ascii_lowercase();

let relays = self.relays.with_auth_token(token);

Ok(IrohServicesPreset {
secret_key,
relays,
api_secret,
})
}
}
Loading