From 76c1d442f409edd4844b7ac6168ae1b9cbdfa5f8 Mon Sep 17 00:00:00 2001 From: glyph Date: Thu, 23 Mar 2023 18:28:16 +0200 Subject: [PATCH] Introduce `identify` and `rendezvous` network protocols (#304) * Add rendezvous and identify feature dependencies for libp2p * Introduce configuration parameters for rendezvous client and server * Move network behaviour composer into separate module * Add behaviour handlers for identify and rendezvous * Add custom validator for rendezvous client mode * Add handler to dial peers discovered via rendezvous server * Log failed registration attempt with rendezvous server * Add handlers for Sent, Pushed and Error identify events * Add serde feature to libp2p dependency * Add libp2p dependency to CLI for parser * Update lockfile * Replace String with Multiaddr and PeerId in CLI and config * Add glyph to authors * Update CHANGELOG * Remove TODOs and errant whitespace * Manually add external address and discover peers on rendezvous connection establishment * Make log formatting syntax more consistent * Update CLI usage instructions * Add additional rendezvous client behaviour check and unwrap comments * Use if let to avoid unwrap --- CHANGELOG.md | 2 + Cargo.lock | 205 ++++++++++++++++++++++++++++- aquadoggo/Cargo.toml | 4 + aquadoggo/src/network/behaviour.rs | 101 ++++++++++++++ aquadoggo/src/network/config.rs | 26 +++- aquadoggo/src/network/mod.rs | 1 + aquadoggo/src/network/service.rs | 193 ++++++++++++++++++++------- aquadoggo_cli/Cargo.toml | 2 + aquadoggo_cli/README.md | 22 ++++ aquadoggo_cli/src/main.rs | 59 ++++++++- 10 files changed, 555 insertions(+), 60 deletions(-) create mode 100644 aquadoggo/src/network/behaviour.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 052af6922..9c46a77c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Introduce `identify` and `rendezvous` network protocols / behaviours [#304](https://github.com/p2panda/aquadoggo/pull/304) + ### Added - Introduce libp2p networking service and configuration [#282](https://github.com/p2panda/aquadoggo/pull/282) diff --git a/Cargo.lock b/Cargo.lock index 9e4c9c8c3..8087be9a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -202,6 +213,7 @@ dependencies = [ "aquadoggo", "clap", "env_logger", + "libp2p", "tokio", ] @@ -613,6 +625,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bimap" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b" + [[package]] name = "bincode" version = "1.3.3" @@ -1002,6 +1020,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-bigint" version = "0.4.9" @@ -1808,7 +1832,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", ] [[package]] @@ -1817,7 +1850,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -1887,6 +1920,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "hkdf" version = "0.12.3" @@ -2072,7 +2111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] @@ -2217,11 +2256,15 @@ dependencies = [ "instant", "libp2p-core", "libp2p-dns", + "libp2p-gossipsub", + "libp2p-identify", "libp2p-identity", + "libp2p-kad", "libp2p-mdns", "libp2p-metrics", "libp2p-ping", "libp2p-quic", + "libp2p-rendezvous", "libp2p-swarm", "libp2p-tcp", "libp2p-webrtc", @@ -2251,6 +2294,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "rw-stream-sink", + "serde", "smallvec", "thiserror", "unsigned-varint", @@ -2271,6 +2315,59 @@ dependencies = [ "trust-dns-resolver", ] +[[package]] +name = "libp2p-gossipsub" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708235886ca7c8f3792a8683ef81f9b7fb0a952bbd15fe5038b7610689a88390" +dependencies = [ + "asynchronous-codec", + "base64 0.21.0", + "byteorder", + "bytes", + "fnv", + "futures", + "hex_fmt", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "log", + "prometheus-client", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "regex", + "serde", + "sha2 0.10.6", + "smallvec", + "thiserror", + "unsigned-varint", + "wasm-timer", +] + +[[package]] +name = "libp2p-identify" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d1da1f75baf824cfdc80f6aced51f7cbf8dc14e32363e9443570a80d4ee337" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "log", + "lru", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec", + "thiserror", + "void", +] + [[package]] name = "libp2p-identity" version = "0.1.0" @@ -2286,10 +2383,40 @@ dependencies = [ "prost-build", "quick-protobuf", "rand 0.8.5", + "serde", "thiserror", "zeroize", ] +[[package]] +name = "libp2p-kad" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc57e02d7ad49a63792370f24b829af38f34982ff56556e59e4cb325a4dbf6b" +dependencies = [ + "arrayvec 0.7.2", + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "log", + "quick-protobuf", + "rand 0.8.5", + "serde", + "sha2 0.10.6", + "smallvec", + "thiserror", + "uint", + "unsigned-varint", + "void", +] + [[package]] name = "libp2p-mdns" version = "0.43.0" @@ -2317,6 +2444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a42ec91e227d7d0dafa4ce88b333cdf5f277253873ab087555c92798db2ddd46" dependencies = [ "libp2p-core", + "libp2p-identify", "libp2p-ping", "libp2p-swarm", "prometheus-client", @@ -2384,6 +2512,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "libp2p-rendezvous" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633f2dc23d63ad04955642f3025e740a943da4deb79b252b5fcf882208164467" +dependencies = [ + "asynchronous-codec", + "bimap", + "futures", + "futures-timer", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "log", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "thiserror", + "void", +] + [[package]] name = "libp2p-swarm" version = "0.42.0" @@ -2537,6 +2687,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "lru-cache" version = "0.1.2" @@ -2689,6 +2848,8 @@ dependencies = [ "core2", "digest 0.10.6", "multihash-derive", + "serde", + "serde-big-array", "sha2 0.10.6", "unsigned-varint", ] @@ -3988,6 +4149,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd31f59f6fe2b0c055371bb2f16d7f0aa7d8881676c04a55b1596d1a17cd10a4" +dependencies = [ + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.4.5" @@ -4233,7 +4403,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" dependencies = [ - "ahash", + "ahash 0.7.6", "atoi", "base64 0.13.1", "bitflags", @@ -4805,6 +4975,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions 1.1.0", +] + [[package]] name = "unicode-bidi" version = "0.3.11" @@ -5055,6 +5237,21 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.61" diff --git a/aquadoggo/Cargo.toml b/aquadoggo/Cargo.toml index 790d58476..773ed19af 100644 --- a/aquadoggo/Cargo.toml +++ b/aquadoggo/Cargo.toml @@ -7,6 +7,7 @@ authors = [ "pietgeursen ", "sandreae ", "sophiiistika ", + "glyph ", ] description = "Embeddable p2p network node" license = "AGPL-3.0-or-later" @@ -38,6 +39,9 @@ libp2p = { version = "^0.51.0", features = [ "quic", "ping", "mdns", + "identify", + "rendezvous", + "serde", ] } lipmaa-link = "^0.2.2" log = "^0.4.17" diff --git a/aquadoggo/src/network/behaviour.rs b/aquadoggo/src/network/behaviour.rs new file mode 100644 index 000000000..779aaacee --- /dev/null +++ b/aquadoggo/src/network/behaviour.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use anyhow::Result; +use libp2p::identity::Keypair; +use libp2p::swarm::behaviour::toggle::Toggle; +use libp2p::swarm::NetworkBehaviour; +use libp2p::{identify, mdns, ping, rendezvous, PeerId}; +use log::debug; + +use crate::network::config::NODE_NAMESPACE; +use crate::network::NetworkConfiguration; + +/// Network behaviour for the aquadoggo node. +#[derive(NetworkBehaviour)] +pub struct Behaviour { + /// Automatically discover peers on the local network. + pub mdns: Toggle, + + /// Respond to inbound pings and periodically send outbound ping on every established + /// connection. + pub ping: Toggle, + + /// Register with a rendezvous server and query remote peer addresses. + pub rendezvous_client: Toggle, + + /// Serve as a rendezvous point for remote peers to register their external addresses + /// and query the addresses of other peers. + pub rendezvous_server: Toggle, + + /// Periodically exchange information between peer on an established connection. This + /// is useful for learning the external address of the local node from a remote peer. + pub identify: Toggle, +} + +impl Behaviour { + /// Generate a new instance of the composed network behaviour according to + /// the network configuration context. + pub fn new( + network_config: &NetworkConfiguration, + peer_id: PeerId, + key_pair: Keypair, + ) -> Result { + let public_key = key_pair.public(); + + // Create an mDNS behaviour with default configuration if the mDNS flag is set + let mdns = if network_config.mdns { + debug!("mDNS network behaviour enabled"); + Some(mdns::Behaviour::new(Default::default(), peer_id)?) + } else { + None + }; + + // Create a ping behaviour with default configuration if the ping flag is set + let ping = if network_config.ping { + debug!("Ping network behaviour enabled"); + Some(ping::Behaviour::default()) + } else { + None + }; + + // Create a rendezvous client behaviour with default configuration if the rendezvous client + // flag is set + let rendezvous_client = if network_config.rendezvous_client { + debug!("Rendezvous client network behaviour enabled"); + Some(rendezvous::client::Behaviour::new(key_pair)) + } else { + None + }; + + // Create a rendezvous server behaviour with default configuration if the rendezvous server + // flag is set + let rendezvous_server = if network_config.rendezvous_server { + debug!("Rendezvous server network behaviour enabled"); + Some(rendezvous::server::Behaviour::new( + rendezvous::server::Config::default(), + )) + } else { + None + }; + + // Create an identify server behaviour with default configuration if either the rendezvous + // client or server flag is set + let identify = if network_config.rendezvous_client || network_config.rendezvous_server { + debug!("Identify network behaviour enabled"); + Some(identify::Behaviour::new(identify::Config::new( + format!("{NODE_NAMESPACE}/1.0.0"), + public_key, + ))) + } else { + None + }; + + Ok(Self { + mdns: mdns.into(), // Convert the `Option` into a `Toggle` + ping: ping.into(), + rendezvous_client: rendezvous_client.into(), + rendezvous_server: rendezvous_server.into(), + identify: identify.into(), + }) + } +} diff --git a/aquadoggo/src/network/config.rs b/aquadoggo/src/network/config.rs index 63fe04c61..6cde0adc4 100644 --- a/aquadoggo/src/network/config.rs +++ b/aquadoggo/src/network/config.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use anyhow::Result; use libp2p::identity::Keypair; use libp2p::swarm::ConnectionLimits; +use libp2p::{Multiaddr, PeerId}; use log::info; use serde::{Deserialize, Serialize}; @@ -13,6 +14,9 @@ use crate::network::identity::Identity; /// Key pair file name. const KEY_PAIR_FILE_NAME: &str = "private-key"; +/// The namespace used by the `identify` network behaviour. +pub const NODE_NAMESPACE: &str = "aquadoggo"; + /// Network config for the node. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct NetworkConfiguration { @@ -71,7 +75,23 @@ pub struct NetworkConfiguration { pub quic_port: u16, /// The addresses of remote peers to replicate from. - pub remote_peers: Vec, + pub remote_peers: Vec, + + /// Rendezvous client behaviour enabled. + /// + /// Connect to a rendezvous point, register the local node and query addresses of remote peers. + pub rendezvous_client: bool, + + /// Rendezvous server behaviour enabled. + /// + /// Serve as a rendezvous point for peer discovery, allowing peer registration and queries. + pub rendezvous_server: bool, + + /// Address of a rendezvous server in the form of a multiaddress. + pub rendezvous_address: Option, + + /// Peer ID of a rendezvous server. + pub rendezvous_peer_id: Option, } impl Default for NetworkConfiguration { @@ -89,6 +109,10 @@ impl Default for NetworkConfiguration { ping: false, quic_port: 2022, remote_peers: Vec::new(), + rendezvous_client: false, + rendezvous_server: false, + rendezvous_address: None, + rendezvous_peer_id: None, } } } diff --git a/aquadoggo/src/network/mod.rs b/aquadoggo/src/network/mod.rs index f62375b9d..520d3146b 100644 --- a/aquadoggo/src/network/mod.rs +++ b/aquadoggo/src/network/mod.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +mod behaviour; mod config; mod identity; mod service; diff --git a/aquadoggo/src/network/service.rs b/aquadoggo/src/network/service.rs index bfdb93f1d..b8c9b4fe8 100644 --- a/aquadoggo/src/network/service.rs +++ b/aquadoggo/src/network/service.rs @@ -5,57 +5,20 @@ use std::convert::TryInto; use anyhow::Result; use futures::StreamExt; use libp2p::core::muxing::StreamMuxerBox; +use libp2p::multiaddr::Protocol; use libp2p::ping::Event; -use libp2p::swarm::behaviour::toggle::Toggle; -use libp2p::swarm::{NetworkBehaviour, SwarmBuilder, SwarmEvent}; -use libp2p::{mdns, ping, quic, Multiaddr, PeerId, Transport}; +use libp2p::swarm::{AddressScore, SwarmBuilder, SwarmEvent}; +use libp2p::{identify, mdns, quic, rendezvous, Multiaddr, PeerId, Transport}; use log::{debug, info, warn}; use crate::bus::ServiceSender; use crate::context::Context; use crate::manager::{ServiceReadySender, Shutdown}; +use crate::network::behaviour::{Behaviour, BehaviourEvent}; +use crate::network::config::NODE_NAMESPACE; use crate::network::NetworkConfiguration; -/// Network behaviour for the aquadoggo node. -#[derive(NetworkBehaviour)] -struct Behaviour { - /// Automatically discover peers on the local network. - mdns: Toggle, - - /// Respond to inbound pings and periodically send outbound ping on every established - /// connection. - ping: Toggle, -} - -impl Behaviour { - /// Generate a new instance of the composed network behaviour according to - /// the network configuration context. - fn new(network_config: &NetworkConfiguration, peer_id: PeerId) -> Result { - // Create an mDNS behaviour with default configuration if the mDNS flag is set - let mdns = if network_config.mdns { - debug!("mDNS network behaviour enabled"); - Some(mdns::Behaviour::new(Default::default(), peer_id)?) - } else { - None - }; - - // Create a ping behaviour with default configuration if the ping flag is set - let ping = if network_config.ping { - debug!("Ping network behaviour enabled"); - Some(ping::Behaviour::default()) - } else { - None - }; - - Ok(Self { - mdns: mdns.into(), // Convert the `Option` into a `Toggle` - ping: ping.into(), - }) - } -} - -/// Network service that configures and deploys a network swarm over QUIC transports, -/// with mDNS provided for peer discovery on the local network. +/// Network service that configures and deploys a network swarm over QUIC transports. /// /// The swarm listens for incoming connections, dials remote nodes, manages /// connections and executes predefined network behaviours. @@ -88,7 +51,7 @@ pub async fn network_service( // Instantiate the custom network behaviour with default configuration // and the libp2p peer ID - let behaviour = Behaviour::new(&network_config, peer_id)?; + let behaviour = Behaviour::new(&network_config, peer_id, key_pair)?; // Initialise a swarm with QUIC transports, our composed network behaviour // and the default configuration parameters @@ -103,13 +66,20 @@ pub async fn network_service( // Listen for incoming connection requests over the QUIC transport swarm.listen_on(quic_multiaddr)?; - // Dial the peer identified by the multi-address given in the `--remote-node-addresses` if given - if let Some(addr) = network_config.remote_peers.get(0) { - let remote: Multiaddr = addr.parse()?; - swarm.dial(remote)?; + // Dial each peer identified by the multi-address provided via `--remote-node-addresses` if given + for addr in network_config.remote_peers.clone() { + swarm.dial(addr)? + } + + // Dial the peer identified by the multi-address provided via `--rendezvous_address` if given + if let Some(addr) = network_config.rendezvous_address.clone() { + swarm.dial(addr)?; } - // Spawn a task logging swarm events + // Create a cookie holder for the identify service + let mut cookie = None; + + // Spawn a task to handle swarm events let handle = tokio::spawn(async move { loop { match swarm.select_next_some().await { @@ -140,13 +110,36 @@ pub async fn network_service( } => { debug!("ConnectionClosed: {peer_id} {endpoint:?} {num_established} {cause:?}") } + SwarmEvent::ConnectionEstablished { peer_id, .. } + // Match on a connection with the rendezvous server + if network_config.rendezvous_client + // Should be safe to unwrap rendezvous_peer_id because the CLI parser ensures + // it's provided if rendezvous_client is set to true + && network_config.rendezvous_peer_id.unwrap() == peer_id => + { + if let Some(rendezvous_client) = + swarm.behaviour_mut().rendezvous_client.as_mut() + { + debug!( + "Connected to rendezvous point, discovering nodes in '{NODE_NAMESPACE}' namespace ..." + ); + + rendezvous_client.discover( + Some(rendezvous::Namespace::from_static(NODE_NAMESPACE)), + None, + None, + network_config + .rendezvous_peer_id + .expect("Rendezvous server peer ID was provided"), + ); + } + } SwarmEvent::ConnectionEstablished { peer_id, endpoint, num_established, .. } => debug!("ConnectionEstablished: {peer_id} {endpoint:?} {num_established}"), - SwarmEvent::Dialing(peer_id) => info!("Dialing: {peer_id}"), SwarmEvent::ExpiredListenAddr { listener_id, @@ -179,7 +172,105 @@ pub async fn network_service( SwarmEvent::OutgoingConnectionError { peer_id, error } => { warn!("OutgoingConnectionError: {peer_id:?} {error:?}") } - }; + SwarmEvent::Behaviour(BehaviourEvent::RendezvousClient(event)) => match event { + rendezvous::client::Event::Registered { + namespace, + ttl, + rendezvous_node, + } => { + debug!("Registered for namespace '{namespace}' at rendezvous point {rendezvous_node} for the next {ttl} seconds") + } + rendezvous::client::Event::Discovered { + registrations, + cookie: new_cookie, + .. + } => { + debug!("Rendezvous point responded with peer registration data"); + + cookie.replace(new_cookie); + + for registration in registrations { + for address in registration.record.addresses() { + let peer = registration.record.peer_id(); + debug!("Discovered peer {peer} at {address}"); + + let p2p_suffix = Protocol::P2p(*peer.as_ref()); + let address_with_p2p = if !address + .ends_with(&Multiaddr::empty().with(p2p_suffix.clone())) + { + address.clone().with(p2p_suffix) + } else { + address.clone() + }; + + swarm.dial(address_with_p2p).unwrap(); + } + } + } + rendezvous::client::Event::RegisterFailed(error) => { + warn!("Failed to register with rendezvous point: {error}"); + } + other => debug!("Unhandled rendezvous client event: {other:?}"), + }, + SwarmEvent::Behaviour(BehaviourEvent::RendezvousServer(event)) => match event { + rendezvous::server::Event::PeerRegistered { peer, registration } => { + debug!( + "Peer {peer} registered for namespace '{}'", + registration.namespace + ); + } + rendezvous::server::Event::DiscoverServed { + enquirer, + registrations, + } => { + debug!( + "Served peer {enquirer} with {} registrations", + registrations.len() + ); + } + other => debug!("Unhandled rendezvous server event: {other:?}"), + }, + SwarmEvent::Behaviour(BehaviourEvent::Identify(event)) => { + match event { + identify::Event::Received { peer_id, info } => { + debug!("Received identify information from peer {peer_id}"); + debug!( + "Peer {peer_id} reported local external address: {}", + info.observed_addr + ); + + swarm.add_external_address(info.observed_addr, AddressScore::Infinite); + + // Only attempt registration if the local node is running as a rendezvous client + if network_config.rendezvous_client { + // Once `identify` information is received from a remote peer, the external + // address of the local node is known and registration with the rendezvous + // server can be carried out. + + // We call `as_mut()` on the rendezvous client network behaviour in + // order to get a mutable reference out of the `Toggle` + if let Some (rendezvous_client) = swarm.behaviour_mut().rendezvous_client.as_mut() { + rendezvous_client.register( + rendezvous::Namespace::from_static(NODE_NAMESPACE), + network_config + .rendezvous_peer_id + .expect("Rendezvous server peer ID was provided"), + None, + ); + } + } + } + identify::Event::Sent { peer_id } | identify::Event::Pushed { peer_id } => { + debug!( + "Sent identification information of the local node to peer {peer_id}" + ) + } + identify::Event::Error { peer_id, error } => { + warn!("Failed to identify the remote peer {peer_id}: {error}") + } + } + } + } } }); diff --git a/aquadoggo_cli/Cargo.toml b/aquadoggo_cli/Cargo.toml index 4be9c9d0c..41fd26e10 100644 --- a/aquadoggo_cli/Cargo.toml +++ b/aquadoggo_cli/Cargo.toml @@ -7,6 +7,7 @@ authors = [ "pietgeursen ", "sandreae ", "sophiiistika ", + "glyph ", ] license = "AGPL-3.0-or-later" repository = "https://github.com/p2panda/aquadoggo" @@ -23,6 +24,7 @@ anyhow = "^1.0.62" tokio = { version = "^1.20.1", features = ["full"] } env_logger = "^0.9.0" clap = { version = "^4.1.8", features = ["derive"] } +libp2p = "^0.51.0" [dependencies.aquadoggo] version = "~0.4.0" diff --git a/aquadoggo_cli/README.md b/aquadoggo_cli/README.md index 3fe881943..203b05439 100644 --- a/aquadoggo_cli/README.md +++ b/aquadoggo_cli/README.md @@ -4,6 +4,8 @@ Node server with GraphQL API for the p2panda network. ## Usage +When running the node as a rendezvous client (`--rendezvous-client true`) both the rendezvous address and peer ID must be provided. + ``` Options: -d, --data-dir @@ -28,6 +30,26 @@ Options: [possible values: true, false] + -C, --rendezvous-client + Enable rendezvous client to facilitate peer discovery via a rendezvous server, false by default + + [possible values: true, false] + + -S, --rendezvous-server + Enable rendezvous server to facilitate peer discovery for remote peers, false by default + + [possible values: true, false] + + --rendezvous-address + The IP address of a rendezvous server in the form of a multiaddress. + + eg. --rendezvous-address "/ip4/127.0.0.1/udp/12345/quic-v1" + + --rendezvous-peer-id + The peer ID of a rendezvous server in the form of an Ed25519 key encoded as a raw base58btc multihash. + + eg. --rendezvous-peer-id "12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA" + -h, --help Print help (see a summary with '-h') diff --git a/aquadoggo_cli/src/main.rs b/aquadoggo_cli/src/main.rs index f80a424f3..5d032860e 100644 --- a/aquadoggo_cli/src/main.rs +++ b/aquadoggo_cli/src/main.rs @@ -5,7 +5,9 @@ use std::convert::{TryFrom, TryInto}; use anyhow::Result; use aquadoggo::{Configuration, NetworkConfiguration, Node}; -use clap::Parser; +use clap::error::ErrorKind as ClapErrorKind; +use clap::{CommandFactory, Parser}; +use libp2p::{Multiaddr, PeerId}; #[derive(Parser, Debug)] #[command(name = "aquadoggo Node", version)] @@ -25,7 +27,7 @@ struct Cli { /// URLs of remote nodes to replicate with. #[arg(short, long)] - remote_node_addresses: Vec, + remote_node_addresses: Vec, /// Enable mDNS for peer discovery over LAN (using port 5353), true by default. #[arg(short, long)] @@ -34,6 +36,49 @@ struct Cli { /// Enable ping for connected peers (send and receive ping packets), true by default. #[arg(long)] ping: Option, + + /// Enable rendezvous client to facilitate peer discovery via a rendezvous server, false by default. + #[arg(short = 'C', long)] + rendezvous_client: Option, + + /// Enable rendezvous server to facilitate peer discovery for remote peers, false by default. + #[arg(short = 'S', long)] + rendezvous_server: Option, + + /// The IP address of a rendezvous server in the form of a multiaddress. + /// + /// eg. --rendezvous-address "/ip4/127.0.0.1/udp/12345/quic-v1" + #[arg(long)] + rendezvous_address: Option, + + /// The peer ID of a rendezvous server in the form of an Ed25519 key encoded as a raw + /// base58btc multihash. + /// + /// eg. --rendezvous-peer-id "12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA" + #[arg(long)] + rendezvous_peer_id: Option, +} + +impl Cli { + // Run custom validators on parsed CLI input + fn validate(self) -> Self { + // Ensure rendezvous server address and peer ID are both provided if + // rendezvous client mode has been set to `true`. Both values are required + // to dial the rendezvous server. + if let Some(true) = self.rendezvous_client { + if self.rendezvous_address.is_none() || self.rendezvous_peer_id.is_none() { + // Print a help message about the missing value(s) and exit + Cli::command() + .error( + ClapErrorKind::MissingRequiredArgument, + "'--rendezvous-address' and '--rendezvous-peer-id' must both be provided if '--rendezvous-client true'", + ) + .exit() + } + } + + self + } } impl TryFrom for Configuration { @@ -48,6 +93,10 @@ impl TryFrom for Configuration { ping: cli.ping.unwrap_or(true), quic_port: cli.quic_port.unwrap_or(2022), remote_peers: cli.remote_node_addresses, + rendezvous_client: cli.rendezvous_client.unwrap_or(false), + rendezvous_server: cli.rendezvous_server.unwrap_or(false), + rendezvous_address: cli.rendezvous_address, + rendezvous_peer_id: cli.rendezvous_peer_id, ..NetworkConfiguration::default() }; @@ -59,8 +108,10 @@ impl TryFrom for Configuration { async fn main() { env_logger::init(); - // Parse command line arguments and load configuration - let cli = Cli::parse(); + // Parse command line arguments and run custom validators + let cli = Cli::parse().validate(); + + // Load configuration parameters and apply defaults let config = cli.try_into().expect("Could not load configuration"); // Start p2panda node in async runtime