189 lines
6.9 KiB
Rust
189 lines
6.9 KiB
Rust
|
|
//! iroh endpoint, blob provider and downloader.
|
||
|
|
//!
|
||
|
|
//! This module is also the seam where transport-level traffic shaping
|
||
|
|
//! lives: the rate limiters (milestone 4) and, later, scavenger
|
||
|
|
//! congestion control would slot in here without touching the daemon
|
||
|
|
//! logic above.
|
||
|
|
|
||
|
|
use std::path::Path;
|
||
|
|
|
||
|
|
use anyhow::{Context, Result};
|
||
|
|
use iroh::protocol::Router;
|
||
|
|
use iroh::{Endpoint, NodeAddr, RelayMode, SecretKey};
|
||
|
|
use iroh_blobs::downloader::{DownloadRequest, Downloader};
|
||
|
|
use iroh_blobs::net_protocol::Blobs;
|
||
|
|
use iroh_blobs::ticket::BlobTicket;
|
||
|
|
use iroh_blobs::util::local_pool::LocalPoolHandle;
|
||
|
|
use iroh_blobs::{BlobFormat, Hash, HashAndFormat};
|
||
|
|
use tokio::sync::broadcast;
|
||
|
|
use tracing::{debug, info, warn};
|
||
|
|
use varde_proto::Event;
|
||
|
|
|
||
|
|
use crate::config::Config;
|
||
|
|
use crate::store::BlobStore;
|
||
|
|
|
||
|
|
/// The network side of the daemon: one iroh endpoint serving the blob
|
||
|
|
/// store, plus a downloader for fetching pinned content from providers.
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub struct Transfer {
|
||
|
|
endpoint: Endpoint,
|
||
|
|
router: Router,
|
||
|
|
downloader: Downloader,
|
||
|
|
events: broadcast::Sender<Event>,
|
||
|
|
}
|
||
|
|
|
||
|
|
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,
|
||
|
|
pool: LocalPoolHandle,
|
||
|
|
events: broadcast::Sender<Event>,
|
||
|
|
) -> Result<Transfer> {
|
||
|
|
let secret = load_or_create_secret(&config.store_dir.join("secret.key"))?;
|
||
|
|
let mut builder = Endpoint::builder().secret_key(secret);
|
||
|
|
if !config.wan_upload {
|
||
|
|
builder = builder.relay_mode(RelayMode::Disabled);
|
||
|
|
}
|
||
|
|
let endpoint = builder.bind().await.context("binding iroh endpoint")?;
|
||
|
|
info!(node_id = %endpoint.node_id(), "endpoint up");
|
||
|
|
|
||
|
|
let blobs = Blobs::builder(store.inner().clone())
|
||
|
|
.local_pool(pool)
|
||
|
|
.build(&endpoint);
|
||
|
|
let downloader = blobs.downloader().clone();
|
||
|
|
let router = Router::builder(endpoint.clone())
|
||
|
|
.accept(iroh_blobs::ALPN, blobs)
|
||
|
|
.spawn();
|
||
|
|
|
||
|
|
Ok(Transfer {
|
||
|
|
endpoint,
|
||
|
|
router,
|
||
|
|
downloader,
|
||
|
|
events,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Our stable node id.
|
||
|
|
pub fn node_id(&self) -> String {
|
||
|
|
self.endpoint.node_id().to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Produce a ticket for out-of-band sharing of `content`.
|
||
|
|
pub async fn export_ticket(&self, content: HashAndFormat) -> Result<String> {
|
||
|
|
let addr = self
|
||
|
|
.endpoint
|
||
|
|
.node_addr()
|
||
|
|
.await
|
||
|
|
.context("waiting for endpoint address")?;
|
||
|
|
let ticket = BlobTicket::new(addr, content.hash, content.format)?;
|
||
|
|
Ok(ticket.to_string())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Queue a background fetch of `content` from `providers`. Emits
|
||
|
|
/// [`Event::PinComplete`] when the content is fully verified locally.
|
||
|
|
pub fn spawn_fetch(&self, content: HashAndFormat, providers: Vec<NodeAddr>) {
|
||
|
|
let downloader = self.downloader.clone();
|
||
|
|
let events = self.events.clone();
|
||
|
|
let endpoint = self.endpoint.clone();
|
||
|
|
tokio::spawn(async move {
|
||
|
|
// The downloader dials by NodeId; the endpoint must be taught
|
||
|
|
// the provider's direct addresses first or the dial fails
|
||
|
|
// with "no addressing information".
|
||
|
|
for provider in &providers {
|
||
|
|
debug!(node = %provider.node_id, addrs = ?provider.direct_addresses, "provider");
|
||
|
|
if let Err(e) = endpoint.add_node_addr(provider.clone()) {
|
||
|
|
warn!(node = %provider.node_id, error = %e, "adding provider address");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let request = DownloadRequest::new(content, providers);
|
||
|
|
let handle = downloader.queue(request).await;
|
||
|
|
match handle.await {
|
||
|
|
Ok(_stats) => {
|
||
|
|
info!(hash = %content.hash.to_hex(), "fetch complete");
|
||
|
|
let _ = events.send(Event::PinComplete {
|
||
|
|
hash: content.hash.to_hex().to_string(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
Err(e) => {
|
||
|
|
warn!(hash = %content.hash.to_hex(), error = %e, "fetch failed");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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).
|
||
|
|
pub fn parse_ticket(ticket: &str) -> Result<(Hash, BlobFormat, NodeAddr), String> {
|
||
|
|
let ticket: BlobTicket = ticket.parse().map_err(|e| format!("invalid ticket: {e}"))?;
|
||
|
|
Ok((ticket.hash(), ticket.format(), ticket.node_addr().clone()))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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 => {
|
||
|
|
let secret = SecretKey::generate(rand::rngs::OsRng);
|
||
|
|
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");
|
||
|
|
}
|
||
|
|
}
|