Skip to content

Commit ed48692

Browse files
committed
Add mdns discovery example
changes: - client does not create a router - removed progress for now
1 parent 355e624 commit ed48692

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

Cargo.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ walkdir = "2.5.0"
6363
hide-proto-docs = []
6464
metrics = []
6565
default = ["hide-proto-docs"]
66+
examples = ["iroh/discovery-local-network"]
67+
68+
[[example]]
69+
name = "mdns-discovery"
70+
required-features = ["examples"]
6671

6772
[patch.crates-io]
6873
iroh = { git = "https://github.com/n0-computer/iroh.git", branch = "main" }

examples/mdns-discovery.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//! Example that runs and iroh node with local node discovery and no relay server.
2+
//!
3+
//! You can think of this as a local version of [sendme](https://www.iroh.computer/sendme)
4+
//! that only works for individual files.
5+
//!
6+
//! **This example is using a non-default feature of iroh, so you need to run it with the
7+
//! examples feature enabled.**
8+
//!
9+
//! Run the follow command to run the "accept" side, that hosts the content:
10+
//! $ cargo run --example mdns-discovery --features examples -- accept [FILE_PATH]
11+
//! Wait for output that looks like the following:
12+
//! $ cargo run --example mdns-discovery --features examples -- connect [NODE_ID] [HASH] -o [FILE_PATH]
13+
//! Run that command on another machine in the same local network, replacing [FILE_PATH] to the path on which you want to save the transferred content.
14+
use std::path::{Path, PathBuf};
15+
16+
use anyhow::{ensure, Result};
17+
use clap::{Parser, Subcommand};
18+
use iroh::{
19+
discovery::mdns::MdnsDiscovery, protocol::Router, Endpoint, PublicKey, RelayMode, SecretKey,
20+
};
21+
use iroh_blobs::{store::mem::MemStore, BlobsProtocol, Hash};
22+
use tracing_subscriber::{prelude::*, EnvFilter};
23+
24+
// set the RUST_LOG env var to one of {debug,info,warn} to see logging info
25+
pub fn setup_logging() {
26+
tracing_subscriber::registry()
27+
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
28+
.with(EnvFilter::from_default_env())
29+
.try_init()
30+
.ok();
31+
}
32+
33+
#[derive(Debug, Parser)]
34+
#[command(version, about)]
35+
pub struct Cli {
36+
#[clap(subcommand)]
37+
command: Commands,
38+
}
39+
40+
#[derive(Subcommand, Clone, Debug)]
41+
pub enum Commands {
42+
/// Launch an iroh node and provide the content at the given path
43+
Accept {
44+
/// path to the file you want to provide
45+
path: PathBuf,
46+
},
47+
/// Get the node_id and hash string from a node running accept in the local network
48+
/// Download the content from that node.
49+
Connect {
50+
/// Node ID of a node on the local network
51+
node_id: PublicKey,
52+
/// Hash of content you want to download from the node
53+
hash: Hash,
54+
/// save the content to a file
55+
#[clap(long, short)]
56+
out: Option<PathBuf>,
57+
},
58+
}
59+
60+
async fn accept(path: &Path) -> Result<()> {
61+
if !path.is_file() {
62+
println!("Content must be a file.");
63+
return Ok(());
64+
}
65+
66+
let key = get_or_generate_secret_key()?;
67+
let discovery = MdnsDiscovery::new(key.public())?;
68+
69+
println!("Starting iroh node with mdns discovery...");
70+
// create a new node
71+
let endpoint = Endpoint::builder()
72+
.secret_key(key)
73+
.add_discovery(discovery)
74+
.relay_mode(RelayMode::Disabled)
75+
.bind()
76+
.await?;
77+
let builder = Router::builder(endpoint.clone());
78+
let store = MemStore::new();
79+
let blobs = BlobsProtocol::new(&store, endpoint.clone(), None);
80+
let builder = builder.accept(iroh_blobs::ALPN, blobs.clone());
81+
let node = builder.spawn();
82+
83+
if !path.is_file() {
84+
println!("Content must be a file.");
85+
node.shutdown().await?;
86+
return Ok(());
87+
}
88+
let absolute = path.canonicalize()?;
89+
println!("Adding {} as {}...", path.display(), absolute.display());
90+
let tag = store.add_path(absolute).await?;
91+
println!("To fetch the blob:\n\tcargo run --example mdns-discovery --features examples -- connect {} {} -o [FILE_PATH]", node.endpoint().node_id(), tag.hash);
92+
tokio::signal::ctrl_c().await?;
93+
node.shutdown().await?;
94+
Ok(())
95+
}
96+
97+
async fn connect(node_id: PublicKey, hash: Hash, out: Option<PathBuf>) -> Result<()> {
98+
let key = SecretKey::generate(rand::rngs::OsRng);
99+
// todo: disable discovery publishing once https://github.com/n0-computer/iroh/issues/3401 is implemented
100+
let discovery = MdnsDiscovery::new(key.public())?;
101+
102+
println!("Starting iroh node with mdns discovery...");
103+
// create a new node
104+
let endpoint = Endpoint::builder()
105+
.secret_key(key)
106+
.add_discovery(discovery)
107+
.relay_mode(RelayMode::Disabled)
108+
.bind()
109+
.await?;
110+
let store = MemStore::new();
111+
112+
println!("NodeID: {}", endpoint.node_id());
113+
let conn = endpoint.connect(node_id, iroh_blobs::ALPN).await?;
114+
let stats = store.remote().fetch(conn, hash).await?;
115+
println!(
116+
"Fetched {} bytes for hash {}",
117+
stats.payload_bytes_read, hash
118+
);
119+
if let Some(path) = out {
120+
let absolute = std::env::current_dir()?.join(&path);
121+
ensure!(!absolute.is_dir(), "output must not be a directory");
122+
println!(
123+
"exporting {hash} to {} -> {}",
124+
path.display(),
125+
absolute.display()
126+
);
127+
let size = store.export(hash, absolute).await?;
128+
println!("Exported {size} bytes");
129+
}
130+
131+
endpoint.close().await;
132+
// Shutdown the store. This is not needed for the mem store, but would be
133+
// necessary for a persistent store to allow it to write any pending data to disk.
134+
store.shutdown().await?;
135+
Ok(())
136+
}
137+
138+
#[tokio::main]
139+
async fn main() -> anyhow::Result<()> {
140+
setup_logging();
141+
let cli = Cli::parse();
142+
143+
match &cli.command {
144+
Commands::Accept { path } => {
145+
accept(path).await?;
146+
}
147+
Commands::Connect { node_id, hash, out } => {
148+
connect(*node_id, *hash, out.clone()).await?;
149+
}
150+
}
151+
Ok(())
152+
}
153+
154+
/// Gets a secret key from the IROH_SECRET environment variable or generates a new random one.
155+
/// If the environment variable is set, it must be a valid string representation of a secret key.
156+
pub fn get_or_generate_secret_key() -> Result<SecretKey> {
157+
use std::{env, str::FromStr};
158+
159+
use anyhow::Context;
160+
use rand::thread_rng;
161+
if let Ok(secret) = env::var("IROH_SECRET") {
162+
// Parse the secret key from string
163+
SecretKey::from_str(&secret).context("Invalid secret key format")
164+
} else {
165+
// Generate a new random key
166+
let secret_key = SecretKey::generate(&mut thread_rng());
167+
println!(
168+
"Generated new secret key: {}",
169+
hex::encode(secret_key.to_bytes())
170+
);
171+
println!("To reuse this key, set the IROH_SECRET environment variable to this value");
172+
Ok(secret_key)
173+
}
174+
}

0 commit comments

Comments
 (0)