decdn_cli/commands/
probe.rs1use decdn_common::cli;
5
6use decdn_client::probe::probe_once;
7
8fn parse_hash(s: &str) -> anyhow::Result<[u8; 32]> {
11 let hex = s.strip_prefix("0x").unwrap_or(s);
12 let h = blake3::Hash::from_hex(hex).map_err(|e| {
13 anyhow::anyhow!("invalid --hash {s:?}: expected 64 hex chars (BLAKE3 digest): {e}")
14 })?;
15 Ok(*h.as_bytes())
16}
17
18pub async fn probe(
25 args: &cli::ProbeArgs,
26 config_path: Option<&std::path::Path>,
27) -> anyhow::Result<()> {
28 use std::str::FromStr;
29 use std::time::{Duration, SystemTime, UNIX_EPOCH};
30
31 use iroh::{EndpointAddr, PublicKey};
32
33 use decdn_client::endpoint as client_endpoint;
34
35 let node_id = PublicKey::from_str(&args.node_id)
36 .map_err(|e| anyhow::anyhow!("invalid --node-id {:?}: {e}", args.node_id))?;
37 let hash = parse_hash(&args.hash)?;
38
39 let relays = client_endpoint::resolve_relays(args.relay_url.as_deref(), config_path)?;
44 let discovery = client_endpoint::client_discovery(config_path)?;
45
46 let endpoint = client_endpoint::client_endpoint(&relays, &discovery).await?;
51
52 let mut target = EndpointAddr::new(node_id);
53 if let Some(addr) = args.addr {
54 target = target.with_ip_addr(addr);
55 }
56 if let Some(url) = relays.first() {
58 target = target.with_relay_url(url.clone());
59 }
60
61 let timeout = Duration::from_millis(args.timeout_ms);
62 let timestamp_us = u64::try_from(
68 SystemTime::now()
69 .duration_since(UNIX_EPOCH)
70 .map_or(0, |d| d.as_micros()),
71 )
72 .unwrap_or(u64::MAX);
73
74 let result = probe_once(&endpoint, target, hash, timestamp_us, timeout).await;
76 endpoint.close().await;
77 let (resp, resp_ext, rtt) = result?;
78
79 if resp.body.timestamp_us != timestamp_us {
83 anyhow::bail!(
84 "timestamp mismatch: sent {timestamp_us}, received {}",
85 resp.body.timestamp_us
86 );
87 }
88 if resp.body.hash != hash {
89 anyhow::bail!("hash mismatch: response is for a different blob");
90 }
91
92 resp.validate()
104 .map_err(|e| anyhow::anyhow!("rejecting probe response: {e}"))?;
105
106 let mut stdout = std::io::stdout().lock();
107 write_probe_response(&mut stdout, &resp, &resp_ext, rtt.ms, args.json)
108 .map_err(|e| anyhow::anyhow!("failed to write probe response: {e}"))?;
109 Ok(())
110}
111
112pub(crate) fn write_probe_response(
118 w: &mut impl std::io::Write,
119 resp: &decdn_protocol::message::ProbeResponse,
120 resp_ext: &decdn_protocol::ProbeResponseExt,
121 rtt_ms: f64,
122 json: bool,
123) -> std::io::Result<()> {
124 use std::fmt::Write as _;
125
126 let hash_hex = {
127 let mut s = String::with_capacity(64);
128 for b in resp.body.hash {
129 let _ = write!(s, "{b:02x}");
130 }
131 s
132 };
133 let slash_sig_hex = {
134 let mut s = String::with_capacity(resp.slash_sig.len() * 2);
135 for b in &resp.slash_sig {
136 let _ = write!(s, "{b:02x}");
137 }
138 s
139 };
140
141 if json {
142 let rtt_ms_quantized = (rtt_ms * 1000.0).round() / 1000.0;
146 let output = serde_json::json!({
147 "hash": hash_hex,
148 "has_blob": resp.body.has_blob,
149 "rate_per_mb": resp.body.rate_per_mb,
150 "total_bytes": resp_ext.total_bytes,
151 "timestamp_us": resp.body.timestamp_us,
152 "rtt_ms": rtt_ms_quantized,
153 "slash_sig": slash_sig_hex,
154 });
155 writeln!(w, "{output}")
156 } else {
157 writeln!(w, "hash: {hash_hex}")?;
158 writeln!(w, "has_blob: {}", resp.body.has_blob)?;
159 writeln!(w, "rate_per_mb: {} (base units)", resp.body.rate_per_mb)?;
160 match resp_ext.total_bytes {
161 Some(n) => writeln!(w, "total_bytes: {n}")?,
162 None => writeln!(w, "total_bytes: (unknown)")?,
163 }
164 writeln!(w, "timestamp_us: {} (echoed ok)", resp.body.timestamp_us)?;
165 writeln!(w, "rtt: {rtt_ms:.3} ms")?;
166 writeln!(w, "slash_sig: {slash_sig_hex}")
167 }
168}
169
170#[cfg(test)]
171#[allow(
172 clippy::unwrap_used,
173 clippy::expect_used,
174 clippy::indexing_slicing,
175 clippy::panic
176)]
177mod tests;