diff --git a/protocols/autonat/CHANGELOG.md b/protocols/autonat/CHANGELOG.md index f76cf47e..ab74a16c 100644 --- a/protocols/autonat/CHANGELOG.md +++ b/protocols/autonat/CHANGELOG.md @@ -6,6 +6,11 @@ - Update to `libp2p-request-response` `v0.18.0`. +- Add `Config::only_global_ips` to skip peers that are observed at a private IP-address + (see [PR 2618]). + +[PR 2618]: https://github.com/libp2p/rust-libp2p/pull/2618 + # 0.3.0 - Update to `libp2p-swarm` `v0.35.0`. diff --git a/protocols/autonat/src/behaviour.rs b/protocols/autonat/src/behaviour.rs index ce5b1a92..86c65bb3 100644 --- a/protocols/autonat/src/behaviour.rs +++ b/protocols/autonat/src/behaviour.rs @@ -30,6 +30,7 @@ use futures_timer::Delay; use instant::Instant; use libp2p_core::{ connection::{ConnectionId, ListenerId}, + multiaddr::Protocol, ConnectedPoint, Endpoint, Multiaddr, PeerId, }; use libp2p_request_response::{ @@ -77,6 +78,11 @@ pub struct Config { pub throttle_clients_peer_max: usize, /// Period for throttling clients requests. pub throttle_clients_period: Duration, + /// As a server reject probes for clients that are observed at a non-global ip address. + /// Correspondingly as a client only pick peers as server that are not observed at a + /// private ip address. Note that this does not apply for servers that are added via + /// [`Behaviour::add_server`]. + pub only_global_ips: bool, } impl Default for Config { @@ -93,6 +99,7 @@ impl Default for Config { throttle_clients_global_max: 30, throttle_clients_peer_max: 3, throttle_clients_period: Duration::from_secs(1), + only_global_ips: true, } } } @@ -188,7 +195,8 @@ pub struct Behaviour { ongoing_outbound: HashMap, // Connected peers with the observed address of each connection. - // If the endpoint of a connection is relayed, the observed address is `None`. + // If the endpoint of a connection is relayed or not global (in case of Config::only_global_ips), + // the observed address is `None`. connected: HashMap>>, // Used servers in recent outbound probes that are throttled through Config::throttle_server_period. @@ -313,12 +321,14 @@ impl NetworkBehaviour for Behaviour { other_established, ); let connections = self.connected.entry(*peer).or_default(); - let addr = if endpoint.is_relayed() { - None - } else { - Some(endpoint.get_remote_address().clone()) - }; - connections.insert(*conn, addr); + let addr = endpoint.get_remote_address(); + let observed_addr = + if !endpoint.is_relayed() && (!self.config.only_global_ips || addr.is_global_ip()) { + Some(addr.clone()) + } else { + None + }; + connections.insert(*conn, observed_addr); match endpoint { ConnectedPoint::Dialer { @@ -386,12 +396,14 @@ impl NetworkBehaviour for Behaviour { return; } let connections = self.connected.get_mut(peer).expect("Peer is connected."); - let addr = if new.is_relayed() { - None - } else { - Some(new.get_remote_address().clone()) - }; - connections.insert(*conn, addr); + let addr = new.get_remote_address(); + let observed_addr = + if !new.is_relayed() && (!self.config.only_global_ips || addr.is_global_ip()) { + Some(addr.clone()) + } else { + None + }; + connections.insert(*conn, observed_addr); } fn inject_new_listen_addr(&mut self, id: ListenerId, addr: &Multiaddr) { @@ -512,3 +524,119 @@ trait HandleInnerEvent { event: RequestResponseEvent, ) -> (VecDeque, Option); } + +trait GlobalIp { + fn is_global_ip(&self) -> bool; +} + +impl GlobalIp for Multiaddr { + fn is_global_ip(&self) -> bool { + match self.iter().next() { + Some(Protocol::Ip4(a)) => a.is_global_ip(), + Some(Protocol::Ip6(a)) => a.is_global_ip(), + _ => false, + } + } +} + +impl GlobalIp for std::net::Ipv4Addr { + // NOTE: The below logic is copied from `std::net::Ipv4Addr::is_global`, which is at the time of + // writing behind the unstable `ip` feature. + // See https://github.com/rust-lang/rust/issues/27709 for more info. + fn is_global_ip(&self) -> bool { + // Check if this address is 192.0.0.9 or 192.0.0.10. These addresses are the only two + // globally routable addresses in the 192.0.0.0/24 range. + if u32::from_be_bytes(self.octets()) == 0xc0000009 + || u32::from_be_bytes(self.octets()) == 0xc000000a + { + return true; + } + + // Copied from the unstable method `std::net::Ipv4Addr::is_shared`. + fn is_shared(addr: &std::net::Ipv4Addr) -> bool { + addr.octets()[0] == 100 && (addr.octets()[1] & 0b1100_0000 == 0b0100_0000) + } + + // Copied from the unstable method `std::net::Ipv4Addr::is_reserved`. + // + // **Warning**: As IANA assigns new addresses, this logic will be + // updated. This may result in non-reserved addresses being + // treated as reserved in code that relies on an outdated version + // of this method. + fn is_reserved(addr: &std::net::Ipv4Addr) -> bool { + addr.octets()[0] & 240 == 240 && !addr.is_broadcast() + } + + // Copied from the unstable method `std::net::Ipv4Addr::is_benchmarking`. + fn is_benchmarking(addr: &std::net::Ipv4Addr) -> bool { + addr.octets()[0] == 198 && (addr.octets()[1] & 0xfe) == 18 + } + + !self.is_private() + && !self.is_loopback() + && !self.is_link_local() + && !self.is_broadcast() + && !self.is_documentation() + && !is_shared(self) + // addresses reserved for future protocols (`192.0.0.0/24`) + && !(self.octets()[0] == 192 && self.octets()[1] == 0 && self.octets()[2] == 0) + && !is_reserved(self) + && !is_benchmarking(self) + // Make sure the address is not in 0.0.0.0/8 + && self.octets()[0] != 0 + } +} + +impl GlobalIp for std::net::Ipv6Addr { + // NOTE: The below logic is copied from `std::net::Ipv6Addr::is_global`, which is at the time of + // writing behind the unstable `ip` feature. + // See https://github.com/rust-lang/rust/issues/27709 for more info. + // + // Note that contrary to `Ipv4Addr::is_global_ip` this currently checks for global scope + // rather than global reachability. + fn is_global_ip(&self) -> bool { + // Copied from the unstable method `std::net::Ipv6Addr::is_unicast`. + fn is_unicast(addr: &std::net::Ipv6Addr) -> bool { + !addr.is_multicast() + } + // Copied from the unstable method `std::net::Ipv6Addr::is_unicast_link_local`. + fn is_unicast_link_local(addr: &std::net::Ipv6Addr) -> bool { + (addr.segments()[0] & 0xffc0) == 0xfe80 + } + // Copied from the unstable method `std::net::Ipv6Addr::is_unique_local`. + fn is_unique_local(addr: &std::net::Ipv6Addr) -> bool { + (addr.segments()[0] & 0xfe00) == 0xfc00 + } + // Copied from the unstable method `std::net::Ipv6Addr::is_documentation`. + fn is_documentation(addr: &std::net::Ipv6Addr) -> bool { + (addr.segments()[0] == 0x2001) && (addr.segments()[1] == 0xdb8) + } + + // Copied from the unstable method `std::net::Ipv6Addr::is_unicast_global`. + fn is_unicast_global(addr: &std::net::Ipv6Addr) -> bool { + is_unicast(addr) + && !addr.is_loopback() + && !is_unicast_link_local(addr) + && !is_unique_local(addr) + && !addr.is_unspecified() + && !is_documentation(addr) + } + + // Variation of unstable method [`std::net::Ipv6Addr::multicast_scope`] that instead of the + // `Ipv6MulticastScope` just returns if the scope is global or not. + // Equivalent to `Ipv6Addr::multicast_scope(..).map(|scope| matches!(scope, Ipv6MulticastScope::Global))`. + fn is_multicast_scope_global(addr: &std::net::Ipv6Addr) -> Option { + match addr.segments()[0] & 0x000f { + 14 => Some(true), // Global multicast scope. + 1..=5 | 8 => Some(false), // Local multicast scope. + _ => None, // Unknown multicast scope. + } + } + + match is_multicast_scope_global(self) { + Some(true) => true, + None => is_unicast_global(self), + _ => false, + } + } +} diff --git a/protocols/autonat/src/behaviour/as_client.rs b/protocols/autonat/src/behaviour/as_client.rs index de6a00d5..8d0097de 100644 --- a/protocols/autonat/src/behaviour/as_client.rs +++ b/protocols/autonat/src/behaviour/as_client.rs @@ -262,7 +262,12 @@ impl<'a> AsClient<'a> { let mut servers: Vec<&PeerId> = self.servers.iter().collect(); if self.config.use_connected { - servers.extend(self.connected.iter().map(|(id, _)| id)); + servers.extend(self.connected.iter().filter_map(|(id, addrs)| { + // Filter servers for which no qualified address is known. + // This is the case if the connection is relayed or the address is + // not global (in case of Config::only_global_ips). + addrs.values().any(|a| a.is_some()).then(|| id) + })); } servers.retain(|s| !self.throttled_servers.iter().any(|(id, _)| s == &id)); diff --git a/protocols/autonat/src/behaviour/as_server.rs b/protocols/autonat/src/behaviour/as_server.rs index 24ffb443..9b045c02 100644 --- a/protocols/autonat/src/behaviour/as_server.rs +++ b/protocols/autonat/src/behaviour/as_server.rs @@ -295,8 +295,8 @@ impl<'a> AsServer<'a> { .values() .find_map(|a| a.as_ref()) .ok_or_else(|| { - let status_text = "no dial-request over relayed connections".to_string(); - (status_text, ResponseError::DialError) + let status_text = "refusing to dial peer with blocked observed address".to_string(); + (status_text, ResponseError::DialRefused) })?; let mut addrs = Self::filter_valid_addrs(sender, request.addresses, observed_addr); @@ -304,7 +304,7 @@ impl<'a> AsServer<'a> { if addrs.is_empty() { let status_text = "no dialable addresses".to_string(); - return Err((status_text, ResponseError::DialError)); + return Err((status_text, ResponseError::DialRefused)); } Ok(addrs) @@ -316,10 +316,6 @@ impl<'a> AsServer<'a> { demanded: Vec, observed_remote_at: &Multiaddr, ) -> Vec { - // Skip if the observed address is a relay address. - if observed_remote_at.iter().any(|p| p == Protocol::P2pCircuit) { - return Vec::new(); - } let observed_ip = match observed_remote_at .into_iter() .find(|p| matches!(p, Protocol::Ip4(_) | Protocol::Ip6(_))) @@ -417,23 +413,4 @@ mod test { .with(Protocol::P2p(peer_id.into())); assert_eq!(filtered, vec![expected_1, expected_2]); } - - #[test] - fn skip_relayed_addr() { - let peer_id = PeerId::random(); - let observed_ip = random_ip(); - // Observed address is relayed. - let observed_addr = Multiaddr::empty() - .with(observed_ip.clone()) - .with(random_port()) - .with(Protocol::P2p(PeerId::random().into())) - .with(Protocol::P2pCircuit) - .with(Protocol::P2p(peer_id.into())); - let demanded = Multiaddr::empty() - .with(random_ip()) - .with(random_port()) - .with(Protocol::P2p(peer_id.into())); - let filtered = AsServer::filter_valid_addrs(peer_id, vec![demanded], &observed_addr); - assert!(filtered.is_empty()); - } } diff --git a/protocols/autonat/tests/test_client.rs b/protocols/autonat/tests/test_client.rs index cd84b765..72088f6e 100644 --- a/protocols/autonat/tests/test_client.rs +++ b/protocols/autonat/tests/test_client.rs @@ -49,6 +49,7 @@ async fn spawn_server(kill: oneshot::Receiver<()>) -> (PeerId, Multiaddr) { let mut server = init_swarm(Config { boot_delay: Duration::from_secs(60), throttle_clients_peer_max: usize::MAX, + only_global_ips: false, ..Default::default() }) .await; @@ -100,6 +101,7 @@ async fn test_auto_probe() { retry_interval: TEST_RETRY_INTERVAL, refresh_interval: TEST_REFRESH_INTERVAL, confidence_max: MAX_CONFIDENCE, + only_global_ips: false, throttle_server_period: Duration::ZERO, boot_delay: Duration::ZERO, ..Default::default() @@ -237,8 +239,6 @@ async fn test_auto_probe() { assert_eq!(client.behaviour().confidence(), 0); assert!(client.behaviour().nat_status().is_public()); assert!(client.behaviour().public_address().is_some()); - - drop(_handle); }; run_test_with_timeout(test).await; @@ -251,6 +251,7 @@ async fn test_confidence() { retry_interval: TEST_RETRY_INTERVAL, refresh_interval: TEST_REFRESH_INTERVAL, confidence_max: MAX_CONFIDENCE, + only_global_ips: false, throttle_server_period: Duration::ZERO, boot_delay: Duration::from_millis(100), ..Default::default() @@ -332,8 +333,6 @@ async fn test_confidence() { } } } - - drop(_handle); }; run_test_with_timeout(test).await; @@ -346,6 +345,7 @@ async fn test_throttle_server_period() { retry_interval: TEST_RETRY_INTERVAL, refresh_interval: TEST_REFRESH_INTERVAL, confidence_max: MAX_CONFIDENCE, + only_global_ips: false, // Throttle servers so they can not be re-used for dial request. throttle_server_period: Duration::from_secs(1000), boot_delay: Duration::from_millis(100), @@ -393,8 +393,6 @@ async fn test_throttle_server_period() { other => panic!("Unexpected behaviour event: {:?}.", other), } assert_eq!(client.behaviour().confidence(), 0); - - drop(_handle) }; run_test_with_timeout(test).await; @@ -407,6 +405,7 @@ async fn test_use_connected_as_server() { retry_interval: TEST_RETRY_INTERVAL, refresh_interval: TEST_REFRESH_INTERVAL, confidence_max: MAX_CONFIDENCE, + only_global_ips: false, throttle_server_period: Duration::ZERO, boot_delay: Duration::from_millis(100), ..Default::default() @@ -445,8 +444,6 @@ async fn test_use_connected_as_server() { } other => panic!("Unexpected behaviour event: {:?}.", other), } - - drop(_handle); }; run_test_with_timeout(test).await; @@ -461,6 +458,7 @@ async fn test_outbound_failure() { retry_interval: TEST_RETRY_INTERVAL, refresh_interval: TEST_REFRESH_INTERVAL, confidence_max: MAX_CONFIDENCE, + only_global_ips: false, throttle_server_period: Duration::ZERO, boot_delay: Duration::from_millis(100), ..Default::default() @@ -524,3 +522,55 @@ async fn test_outbound_failure() { run_test_with_timeout(test).await; } + +#[async_std::test] +async fn test_global_ips_config() { + let test = async { + let mut client = init_swarm(Config { + retry_interval: TEST_RETRY_INTERVAL, + refresh_interval: TEST_REFRESH_INTERVAL, + confidence_max: MAX_CONFIDENCE, + // Enforce that only peers outside of the local network are used as servers. + only_global_ips: true, + boot_delay: Duration::from_millis(100), + ..Default::default() + }) + .await; + + client + .listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap()) + .unwrap(); + loop { + match client.select_next_some().await { + SwarmEvent::NewListenAddr { .. } => break, + _ => {} + } + } + + let (_handle, rx) = oneshot::channel(); + let (server_id, addr) = spawn_server(rx).await; + + // Dial server instead of adding it via `Behaviour::add_server` because the + // `only_global_ips` restriction does not apply for manually added servers. + client.dial(addr).unwrap(); + loop { + if let SwarmEvent::ConnectionEstablished { peer_id, .. } = + client.select_next_some().await + { + assert_eq!(peer_id, server_id); + break; + } + } + + // Expect that the server is not qualified for dial-back because it is observed + // at a local IP. + match next_event(&mut client).await { + Event::OutboundProbe(OutboundProbeEvent::Error { error, .. }) => { + assert!(matches!(error, OutboundProbeError::NoServer)) + } + other => panic!("Unexpected behaviour event: {:?}.", other), + } + }; + + run_test_with_timeout(test).await; +} diff --git a/protocols/autonat/tests/test_server.rs b/protocols/autonat/tests/test_server.rs index 5043e715..36328319 100644 --- a/protocols/autonat/tests/test_server.rs +++ b/protocols/autonat/tests/test_server.rs @@ -43,7 +43,10 @@ async fn init_swarm(config: Config) -> Swarm { } async fn init_server(config: Option) -> (Swarm, PeerId, Multiaddr) { - let mut config = config.unwrap_or_default(); + let mut config = config.unwrap_or_else(|| Config { + only_global_ips: false, + ..Default::default() + }); // Don't do any outbound probes. config.boot_delay = Duration::from_secs(60); @@ -74,6 +77,7 @@ async fn spawn_client( boot_delay: Duration::from_secs(1), retry_interval: Duration::from_secs(1), throttle_server_period: Duration::ZERO, + only_global_ips: false, ..Default::default() }) .await; @@ -224,8 +228,6 @@ async fn test_dial_back() { } other => panic!("Unexpected behaviour event: {:?}.", other), } - - drop(_handle); }; run_test_with_timeout(test).await; @@ -270,8 +272,6 @@ async fn test_dial_error() { } other => panic!("Unexpected behaviour event: {:?}.", other), } - - drop(_handle); }; run_test_with_timeout(test).await; @@ -283,6 +283,7 @@ async fn test_throttle_global_max() { let (mut server, server_id, server_addr) = init_server(Some(Config { throttle_clients_global_max: 1, throttle_clients_period: Duration::from_secs(60), + only_global_ips: false, ..Default::default() })) .await; @@ -318,8 +319,6 @@ async fn test_throttle_global_max() { other => panic!("Unexpected behaviour event: {:?}.", other), }; } - - drop(_handles); }; run_test_with_timeout(test).await; @@ -331,6 +330,7 @@ async fn test_throttle_peer_max() { let (mut server, server_id, server_addr) = init_server(Some(Config { throttle_clients_peer_max: 1, throttle_clients_period: Duration::from_secs(60), + only_global_ips: false, ..Default::default() })) .await; @@ -369,8 +369,6 @@ async fn test_throttle_peer_max() { } other => panic!("Unexpected behaviour event: {:?}.", other), }; - - drop(_handle); }; run_test_with_timeout(test).await; @@ -382,6 +380,7 @@ async fn test_dial_multiple_addr() { let (mut server, server_id, server_addr) = init_server(Some(Config { throttle_clients_peer_max: 1, throttle_clients_period: Duration::from_secs(60), + only_global_ips: false, ..Default::default() })) .await; @@ -428,3 +427,29 @@ async fn test_dial_multiple_addr() { run_test_with_timeout(test).await; } + +#[async_std::test] +async fn test_global_ips_config() { + let test = async { + let (mut server, server_id, server_addr) = init_server(Some(Config { + // Enforce that only clients outside of the local network are qualified for dial-backs. + only_global_ips: true, + ..Default::default() + })) + .await; + + let (_handle, rx) = oneshot::channel(); + spawn_client(true, false, server_id, server_addr.clone(), rx).await; + + // Expect the probe to be refused as both peers run on the same machine and thus in the same local network. + match next_event(&mut server).await { + Event::InboundProbe(InboundProbeEvent::Error { error, .. }) => assert!(matches!( + error, + InboundProbeError::Response(ResponseError::DialRefused) + )), + other => panic!("Unexpected behaviour event: {:?}.", other), + }; + }; + + run_test_with_timeout(test).await; +}