diff --git a/.github/workflows/release-dcutr-example.yml b/.github/workflows/release-dcutr-example.yml new file mode 100644 index 0000000..166f250 --- /dev/null +++ b/.github/workflows/release-dcutr-example.yml @@ -0,0 +1,70 @@ +name: Build and release dcutr example +on: + push: + branches: + - master +permissions: write-all +jobs: + metadata: + name: Get release metadata + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get_version.outputs.version }} + release_exists: ${{ steps.check_release.outputs.exists }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get version + id: get_version + run: echo "version=dcutr-example-$(cargo read-manifest --manifest-path examples/dcutr/Cargo.toml | jq -r '.version')" >> $GITHUB_OUTPUT + + - name: Check if release exists + id: check_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RELEASE_URL=$(curl --silent "https://api.github.com/repos/calimero-network/relay-server/releases/tags/${{ steps.get_version.outputs.version }}" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" | jq -r '.url') + if [[ "$RELEASE_URL" != "null" ]]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + release: + name: Build and release + runs-on: ubuntu-latest + needs: metadata + if: needs.metadata.outputs.release_exists == 'false' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup rust toolchain + run: rustup toolchain install stable --profile minimal + + - name: Setup rust cache + uses: Swatinem/rust-cache@v2 + + - name: Build for Intel Linux + run: cargo build -p dcutr-example --release --target=x86_64-unknown-linux-gnu + + - name: Build for Aarch Linux + run: cross build -p dcutr-example --release --target=aarch64-unknown-linux-gnu + + - name: Create artifacts directory + run: | + mkdir -p artifacts + cp target/x86_64-unknown-linux-gnu/release/dcutr-example artifacts/dcutr-example-x86_64-unknown-linux + cp target/aarch64-unknown-linux-gnu/release/dcutr-example artifacts/dcutr-example-aarch64-unknown-linux + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.metadata.outputs.version }} + files: | + artifacts/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 1bc1d3c..7c90fef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,6 +219,19 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "asynchronous-codec" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "asynchronous-codec" version = "0.7.0" @@ -567,6 +580,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dcutr-example" +version = "0.1.0" +dependencies = [ + "camino", + "clap", + "eyre", + "libp2p", + "multiaddr", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + [[package]] name = "der" version = "0.7.9" @@ -1244,6 +1274,7 @@ dependencies = [ "libp2p-allow-block-list", "libp2p-connection-limits", "libp2p-core", + "libp2p-dcutr", "libp2p-dns", "libp2p-identify", "libp2p-identity", @@ -1316,6 +1347,29 @@ dependencies = [ "void", ] +[[package]] +name = "libp2p-dcutr" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f7bb7fa2b9e6cad9c30a6f67e3ff5c1e4b658c62b6375e35861a85f9c97bf3" +dependencies = [ + "asynchronous-codec 0.6.2", + "either", + "futures", + "futures-bounded", + "futures-timer", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "lru 0.11.1", + "quick-protobuf", + "quick-protobuf-codec 0.2.0", + "thiserror", + "tracing", + "void", +] + [[package]] name = "libp2p-dns" version = "0.41.1" @@ -1338,7 +1392,7 @@ version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20499a945d2f0221fdc6269b3848892c0f370d2ee3e19c7f65a29d8f860f6126" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.7.0", "either", "futures", "futures-bounded", @@ -1346,9 +1400,9 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "lru", + "lru 0.12.3", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1", "smallvec", "thiserror", "tracing", @@ -1403,6 +1457,7 @@ dependencies = [ "futures", "instant", "libp2p-core", + "libp2p-dcutr", "libp2p-identify", "libp2p-identity", "libp2p-ping", @@ -1418,7 +1473,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecd0545ce077f6ea5434bcb76e8d0fe942693b4380aaad0d34a358c2bd05793" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.7.0", "bytes", "curve25519-dalek", "futures", @@ -1486,7 +1541,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0aadb213ffc8e1a6f2b9c48dcf0fc07bf370f2ea4db7981813d45e50671c8d9d" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.7.0", "bytes", "either", "futures", @@ -1497,7 +1552,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1", "rand", "static_assertions", "thiserror", @@ -1635,6 +1690,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown", +] + [[package]] name = "lru" version = "0.12.3" @@ -2134,13 +2198,26 @@ dependencies = [ "byteorder", ] +[[package]] +name = "quick-protobuf-codec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ededb1cd78531627244d51dd0c7139fbe736c7d57af0092a76f0ffb2f56e98" +dependencies = [ + "asynchronous-codec 0.6.2", + "bytes", + "quick-protobuf", + "thiserror", + "unsigned-varint 0.7.2", +] + [[package]] name = "quick-protobuf-codec" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.7.0", "bytes", "quick-protobuf", "thiserror", @@ -2955,6 +3032,10 @@ name = "unsigned-varint" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" +dependencies = [ + "asynchronous-codec 0.6.2", + "bytes", +] [[package]] name = "unsigned-varint" diff --git a/Cargo.toml b/Cargo.toml index 23eaaf3..6b98fd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = [".", "examples/dcutr"] + [package] name = "relay-server" version = "0.2.0" diff --git a/examples/dcutr/Cargo.toml b/examples/dcutr/Cargo.toml new file mode 100644 index 0000000..f8e4295 --- /dev/null +++ b/examples/dcutr/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "dcutr-example" +version = "0.1.0" +authors = ["Calimero Limited "] +edition = "2021" +repository = "https://github.com/calimero-network/relay-server" +license = "MIT OR Apache-2.0" + +[dependencies] +camino = "1.1.6" +clap = { version = "4.5.4", features = ["derive", "env"] } +eyre = "0.6.12" +libp2p = { version = "0.53.2", features = [ + "dcutr", + "dns", + "identify", + "macros", + "noise", + "ping", + "quic", + "relay", + "tokio", + "tcp", + "tls", + "yamux", +] } +multiaddr = "0.18.1" +serde = "1.0.196" +serde_json = "1.0.113" +tokio = { version = "1.35.1", features = [ + "io-std", + "macros", + "rt", + "rt-multi-thread", +] } +toml = "0.8.9" +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/examples/dcutr/src/main.rs b/examples/dcutr/src/main.rs new file mode 100644 index 0000000..2b42b71 --- /dev/null +++ b/examples/dcutr/src/main.rs @@ -0,0 +1,234 @@ +use std::str::FromStr; +use std::{error::Error, time::Duration}; + +use clap::Parser; +use libp2p::futures::prelude::*; +use libp2p::swarm::{NetworkBehaviour, SwarmEvent}; +use libp2p::{dcutr, identify, identity, noise, ping, relay, yamux, Multiaddr, PeerId}; +use multiaddr::Protocol; +use tokio::io::AsyncBufReadExt; +use tracing::info; +use tracing_subscriber::prelude::*; +use tracing_subscriber::EnvFilter; + +#[derive(Debug, Parser)] +#[clap(name = "DCUtR client example")] +struct Opt { + /// The mode (client-listen, client-dial). + #[clap(long)] + mode: Mode, + + /// Fixed value to generate deterministic peer id. + #[clap(long)] + secret_key_seed: u8, + + /// The listening address + #[clap(long)] + relay_address: Multiaddr, + + /// Peer ID of the remote peer to hole punch to. + #[clap(long)] + remote_peer_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Parser)] +enum Mode { + Dial, + Listen, +} + +impl FromStr for Mode { + type Err = String; + fn from_str(mode: &str) -> Result { + match mode { + "dial" => Ok(Mode::Dial), + "listen" => Ok(Mode::Listen), + _ => Err("Expected either 'dial' or 'listen'".to_string()), + } + } +} + +#[derive(NetworkBehaviour)] +struct Behaviour { + relay_client: relay::client::Behaviour, + ping: ping::Behaviour, + identify: identify::Behaviour, + dcutr: dcutr::Behaviour, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::registry() + .with(EnvFilter::builder().parse(format!( + "info,{}", + std::env::var("RUST_LOG").unwrap_or_default() + ))?) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let opt = Opt::parse(); + + let mut swarm = + libp2p::SwarmBuilder::with_existing_identity(generate_ed25519(opt.secret_key_seed)) + .with_tokio() + .with_tcp( + libp2p::tcp::Config::default() + .port_reuse(true) + .nodelay(true), + (libp2p::tls::Config::new, libp2p::noise::Config::new), + libp2p::yamux::Config::default, + )? + .with_quic() + .with_dns()? + .with_relay_client(noise::Config::new, yamux::Config::default)? + .with_behaviour(|keypair, relay_behaviour| Behaviour { + relay_client: relay_behaviour, + ping: ping::Behaviour::new(ping::Config::new()), + identify: identify::Behaviour::new(identify::Config::new( + "/relay-server/0.2.0".to_string(), + keypair.public(), + )), + dcutr: dcutr::Behaviour::new(keypair.public().to_peer_id()), + })? + .with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(60))) + .build(); + + swarm + .listen_on("/ip4/0.0.0.0/udp/0/quic-v1".parse().unwrap()) + .unwrap(); + swarm + .listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap()) + .unwrap(); + + // Wait to listen on all interfaces. + loop { + tokio::select! { + Some(event) = swarm.next() => { + match event { + SwarmEvent::NewListenAddr { address, .. } => { + info!(%address, "\x1b[34mswarm\x1b[0m Listening on address"); + } + event => info!("unexpected: {event:?}"), + } + } + _ = tokio::time::sleep(Duration::from_secs(1)) => { + // Likely listening on all interfaces now, thus continuing by breaking the loop. + break; + } + } + } + + // Connect to the relay server. Not for the reservation or relayed connection, but to (a) learn + // our local public address and (b) enable a freshly started relay to learn its public address. + swarm.dial(opt.relay_address.clone()).unwrap(); + + let mut learned_observed_addr = false; + let mut told_relay_observed_addr = false; + + loop { + match swarm.next().await.unwrap() { + SwarmEvent::NewListenAddr { .. } => {} + SwarmEvent::Dialing { .. } => {} + SwarmEvent::ConnectionEstablished { .. } => {} + SwarmEvent::Behaviour(BehaviourEvent::Ping(_)) => {} + SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Sent { .. })) => { + info!("\x1b[33mrelay\x1b[0m Told relay its public address"); + told_relay_observed_addr = true; + } + SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received { + info: identify::Info { observed_addr, .. }, + .. + })) => { + info!(address=%observed_addr, "\x1b[33mrelay\x1b[0m Relay told us our observed address"); + learned_observed_addr = true; + } + event => info!("unexpected: {event:?}"), + } + + if learned_observed_addr && told_relay_observed_addr { + break; + } + } + + match opt.mode { + Mode::Dial => { + swarm + .dial( + opt.relay_address + .with(multiaddr::Protocol::P2pCircuit) + .with(Protocol::P2p(opt.remote_peer_id.unwrap())), + ) + .unwrap(); + } + Mode::Listen => { + swarm + .listen_on(opt.relay_address.with(Protocol::P2pCircuit)) + .unwrap(); + } + } + + let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines(); + + loop { + let event = tokio::select! { + Some(event) = swarm.next() => event, + Ok(Some(line)) = stdin.next_line() => { + match line.trim() { + "peers" => { + info!("\x1b[34mswarm\x1b[0m Connected peers: {}", swarm.network_info().num_peers()); + for peer in swarm.connected_peers() { + info!(peer=%peer, "\x1b[34mswarm\x1b[0m Connected peer"); + } + } + _ => info!("\x1b[34mswarm\x1b[0m Unknown command"), + } + continue; + } + }; + + match event { + SwarmEvent::NewListenAddr { address, .. } => { + info!(%address, "\x1b[34mswarm\x1b[0m Listening on address"); + } + SwarmEvent::Behaviour(BehaviourEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. }, + )) => { + assert!(opt.mode == Mode::Listen); + info!("\x1b[33mrelay\x1b[0m Relay accepted our reservation request"); + } + SwarmEvent::Behaviour(BehaviourEvent::RelayClient(event)) => { + info!(?event, "\x1b[33mrelay\x1b[0m"); + } + SwarmEvent::Behaviour(BehaviourEvent::Dcutr(event)) => { + info!(?event, "\x1b[32mdcutr\x1b[0m"); + } + SwarmEvent::Behaviour(BehaviourEvent::Identify(event)) => { + info!(?event, "\x1b[36midentify\x1b[0m"); + } + SwarmEvent::Behaviour(BehaviourEvent::Ping(event)) => { + info!(?event, "\x1b[35mping\x1b[0m"); + } + SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + } => { + info!(peer=%peer_id, ?endpoint, "\x1b[34mswarm\x1b[0m Connection established"); + } + SwarmEvent::ConnectionClosed { + peer_id, endpoint, .. + } => { + info!(peer=%peer_id, ?endpoint, "\x1b[34mswarm\x1b[0m Connection closed"); + } + SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { + info!(peer=?peer_id, "\x1b[34mswarm\x1b[0m Outgoing connection failed: {error}"); + } + event => info!(?event, "\x1b[34mswarm\x1b[0m"), + }; + } +} + +fn generate_ed25519(secret_key_seed: u8) -> identity::Keypair { + let mut bytes = [0u8; 32]; + bytes[0] = secret_key_seed; + + identity::Keypair::ed25519_from_bytes(bytes).expect("only errors on wrong length") +}