This repository has been archived on 2026-08-30. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
varde/varde-daemon/src/transfer.rs
T

306 lines
12 KiB
Rust
Raw Normal View History

//! iroh endpoint, blob provider and downloader.
//!
2026-08-16 14:03:15 +02:00
//! This module is the transport seam: trust gating and rate limiting
//! live in [`crate::shaped`]'s provider event handler wired up here, and
//! a scavenger congestion controller would later slot in at the same
//! place without touching the daemon logic above.
use std::path::Path;
use std::sync::Arc;
use anyhow::{Context, Result};
2026-08-16 14:03:15 +02:00
use iroh::address_lookup::memory::MemoryLookup;
use iroh::protocol::Router;
use iroh::{Endpoint, EndpointAddr, EndpointId, RelayMode, SecretKey};
use iroh_blobs::api::downloader::{DownloadOptions, Downloader, Shuffled, SplitStrategy};
use iroh_blobs::{BlobFormat, BlobsProtocol, Hash, HashAndFormat};
use iroh_mdns_address_lookup::{DiscoveryEvent, MdnsAddressLookup};
use n0_future::StreamExt;
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, info, warn};
use varde_proto::{Direction, Event};
use crate::config::Config;
use crate::meta::Meta;
use crate::metered::MeteredState;
2026-08-16 14:03:15 +02:00
use crate::shaped::{Gate, ServeFilter, TokenBucket};
use crate::store::BlobStore;
/// The network side of the daemon: one iroh endpoint serving the blob
/// store (trust-gated, rate-limited), plus a downloader for fetching
/// pinned content from providers.
#[derive(Debug)]
pub struct Transfer {
endpoint: Endpoint,
router: Router,
downloader: Downloader,
2026-08-16 14:03:15 +02:00
/// Address book for provider addresses learned out-of-band (ticket
/// imports): the downloader dials by endpoint id and resolves
/// addresses through the endpoint's address lookup services.
known_addrs: MemoryLookup,
events: broadcast::Sender<Event>,
metered: MeteredState,
2026-08-16 14:03:15 +02:00
discovery_rx: std::sync::Mutex<Option<mpsc::Receiver<EndpointId>>>,
}
impl Transfer {
/// Bind the endpoint and start serving the store.
///
/// With `wan_upload` off (the default) the relay is disabled: the
/// endpoint is only reachable via direct (LAN/localhost) paths and
/// never uploads through third-party infrastructure.
pub async fn start(
config: &Config,
store: &BlobStore,
meta: Arc<Meta>,
open_filter: ServeFilter,
events: broadcast::Sender<Event>,
metered: MeteredState,
) -> Result<Transfer> {
let secret = load_or_create_secret(&config.store_dir.join("secret.key"))?;
2026-08-16 14:03:15 +02:00
// With wan_upload the n0 defaults apply (their relays and DNS
// lookup, matching the pre-1.0 default relay mode); otherwise the
// endpoint gets no external services at all.
let builder = if config.wan_upload {
Endpoint::builder(iroh::endpoint::presets::N0)
} else {
Endpoint::builder(iroh::endpoint::presets::Minimal).relay_mode(RelayMode::Disabled)
};
let endpoint = builder
.secret_key(secret.clone())
.bind()
.await
.context("binding iroh endpoint")?;
info!(endpoint_id = %endpoint.id(), discovery = config.discovery, "endpoint up");
let known_addrs = MemoryLookup::with_provenance("varde-ticket");
endpoint
.address_lookup()
.context("endpoint address lookup services")?
.add(known_addrs.clone());
// LAN discovery: advertise under iroh's mDNS-style local
// discovery service (identifiable `_iroh` mdns records, not
2026-08-16 14:03:15 +02:00
// anonymous noise). Discovered endpoint ids stream to the daemon,
// which decides what to do based on trust.
let (discovery_tx, discovery_rx) = mpsc::channel(64);
if config.discovery {
2026-08-16 14:03:15 +02:00
let mdns = MdnsAddressLookup::builder()
.build(endpoint.id())
.context("starting mdns local discovery")?;
let mut sightings = mdns.subscribe().await;
tokio::spawn(async move {
while let Some(event) = sightings.next().await {
let DiscoveryEvent::Discovered { endpoint_info, .. } = event else {
continue;
};
if discovery_tx.send(endpoint_info.endpoint_id).await.is_err() {
break;
}
2026-08-16 14:03:15 +02:00
}
});
endpoint
.address_lookup()
.context("endpoint address lookup services")?
.add(mdns);
}
// Best-effort low-priority marking of our UDP traffic.
2026-08-16 14:03:15 +02:00
crate::dscp::mark_endpoint_sockets(&endpoint.bound_sockets());
2026-08-16 14:03:15 +02:00
let provider_events = crate::shaped::provider_events(Gate {
meta,
2026-08-16 14:03:15 +02:00
open_filter,
upload: TokenBucket::new(config.max_upload_bytes_per_sec),
serving_enabled: config.max_upload_bytes_per_sec > 0,
metered: metered.clone(),
2026-08-16 14:03:15 +02:00
events: events.clone(),
});
let provider = BlobsProtocol::new(store.api(), Some(provider_events));
2026-08-16 14:03:15 +02:00
let downloader = store.api().downloader(&endpoint);
let router = Router::builder(endpoint.clone())
.accept(iroh_blobs::ALPN, provider)
.spawn();
Ok(Transfer {
endpoint,
router,
downloader,
2026-08-16 14:03:15 +02:00
known_addrs,
events,
metered,
discovery_rx: std::sync::Mutex::new(Some(discovery_rx)),
})
}
2026-08-16 14:03:15 +02:00
/// Our stable endpoint id.
pub fn node_id(&self) -> String {
2026-08-16 14:03:15 +02:00
self.endpoint.id().to_string()
}
/// The endpoint's identity key (also signs our announcements).
pub fn secret_key(&self) -> SecretKey {
self.endpoint.secret_key().clone()
}
2026-08-16 14:03:15 +02:00
/// Take the stream of locally discovered endpoint ids. Yields each
/// sighting (mdns re-announces periodically); the daemon dedupes.
/// Callable once; returns None afterwards or with discovery off.
2026-08-16 14:03:15 +02:00
pub fn take_discovery(&self) -> Option<mpsc::Receiver<EndpointId>> {
self.discovery_rx.lock().expect("discovery lock").take()
}
/// Produce a ticket for out-of-band sharing of `content`.
pub async fn export_ticket(&self, content: HashAndFormat) -> Result<String> {
2026-08-16 14:03:15 +02:00
let addr = self.endpoint.addr();
anyhow::ensure!(
!addr.addrs.is_empty(),
"endpoint has no dialable addresses yet"
);
let ticket = iroh_blobs::ticket::BlobTicket::new(addr, content.hash, content.format);
Ok(ticket.to_string())
}
/// Queue a background fetch of `content` from `providers`. Emits
/// progress events and [`Event::PinComplete`] when fully verified.
2026-08-16 14:03:15 +02:00
pub fn spawn_fetch(&self, content: HashAndFormat, providers: Vec<EndpointAddr>) {
if self.metered.is_metered() {
// The pin stays recorded; auto-sync retries once unmetered.
info!(hash = %content.hash.to_hex(), "metered connection: fetch deferred");
return;
}
2026-08-16 14:03:15 +02:00
// The downloader dials by EndpointId; teach the endpoint any
// direct addresses we know (ticket-embedded ones). Id-only
// provider entries resolve via discovery instead.
let mut ids = Vec::with_capacity(providers.len());
for provider in providers {
ids.push(provider.id);
if !provider.addrs.is_empty() {
self.known_addrs.add_endpoint_info(provider);
}
}
let downloader = self.downloader.clone();
let events = self.events.clone();
tokio::spawn(async move {
2026-08-16 14:03:15 +02:00
let progress = downloader.download_with_opts(DownloadOptions::new(
content,
Shuffled::new(ids),
SplitStrategy::None,
));
let mut stream = match progress.stream().await {
Ok(stream) => stream,
Err(e) => {
warn!(hash = %content.hash.to_hex(), error = %e, "fetch failed to start");
return;
}
};
use iroh_blobs::api::downloader::DownloadProgressItem;
let mut failed = false;
while let Some(item) = stream.next().await {
match item {
DownloadProgressItem::Progress(bytes_done) => {
let _ = events.send(Event::TransferProgress {
hash: content.hash.to_hex().to_string(),
direction: Direction::Download,
bytes_done,
bytes_total: None,
});
}
2026-08-16 14:03:15 +02:00
DownloadProgressItem::ProviderFailed { id, .. } => {
debug!(hash = %content.hash.to_hex(), provider = %id, "provider failed");
}
DownloadProgressItem::Error(e) => {
warn!(hash = %content.hash.to_hex(), error = %e, "fetch failed");
failed = true;
}
DownloadProgressItem::DownloadError => {
warn!(hash = %content.hash.to_hex(), "fetch failed");
failed = true;
}
_ => {}
}
}
2026-08-16 14:03:15 +02:00
if !failed {
info!(hash = %content.hash.to_hex(), "fetch complete");
let _ = events.send(Event::PinComplete {
hash: content.hash.to_hex().to_string(),
});
}
});
}
/// Gracefully close the endpoint.
pub async fn shutdown(&self) {
if let Err(e) = self.router.shutdown().await {
warn!(error = %e, "router shutdown");
}
self.endpoint.close().await;
}
}
/// Parse a ticket string into (root, format, provider address).
2026-08-16 14:03:15 +02:00
pub fn parse_ticket(ticket: &str) -> Result<(Hash, BlobFormat, EndpointAddr), String> {
let ticket: iroh_blobs::ticket::BlobTicket =
ticket.parse().map_err(|e| format!("invalid ticket: {e}"))?;
2026-08-16 14:03:15 +02:00
let (addr, hash, format) = ticket.into_parts();
Ok((hash, format, addr))
}
/// Load the endpoint secret key from `path`, creating it (mode 0600) on
/// first start.
fn load_or_create_secret(path: &Path) -> Result<SecretKey> {
match std::fs::read_to_string(path) {
Ok(hex) => parse_secret(hex.trim())
.with_context(|| format!("parsing secret key {}", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
2026-08-16 14:03:15 +02:00
let secret = SecretKey::generate();
let hex = hex_encode(&secret.to_bytes());
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
.with_context(|| format!("creating secret key {}", path.display()))?;
file.write_all(hex.as_bytes())?;
Ok(secret)
}
Err(e) => Err(e).with_context(|| format!("reading secret key {}", path.display())),
}
}
fn parse_secret(hex: &str) -> Result<SecretKey> {
anyhow::ensure!(hex.len() == 64, "expected 64 hex chars, got {}", hex.len());
let mut bytes = [0u8; 32];
for (i, byte) in bytes.iter_mut().enumerate() {
*byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16)
.map_err(|e| anyhow::anyhow!("bad hex at {}: {e}", 2 * i))?;
}
Ok(SecretKey::from_bytes(&bytes))
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_key_round_trips_through_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret.key");
let first = load_or_create_secret(&path).unwrap();
let second = load_or_create_secret(&path).unwrap();
assert_eq!(first.to_bytes(), second.to_bytes());
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600, "secret key must be 0600");
}
}