diff --git a/Cargo.lock b/Cargo.lock index 5eeb43f1c..c77a0b184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3690,6 +3690,7 @@ dependencies = [ "getrandom", "instant", "libp2p-allow-block-list", + "libp2p-autonat", "libp2p-connection-limits", "libp2p-core", "libp2p-dns", @@ -3726,6 +3727,27 @@ dependencies = [ "void", ] +[[package]] +name = "libp2p-autonat" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95151726170e41b591735bf95c42b888fe4aa14f65216a9fbf0edcc04510586" +dependencies = [ + "async-trait", + "asynchronous-codec 0.6.2", + "futures", + "futures-timer", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-request-response", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec 0.2.0", + "rand", + "tracing", +] + [[package]] name = "libp2p-connection-limits" version = "0.3.1" diff --git a/homestar-runtime/Cargo.toml b/homestar-runtime/Cargo.toml index 204e3f7a3..d8c45c0ba 100644 --- a/homestar-runtime/Cargo.toml +++ b/homestar-runtime/Cargo.toml @@ -97,6 +97,7 @@ jsonrpsee = { version = "0.21", default-features = false, features = [ ] } libipld = { workspace = true } libp2p = { version = "0.53", default-features = false, features = [ + "autonat", "dns", "kad", "request-response", diff --git a/homestar-runtime/src/event_handler/notification.rs b/homestar-runtime/src/event_handler/notification.rs index 69611a0d8..abe5867c8 100644 --- a/homestar-runtime/src/event_handler/notification.rs +++ b/homestar-runtime/src/event_handler/notification.rs @@ -20,8 +20,8 @@ pub(crate) use network::{ NetworkNotification, NewListenAddr, OutgoingConnectionError, PeerRegisteredRendezvous, PublishedReceiptPubsub, PutReceiptDht, PutWorkflowInfoDht, ReceiptQuorumFailureDht, ReceiptQuorumSuccessDht, ReceivedReceiptPubsub, ReceivedWorkflowInfo, RegisteredRendezvous, - SentWorkflowInfo, WorkflowInfoQuorumFailureDht, WorkflowInfoQuorumSuccessDht, - WorkflowInfoSource, + SentWorkflowInfo, StatusChangedAutonat, WorkflowInfoQuorumFailureDht, + WorkflowInfoQuorumSuccessDht, WorkflowInfoSource, }; pub(crate) use receipt::ReceiptNotification; diff --git a/homestar-runtime/src/event_handler/notification/network.rs b/homestar-runtime/src/event_handler/notification/network.rs index 63e3eeae7..1a9323052 100644 --- a/homestar-runtime/src/event_handler/notification/network.rs +++ b/homestar-runtime/src/event_handler/notification/network.rs @@ -3,18 +3,19 @@ //! [swarm]: libp2p::swarm::Swarm use anyhow::anyhow; - use homestar_invocation::ipld::DagJson; use libipld::{serde::from_ipld, Ipld}; use schemars::JsonSchema; use std::{collections::BTreeMap, fmt}; +pub(crate) mod autonat; pub(crate) mod connection; pub(crate) mod dht; pub(crate) mod mdns; pub(crate) mod pubsub; pub(crate) mod rendezvous; pub(crate) mod req_resp; +pub(crate) use autonat::StatusChangedAutonat; pub(crate) use connection::{ ConnectionClosed, ConnectionEstablished, IncomingConnectionError, NewListenAddr, OutgoingConnectionError, @@ -43,12 +44,15 @@ pub enum NetworkNotification { /// Connection closed notification. #[schemars(rename = "connection_closed")] ConnnectionClosed(ConnectionClosed), - /// Outgoing conenction error notification. + /// Outgoing connection error notification. #[schemars(rename = "outgoing_connection_error")] OutgoingConnectionError(OutgoingConnectionError), - /// Incoming conenction error notification. + /// Incoming connection error notification. #[schemars(rename = "incoming_connection_error")] IncomingConnectionError(IncomingConnectionError), + /// Autonat status changed notification. + #[schemars(rename = "status_changed_autonat")] + StatusChangedAutonat(StatusChangedAutonat), /// mDNS discovered notification. #[schemars(rename = "discovered_mdns")] DiscoveredMdns(DiscoveredMdns), @@ -120,6 +124,7 @@ impl fmt::Display for NetworkNotification { NetworkNotification::IncomingConnectionError(_) => { write!(f, "incoming_connection_error") } + NetworkNotification::StatusChangedAutonat(_) => write!(f, "status_changed_autonat"), NetworkNotification::DiscoveredMdns(_) => write!(f, "discovered_mdns"), NetworkNotification::DiscoveredRendezvous(_) => write!(f, "discovered_rendezvous"), NetworkNotification::RegisteredRendezvous(_) => write!(f, "registered_rendezvous"), @@ -180,6 +185,10 @@ impl From for Ipld { "incoming_connection_error".into(), n.into(), )])), + NetworkNotification::StatusChangedAutonat(n) => Ipld::Map(BTreeMap::from([( + "status_changed_autonat".into(), + n.into(), + )])), NetworkNotification::DiscoveredMdns(n) => { Ipld::Map(BTreeMap::from([("discovered_mdns".into(), n.into())])) } @@ -267,6 +276,9 @@ impl TryFrom for NetworkNotification { "incoming_connection_error" => Ok(NetworkNotification::IncomingConnectionError( IncomingConnectionError::try_from(val.to_owned())?, )), + "status_changed_autonat" => Ok(NetworkNotification::StatusChangedAutonat( + StatusChangedAutonat::try_from(val.to_owned())?, + )), "discovered_mdns" => Ok(NetworkNotification::DiscoveredMdns( DiscoveredMdns::try_from(val.to_owned())?, )), @@ -333,10 +345,12 @@ impl TryFrom for NetworkNotification { #[cfg(test)] mod test { use super::*; + use crate::libp2p::nat_status::NatStatusExt; use faststr::FastStr; use homestar_invocation::test_utils::cid::generate_cid; use libipld::Cid; use libp2p::{ + autonat::NatStatus, swarm::{DialError, ListenError}, Multiaddr, PeerId, }; @@ -350,6 +364,7 @@ mod test { cid: Cid, connected_peer_count: usize, name: FastStr, + nat_status: NatStatus, num_tasks: u32, peer_id: PeerId, peers: Vec, @@ -371,6 +386,7 @@ mod test { cid: generate_cid(&mut thread_rng()), connected_peer_count: 1, name: FastStr::new("Strong Bad"), + nat_status: NatStatus::Public(Multiaddr::from_str("/ip4/127.0.0.1/tcp/7002").unwrap()), num_tasks: 1, peer_id: PeerId::random(), peers: vec![PeerId::random(), PeerId::random()], @@ -411,6 +427,7 @@ mod test { cid, connected_peer_count, name, + nat_status, num_tasks, peer_id, peers, @@ -428,6 +445,7 @@ mod test { let outgoing_connection_error = OutgoingConnectionError::new(Some(peer_id), DialError::NoAddresses); let incoming_connection_error = IncomingConnectionError::new(ListenError::Aborted); + let status_changed_autonat = StatusChangedAutonat::new(nat_status); let discovered_mdns = DiscoveredMdns::new(peers_map); let discovered_rendezvous = DiscoveredRendezvous::new(peer_id, peers_map_vec_addr); let registered_rendezvous = RegisteredRendezvous::new(peer_id); @@ -506,6 +524,10 @@ mod test { incoming_connection_error.timestamp().to_owned(), NetworkNotification::IncomingConnectionError(incoming_connection_error), ), + ( + status_changed_autonat.timestamp().to_owned(), + NetworkNotification::StatusChangedAutonat(status_changed_autonat), + ), ( discovered_mdns.timestamp().to_owned(), NetworkNotification::DiscoveredMdns(discovered_mdns), @@ -584,6 +606,7 @@ mod test { cid, connected_peer_count, name, + nat_status, num_tasks, peer_id, peers, @@ -623,6 +646,18 @@ mod test { assert_eq!(n.timestamp(), timestamp); assert_eq!(n.error().to_string(), ListenError::Aborted.to_string()); } + NetworkNotification::StatusChangedAutonat(n) => { + let (status, address) = nat_status.to_tuple(); + + assert_eq!(n.timestamp(), timestamp); + assert_eq!(n.status(), &status); + assert_eq!( + n.address() + .as_ref() + .map(|a| Multiaddr::from_str(&a).unwrap()), + address + ); + } NetworkNotification::DiscoveredMdns(n) => { assert_eq!(n.timestamp(), timestamp); diff --git a/homestar-runtime/src/event_handler/notification/network/autonat.rs b/homestar-runtime/src/event_handler/notification/network/autonat.rs new file mode 100644 index 000000000..a037c853d --- /dev/null +++ b/homestar-runtime/src/event_handler/notification/network/autonat.rs @@ -0,0 +1,89 @@ +//! Notification types for [swarm] autonat events. +//! +//! [swarm]: libp2p::swarm::Swarm + +use crate::libp2p::nat_status::NatStatusExt; +use anyhow::anyhow; +use chrono::prelude::Utc; +use derive_getters::Getters; +use homestar_invocation::ipld::DagJson; +use libipld::{serde::from_ipld, Ipld}; +use libp2p::autonat::NatStatus; +use schemars::JsonSchema; +use std::collections::BTreeMap; + +const ADDRESS_KEY: &str = "address"; +const STATUS_KEY: &str = "status"; +const TIMESTAMP_KEY: &str = "timestamp"; + +#[derive(Debug, Clone, Getters, JsonSchema)] +#[schemars(rename = "status_changed_autonat")] +pub struct StatusChangedAutonat { + timestamp: i64, + status: String, + address: Option, +} + +impl StatusChangedAutonat { + pub(crate) fn new(status: NatStatus) -> StatusChangedAutonat { + let (status, address) = status.to_tuple(); + + StatusChangedAutonat { + timestamp: Utc::now().timestamp_millis(), + status: status.to_string(), + address: address.map(|a| a.to_string()), + } + } +} + +impl DagJson for StatusChangedAutonat {} + +impl From for Ipld { + fn from(notification: StatusChangedAutonat) -> Self { + Ipld::Map(BTreeMap::from([ + (TIMESTAMP_KEY.into(), notification.timestamp.into()), + (STATUS_KEY.into(), notification.status.into()), + ( + ADDRESS_KEY.into(), + notification + .address + .map(|peer_id| peer_id.into()) + .unwrap_or(Ipld::Null), + ), + ])) + } +} + +impl TryFrom for StatusChangedAutonat { + type Error = anyhow::Error; + + fn try_from(ipld: Ipld) -> Result { + let map = from_ipld::>(ipld)?; + + let timestamp = from_ipld( + map.get(TIMESTAMP_KEY) + .ok_or_else(|| anyhow!("missing {TIMESTAMP_KEY}"))? + .to_owned(), + )?; + + let status = from_ipld( + map.get(STATUS_KEY) + .ok_or_else(|| anyhow!("missing {STATUS_KEY}"))? + .to_owned(), + )?; + + let address = map + .get(ADDRESS_KEY) + .and_then(|ipld| match ipld { + Ipld::Null => None, + ipld => Some(ipld), + }) + .and_then(|ipld| from_ipld(ipld.to_owned()).ok()); + + Ok(StatusChangedAutonat { + timestamp, + status, + address, + }) + } +} diff --git a/homestar-runtime/src/event_handler/swarm_event.rs b/homestar-runtime/src/event_handler/swarm_event.rs index cedcce13a..b9c65f24a 100644 --- a/homestar-runtime/src/event_handler/swarm_event.rs +++ b/homestar-runtime/src/event_handler/swarm_event.rs @@ -27,6 +27,7 @@ use libipld::Cid; #[cfg(feature = "websocket-notify")] use libp2p::Multiaddr; use libp2p::{ + autonat::{self, NatStatus}, gossipsub, identify, kad, kad::{AddProviderOk, BootstrapOk, GetProvidersOk, GetRecordOk, PutRecordOk, QueryResult}, mdns, @@ -106,6 +107,106 @@ async fn handle_swarm_event( event_handler: &mut EventHandler, ) { match event { + SwarmEvent::Behaviour(ComposedEvent::Autonat(autonat_event)) => { + match autonat_event { + autonat::Event::InboundProbe(event) => match event { + autonat::InboundProbeEvent::Request { + peer, addresses, .. + } => { + debug!( + subject = "libp2p.autonat.inbound_probe", + category = "handle_swarm_event", + peer_id = peer.to_string(), + addresses = ?addresses, + "received a probe request", + ); + } + autonat::InboundProbeEvent::Response { peer, address, .. } => { + debug!( + subject = "libp2p.autonat.inbound_probe", + category = "handle_swarm_event", + peer_id = peer.to_string(), + address = address.to_string(), + "successfully probed an external address for a peer", + ); + } + autonat::InboundProbeEvent::Error { peer, error, .. } => { + debug!( + subject = "libp2p.autonat.inbound_probe", + category = "handle_swarm_event", + peer_id = peer.to_string(), + error = ?error, + "unable to probe a peer", + ); + } + }, + autonat::Event::OutboundProbe(event) => match event { + autonat::OutboundProbeEvent::Request { peer, .. } => { + debug!( + subject = "libp2p.autonat.outbound_probe", + category = "handle_swarm_event", + peer_id = peer.to_string(), + "requested a probe from a peer", + ); + } + autonat::OutboundProbeEvent::Response { peer, address, .. } => { + debug!( + subject = "libp2p.autonat.outbound_probe", + category = "handle_swarm_event", + peer_id = peer.to_string(), + address = address.to_string(), + "peer successfully probed an external address", + ); + } + autonat::OutboundProbeEvent::Error { peer, error, .. } => { + debug!( + subject = "libp2p.autonat.outbound_probe", + category = "handle_swarm_event", + peer_id = peer.map(|p| p.to_string()).unwrap_or("".to_string()), + error = ?error, + "requested probe failed", + ); + } + }, + autonat::Event::StatusChanged { old, new } => { + match &new { + NatStatus::Public(address) => { + event_handler.swarm.add_external_address(address.clone()); + + info!( + subject = "libp2p.autonat.status_change", + category = "handle_swarm_event", + address = address.to_string(), + "confirmed a public address", + ); + } + _ => { + if let NatStatus::Public(address) = old { + // Announce addresses are configured and should not be removed + if !event_handler.announce_addresses.contains(&address) { + event_handler.swarm.remove_external_address(&address); + + info!( + subject = "libp2p.autonat.status_change", + category = "handle_swarm_event", + address = address.to_string(), + "removed an address that is no longer public", + ); + } + } + } + } + + #[cfg(feature = "websocket-notify")] + notification::emit_network_event( + event_handler.ws_evt_sender(), + NetworkNotification::StatusChangedAutonat( + notification::StatusChangedAutonat::new(new), + ), + ); + } + } + } SwarmEvent::Behaviour(ComposedEvent::Identify(identify_event)) => { match identify_event { identify::Event::Error { peer_id, error } => { @@ -141,7 +242,7 @@ async fn handle_swarm_event( let num_addresses = event_handler.swarm.external_addresses().count(); - // Add observed address as an external address if we are identifying ourselves + // Probe observed address as an external address if we are identifying ourselves if &peer_id == event_handler.swarm.local_peer_id() && num_addresses < event_handler.external_address_limit as usize { @@ -153,9 +254,15 @@ async fn handle_swarm_event( _ => None, }) .all(|proto| !proto.is_private()) - // Identify observed a potentially valid external address that we weren't aware of. - // Add it to the addresses we announce to other peers. - .then(|| event_handler.swarm.add_external_address(info.observed_addr)); + // We have observed a potentially valid external address that we weren't aware of. + // Probe it with AutoNAT to confirm it and on confirmation add it to addresses we announce to peers. + .then(|| { + event_handler + .swarm + .behaviour_mut() + .autonat + .probe_address(info.observed_addr) + }); } let behavior = event_handler.swarm.behaviour_mut(); diff --git a/homestar-runtime/src/lib.rs b/homestar-runtime/src/lib.rs index f23f0d6ff..334764a72 100644 --- a/homestar-runtime/src/lib.rs +++ b/homestar-runtime/src/lib.rs @@ -84,7 +84,7 @@ pub(crate) use scheduler::TaskScheduler; #[cfg(feature = "ipfs")] pub use settings::IpfsBuilder; pub use settings::{ - DatabaseBuilder, Dht, ExistingKeyPath, KeyType, Libp2p, Mdns, MetricsBuilder, + Autonat, DatabaseBuilder, Dht, ExistingKeyPath, KeyType, Libp2p, Mdns, MetricsBuilder, MonitoringBuilder, NetworkBuilder, NodeBuilder, PubkeyConfig, Pubsub, RNGSeed, Rendezvous, RpcBuilder, Settings, SettingsBuilder, WebserverBuilder, }; diff --git a/homestar-runtime/src/libp2p/mod.rs b/homestar-runtime/src/libp2p/mod.rs index 3adffaa43..1acdebe0a 100644 --- a/homestar-runtime/src/libp2p/mod.rs +++ b/homestar-runtime/src/libp2p/mod.rs @@ -1,3 +1,4 @@ //! libp2p utilities. pub(crate) mod multiaddr; +pub(crate) mod nat_status; diff --git a/homestar-runtime/src/libp2p/nat_status.rs b/homestar-runtime/src/libp2p/nat_status.rs new file mode 100644 index 000000000..0e8bdbbfd --- /dev/null +++ b/homestar-runtime/src/libp2p/nat_status.rs @@ -0,0 +1,37 @@ +/// NatStatus extension methods. +use libp2p::{autonat::NatStatus, Multiaddr}; + +/// [NatStatus] extension trait. +pub(crate) trait NatStatusExt { + fn to_tuple(&self) -> (String, Option); +} + +impl NatStatusExt for NatStatus { + fn to_tuple(&self) -> (String, Option) { + match &self { + NatStatus::Public(address) => ("Public".to_string(), Some(address.to_owned())), + NatStatus::Private => ("Private".to_string(), None), + NatStatus::Unknown => ("Unknown".to_string(), None), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn converts_nat_status_to_tuple() { + let address: Multiaddr = "/ip4/127.0.0.1/tcp/7001".parse().unwrap(); + let public_status = NatStatus::Public(address.clone()); + let private_status = NatStatus::Private; + let unknown_status = NatStatus::Unknown; + + assert_eq!( + public_status.to_tuple(), + ("Public".to_string(), Some(address)) + ); + assert_eq!(private_status.to_tuple(), ("Private".to_string(), None)); + assert_eq!(unknown_status.to_tuple(), ("Unknown".to_string(), None)); + } +} diff --git a/homestar-runtime/src/network/swarm.rs b/homestar-runtime/src/network/swarm.rs index 13292f772..564a81ee6 100644 --- a/homestar-runtime/src/network/swarm.rs +++ b/homestar-runtime/src/network/swarm.rs @@ -13,6 +13,7 @@ use enum_assoc::Assoc; use faststr::FastStr; use futures::future::Either; use libp2p::{ + autonat, core::{ muxing::StreamMuxerBox, transport::{self, OptionalTransport}, @@ -62,6 +63,16 @@ pub(crate) async fn new(settings: &settings::Network) -> Result), /// [kad::Event] event. @@ -303,6 +316,8 @@ pub(crate) enum TopicMessage { #[derive(NetworkBehaviour)] #[behaviour(to_swarm = "ComposedEvent")] pub(crate) struct ComposedBehaviour { + /// [autonat::Behaviour] behaviour. + pub(crate) autonat: autonat::Behaviour, /// [gossipsub::Behaviour] behaviour. pub(crate) gossipsub: Toggle, /// In-memory [kademlia: kad::Behaviour] behaviour. @@ -361,6 +376,12 @@ impl ComposedBehaviour { } } +impl From for ComposedEvent { + fn from(event: autonat::Event) -> Self { + ComposedEvent::Autonat(event) + } +} + impl From for ComposedEvent { fn from(event: gossipsub::Event) -> Self { ComposedEvent::Gossipsub(Box::new(event)) diff --git a/homestar-runtime/src/settings.rs b/homestar-runtime/src/settings.rs index 926d0acad..eb044c617 100644 --- a/homestar-runtime/src/settings.rs +++ b/homestar-runtime/src/settings.rs @@ -16,7 +16,7 @@ use std::{ mod libp2p_config; mod pubkey_config; -pub use libp2p_config::{Dht, Libp2p, Mdns, Pubsub, Rendezvous}; +pub use libp2p_config::{Autonat, Dht, Libp2p, Mdns, Pubsub, Rendezvous}; pub use pubkey_config::{ExistingKeyPath, KeyType, PubkeyConfig, RNGSeed}; #[cfg(target_os = "windows")] diff --git a/homestar-runtime/src/settings/libp2p_config.rs b/homestar-runtime/src/settings/libp2p_config.rs index 932fa343b..230d00b0d 100644 --- a/homestar-runtime/src/settings/libp2p_config.rs +++ b/homestar-runtime/src/settings/libp2p_config.rs @@ -16,6 +16,8 @@ pub struct Libp2p { /// network. #[serde_as(as = "Vec")] pub(crate) announce_addresses: Vec, + /// Autonat DHT Settings + pub(crate) autonat: Autonat, /// Kademlia DHT Settings pub(crate) dht: Dht, #[serde_as(as = "DurationSeconds")] @@ -51,11 +53,23 @@ pub struct Libp2p { pub(crate) bootstrap_interval: Duration, } -/// DHT settings. +/// Autonat settings. +#[serde_as] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub(crate) struct Quic { - /// Enable Quic transport. - pub(crate) enable: bool, +pub struct Autonat { + /// Initial delay before starting the fist probe. + #[serde_as(as = "DurationSeconds")] + pub(crate) boot_delay: Duration, + /// Probe interval when max confidence has not been achieved + #[serde_as(as = "DurationSeconds")] + pub(crate) retry_interval: Duration, + /// Throttle period before re-using a peer as server for a dial-request. + #[serde_as(as = "DurationSeconds")] + pub(crate) throttle_server_period: Duration, + /// Use public IP addresses only. A server will only fulfill probe requests + /// for public addresses, and a client will only request probes + /// from servers at public addresses. + pub(crate) only_public_ips: bool, } /// DHT settings. @@ -127,6 +141,13 @@ pub struct Pubsub { pub(crate) mesh_outbound_min: usize, } +/// Quic settings. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct Quic { + /// Enable Quic transport. + pub(crate) enable: bool, +} + /// Rendezvous settings. #[serde_as] #[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -149,6 +170,7 @@ impl Default for Libp2p { fn default() -> Self { Self { announce_addresses: Vec::new(), + autonat: Autonat::default(), dht: Dht::default(), // https://github.com/libp2p/rust-libp2p/pull/4967 // https://github.com/libp2p/rust-libp2p/pull/4887 @@ -169,6 +191,11 @@ impl Default for Libp2p { } impl Libp2p { + /// Autonat settings getter. + pub(crate) fn autonat(&self) -> &Autonat { + &self.autonat + } + /// DHT settings getter. pub(crate) fn dht(&self) -> &Dht { &self.dht @@ -180,6 +207,17 @@ impl Libp2p { } } +impl Default for Autonat { + fn default() -> Self { + Self { + boot_delay: Duration::from_secs(15), + retry_interval: Duration::from_secs(90), + throttle_server_period: Duration::from_secs(90), + only_public_ips: true, + } + } +} + impl Default for Dht { fn default() -> Self { Self { @@ -193,12 +231,6 @@ impl Default for Dht { } } -impl Default for Quic { - fn default() -> Self { - Self { enable: true } - } -} - impl Default for Mdns { fn default() -> Self { Self { @@ -225,6 +257,12 @@ impl Default for Pubsub { } } +impl Default for Quic { + fn default() -> Self { + Self { enable: true } + } +} + impl Default for Rendezvous { fn default() -> Self { Self { diff --git a/homestar-runtime/tests/network.rs b/homestar-runtime/tests/network.rs index 54aaef7c7..1a0a6f4a3 100644 --- a/homestar-runtime/tests/network.rs +++ b/homestar-runtime/tests/network.rs @@ -13,6 +13,8 @@ use std::{ process::{Command, Stdio}, }; +#[cfg(feature = "websocket-notify")] +mod autonat; #[cfg(feature = "websocket-notify")] mod connection; #[cfg(all(feature = "websocket-notify", feature = "test-utils"))] diff --git a/homestar-runtime/tests/network/autonat.rs b/homestar-runtime/tests/network/autonat.rs new file mode 100644 index 000000000..ec2b98017 --- /dev/null +++ b/homestar-runtime/tests/network/autonat.rs @@ -0,0 +1,179 @@ +use crate::{ + make_config, + utils::{ + check_for_line_with, kill_homestar, listen_addr, multiaddr, retrieve_output, + subscribe_network_events, wait_for_socket_connection, ChildGuard, ProcInfo, + TimeoutFutureExt, BIN_NAME, ED25519MULTIHASH, SECP256K1MULTIHASH, + }, +}; +use anyhow::Result; +use once_cell::sync::Lazy; +use std::{ + path::PathBuf, + process::{Command, Stdio}, + time::Duration, +}; + +static BIN: Lazy = Lazy::new(|| assert_cmd::cargo::cargo_bin(BIN_NAME)); + +#[test] +#[serial_test::parallel] +fn test_autonat_confirms_address_integration() -> Result<()> { + let proc_info1 = ProcInfo::new().unwrap(); + let proc_info2 = ProcInfo::new().unwrap(); + + let rpc_port1 = proc_info1.rpc_port; + let rpc_port2 = proc_info2.rpc_port; + let metrics_port1 = proc_info1.metrics_port; + let metrics_port2 = proc_info2.metrics_port; + let ws_port1 = proc_info1.ws_port; + let ws_port2 = proc_info2.ws_port; + let listen_addr1 = listen_addr(proc_info1.listen_port); + let listen_addr2 = listen_addr(proc_info2.listen_port); + let node_addra = multiaddr(proc_info1.listen_port, ED25519MULTIHASH); + + let toml = format!( + r#" + [node] + [node.network.keypair_config] + existing = {{ key_type = "ed25519", path = "./fixtures/__testkey_ed25519.pem" }} + [node.network.libp2p] + listen_address = "{listen_addr1}" + [node.network.libp2p.autonat] + boot_delay = 1 + retry_interval = 3 + throttle_server_period = 2 + only_public_ips = false + [node.network.libp2p.mdns] + enable = false + [node.network.libp2p.rendezvous] + enable_client = false + [node.network.metrics] + port = {metrics_port1} + [node.network.rpc] + port = {rpc_port1} + [node.network.webserver] + port = {ws_port1} + "# + ); + let config1 = make_config!(toml); + + let homestar_proc1 = Command::new(BIN.as_os_str()) + .env("RUST_BACKTRACE", "0") + .env( + "RUST_LOG", + "homestar=debug,homestar_runtime=debug,libp2p=debug,libp2p_gossipsub::behaviour=debug", + ) + .arg("start") + .arg("-c") + .arg(config1.filename()) + .arg("--db") + .arg(&proc_info1.db_path) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let proc_guard1 = ChildGuard::new(homestar_proc1); + + if wait_for_socket_connection(ws_port1, 1000).is_err() { + panic!("Homestar server/runtime failed to start in time"); + } + + tokio_test::block_on(async { + let toml2 = format!( + r#" + [node] + [node.network.keypair_config] + existing = {{ key_type = "secp256k1", path = "./fixtures/__testkey_secp256k1.der" }} + [node.network.libp2p] + listen_address = "{listen_addr2}" + node_addresses = ["{node_addra}"] + [node.network.libp2p.autonat] + boot_delay = 1 + retry_interval = 3 + throttle_server_period = 2 + only_public_ips = false + [node.network.libp2p.mdns] + enable = false + [node.network.metrics] + port = {metrics_port2} + [node.network.libp2p.rendezvous] + enable_client = false + [node.network.rpc] + port = {rpc_port2} + [node.network.webserver] + port = {ws_port2} + "# + ); + let config2 = make_config!(toml2); + + let homestar_proc2 = Command::new(BIN.as_os_str()) + .env("RUST_BACKTRACE", "0") + .env( + "RUST_LOG", + "homestar=debug,homestar_runtime=debug,libp2p=debug,libp2p_gossipsub::behaviour=debug", + ) + .arg("start") + .arg("-c") + .arg(config2.filename()) + .arg("--db") + .arg(&proc_info2.db_path) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let proc_guard2 = ChildGuard::new(homestar_proc2); + + if wait_for_socket_connection(ws_port2, 1000).is_err() { + panic!("Homestar server/runtime failed to start in time"); + } + + let mut net_events = subscribe_network_events(ws_port2).await; + let sub = net_events.sub(); + + // Poll for status changed autonat message + loop { + if let Ok(msg) = sub.next().with_timeout(Duration::from_secs(30)).await { + let json: serde_json::Value = + serde_json::from_slice(&msg.unwrap().unwrap()).unwrap(); + + if json["status_changed_autonat"].is_object() + && json["status_changed_autonat"]["status"] == "Public" + { + break; + } + } else { + panic!("Node two did not receive a NAT public status message in time.") + } + } + + // Kill proceses. + let dead_proc1 = kill_homestar(proc_guard1.take(), None); + let dead_proc2 = kill_homestar(proc_guard2.take(), None); + + // Retrieve logs. + let stdout1 = retrieve_output(dead_proc1); + let stdout2 = retrieve_output(dead_proc2); + + // Check node one successfully probed an address for node two + let one_confirmed_address = check_for_line_with( + stdout1, + vec![ + "successfully probed an external address for a peer", + SECP256K1MULTIHASH, + ], + ); + + // Check node two received a probe confirmation from node one + let two_received_address_confirmation = check_for_line_with( + stdout2, + vec![ + "peer successfully probed an external address", + ED25519MULTIHASH, + ], + ); + + assert!(one_confirmed_address); + assert!(two_received_address_confirmation); + }); + + Ok(()) +}