diff --git a/Cargo.lock b/Cargo.lock index 2d9f3542d93..786ab4fe3f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2704,6 +2704,7 @@ dependencies = [ "libp2p-request-response", "libp2p-swarm", "libp2p-swarm-test", + "network-interface", "quick-protobuf", "quick-protobuf-codec", "rand 0.8.5", @@ -3974,6 +3975,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "network-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433419f898328beca4f2c6c73a1b52540658d92b0a99f0269330457e0fd998d5" +dependencies = [ + "cc", + "libc", + "thiserror", + "winapi", +] + [[package]] name = "nix" version = "0.24.3" diff --git a/protocols/autonat/Cargo.toml b/protocols/autonat/Cargo.toml index 5e975b33795..9f8c524ea26 100644 --- a/protocols/autonat/Cargo.toml +++ b/protocols/autonat/Cargo.toml @@ -31,6 +31,7 @@ rand = "0.8" rand_core = { version = "0.6", optional = true } thiserror = { version = "1.0.52", optional = true } void = { version = "1", optional = true } +network-interface = { version = "2.0.0", optional = true } [dev-dependencies] tokio = { version = "1", features = ["macros", "rt", "sync"]} @@ -43,7 +44,8 @@ libp2p-swarm = { workspace = true, features = ["macros"]} [features] default = ["v1", "v2"] v1 = ["dep:libp2p-request-response"] -v2 = ["dep:bytes", "dep:either", "dep:futures-bounded", "dep:thiserror", "dep:void", "dep:rand_core"] +v2 = ["dep:bytes", "dep:either", "dep:futures-bounded", "dep:thiserror", "dep:void", + "dep:rand_core", "dep:network-interface"] [lints] workspace = true diff --git a/protocols/autonat/src/v2.rs b/protocols/autonat/src/v2.rs index 8cab10a1d14..6d9f36f1d8b 100644 --- a/protocols/autonat/src/v2.rs +++ b/protocols/autonat/src/v2.rs @@ -3,6 +3,7 @@ use libp2p_swarm::StreamProtocol; pub mod client; pub(crate) mod protocol; pub mod server; +mod global_ip; pub(crate) mod generated { #![allow(unreachable_pub)] diff --git a/protocols/autonat/src/v2/global_ip.rs b/protocols/autonat/src/v2/global_ip.rs new file mode 100644 index 00000000000..811c258cec7 --- /dev/null +++ b/protocols/autonat/src/v2/global_ip.rs @@ -0,0 +1,249 @@ +// Copyright 2023 Protocol Labs +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +/// This module contains an implementation of the `is_global` IPv4 address space. +/// +/// Credit for this implementation goes to the Rust standard library team. +/// +/// Unstable tracking issue: [#27709](https://github.com/rust-lang/rust/issues/27709) +pub(crate) mod ipv4_global { + use std::net::Ipv4Addr; + + /// Returns [`true`] if this address is reserved by IANA for future use. [IETF RFC 1112] + /// defines the block of reserved addresses as `240.0.0.0/4`. This range normally includes the + /// broadcast address `255.255.255.255`, but this implementation explicitly excludes it, since + /// it is obviously not reserved for future use. + /// + /// [IETF RFC 1112]: https://tools.ietf.org/html/rfc1112 + /// + /// # Warning + /// + /// As IANA assigns new addresses, this method 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. + #[must_use] + #[inline] + const fn is_reserved(a: Ipv4Addr) -> bool { + a.octets()[0] & 240 == 240 && !a.is_broadcast() + } + + /// Returns [`true`] if this address part of the `198.18.0.0/15` range, which is reserved for + /// network devices benchmarking. This range is defined in [IETF RFC 2544] as `192.18.0.0` + /// through `198.19.255.255` but [errata 423] corrects it to `198.18.0.0/15`. + /// + /// [IETF RFC 2544]: https://tools.ietf.org/html/rfc2544 + /// [errata 423]: https://www.rfc-editor.org/errata/eid423 + #[must_use] + #[inline] + const fn is_benchmarking(a: Ipv4Addr) -> bool { + a.octets()[0] == 198 && (a.octets()[1] & 0xfe) == 18 + } + + /// Returns [`true`] if this address is part of the Shared Address Space defined in + /// [IETF RFC 6598] (`100.64.0.0/10`). + /// + /// [IETF RFC 6598]: https://tools.ietf.org/html/rfc6598 + #[must_use] + #[inline] + const fn is_shared(a: Ipv4Addr) -> bool { + a.octets()[0] == 100 && (a.octets()[1] & 0b1100_0000 == 0b0100_0000) + } + + /// Returns [`true`] if this is a private address. + /// + /// The private address ranges are defined in [IETF RFC 1918] and include: + /// + /// - `10.0.0.0/8` + /// - `172.16.0.0/12` + /// - `192.168.0.0/16` + /// + /// [IETF RFC 1918]: https://tools.ietf.org/html/rfc1918 + #[must_use] + #[inline] + const fn is_private(a: Ipv4Addr) -> bool { + match a.octets() { + [10, ..] => true, + [172, b, ..] if b >= 16 && b <= 31 => true, + [192, 168, ..] => true, + _ => false, + } + } + + /// Returns [`true`] if the address appears to be globally reachable + /// as specified by the [IANA IPv4 Special-Purpose Address Registry]. + /// Whether or not an address is practically reachable will depend on your network configuration. + /// + /// Most IPv4 addresses are globally reachable; + /// unless they are specifically defined as *not* globally reachable. + /// + /// Non-exhaustive list of notable addresses that are not globally reachable: + /// + /// - The [unspecified address] ([`is_unspecified`](Ipv4Addr::is_unspecified)) + /// - Addresses reserved for private use ([`is_private`](Ipv4Addr::is_private)) + /// - Addresses in the shared address space ([`is_shared`](Ipv4Addr::is_shared)) + /// - Loopback addresses ([`is_loopback`](Ipv4Addr::is_loopback)) + /// - Link-local addresses ([`is_link_local`](Ipv4Addr::is_link_local)) + /// - Addresses reserved for documentation ([`is_documentation`](Ipv4Addr::is_documentation)) + /// - Addresses reserved for benchmarking ([`is_benchmarking`](Ipv4Addr::is_benchmarking)) + /// - Reserved addresses ([`is_reserved`](Ipv4Addr::is_reserved)) + /// - The [broadcast address] ([`is_broadcast`](Ipv4Addr::is_broadcast)) + /// + /// For the complete overview of which addresses are globally reachable, see the table at the [IANA IPv4 Special-Purpose Address Registry]. + /// + /// [IANA IPv4 Special-Purpose Address Registry]: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + /// [unspecified address]: Ipv4Addr::UNSPECIFIED + /// [broadcast address]: Ipv4Addr::BROADCAST + #[must_use] + #[inline] + pub(crate) const fn is_global(a: Ipv4Addr) -> bool { + !(a.octets()[0] == 0 // "This network" + || is_private(a) + || is_shared(a) + || a.is_loopback() + || a.is_link_local() + // addresses reserved for future protocols (`192.0.0.0/24`) + ||(a.octets()[0] == 192 && a.octets()[1] == 0 && a.octets()[2] == 0) + || a.is_documentation() + || is_benchmarking(a) + || is_reserved(a) + || a.is_broadcast()) + } +} + +/// This module contains an implementation of the `is_global` IPv6 address space. +/// +/// Credit for this implementation goes to the Rust standard library team. +/// +/// Unstable tracking issue: [#27709](https://github.com/rust-lang/rust/issues/27709) +pub(crate) mod ipv6_global { + use std::net::Ipv6Addr; + + /// Returns `true` if the address is a unicast address with link-local scope, + /// as defined in [RFC 4291]. + /// + /// A unicast address has link-local scope if it has the prefix `fe80::/10`, as per [RFC 4291 section 2.4]. + /// Note that this encompasses more addresses than those defined in [RFC 4291 section 2.5.6], + /// which describes "Link-Local IPv6 Unicast Addresses" as having the following stricter format: + /// + /// ```text + /// | 10 bits | 54 bits | 64 bits | + /// +----------+-------------------------+----------------------------+ + /// |1111111010| 0 | interface ID | + /// +----------+-------------------------+----------------------------+ + /// ``` + /// So while currently the only addresses with link-local scope an application will encounter are all in `fe80::/64`, + /// this might change in the future with the publication of new standards. More addresses in `fe80::/10` could be allocated, + /// and those addresses will have link-local scope. + /// + /// Also note that while [RFC 4291 section 2.5.3] mentions about the [loopback address] (`::1`) that "it is treated as having Link-Local scope", + /// this does not mean that the loopback address actually has link-local scope and this method will return `false` on it. + /// + /// [RFC 4291]: https://tools.ietf.org/html/rfc4291 + /// [RFC 4291 section 2.4]: https://tools.ietf.org/html/rfc4291#section-2.4 + /// [RFC 4291 section 2.5.3]: https://tools.ietf.org/html/rfc4291#section-2.5.3 + /// [RFC 4291 section 2.5.6]: https://tools.ietf.org/html/rfc4291#section-2.5.6 + /// [loopback address]: Ipv6Addr::LOCALHOST + #[must_use] + #[inline] + const fn is_unicast_link_local(a: Ipv6Addr) -> bool { + (a.segments()[0] & 0xffc0) == 0xfe80 + } + + /// Returns [`true`] if this is a unique local address (`fc00::/7`). + /// + /// This property is defined in [IETF RFC 4193]. + /// + /// [IETF RFC 4193]: https://tools.ietf.org/html/rfc4193 + #[must_use] + #[inline] + const fn is_unique_local(a: Ipv6Addr) -> bool { + (a.segments()[0] & 0xfe00) == 0xfc00 + } + + /// Returns [`true`] if this is an address reserved for documentation + /// (`2001:db8::/32`). + /// + /// This property is defined in [IETF RFC 3849]. + /// + /// [IETF RFC 3849]: https://tools.ietf.org/html/rfc3849 + #[must_use] + #[inline] + const fn is_documentation(a: Ipv6Addr) -> bool { + (a.segments()[0] == 0x2001) && (a.segments()[1] == 0xdb8) + } + + /// Returns [`true`] if the address appears to be globally reachable + /// as specified by the [IANA IPv6 Special-Purpose Address Registry]. + /// Whether or not an address is practically reachable will depend on your network configuration. + /// + /// Most IPv6 addresses are globally reachable; + /// unless they are specifically defined as *not* globally reachable. + /// + /// Non-exhaustive list of notable addresses that are not globally reachable: + /// - The [unspecified address] ([`is_unspecified`](Ipv6Addr::is_unspecified)) + /// - The [loopback address] ([`is_loopback`](Ipv6Addr::is_loopback)) + /// - IPv4-mapped addresses + /// - Addresses reserved for benchmarking + /// - Addresses reserved for documentation ([`is_documentation`](Ipv6Addr::is_documentation)) + /// - Unique local addresses ([`is_unique_local`](Ipv6Addr::is_unique_local)) + /// - Unicast addresses with link-local scope ([`is_unicast_link_local`](Ipv6Addr::is_unicast_link_local)) + /// + /// For the complete overview of which addresses are globally reachable, see the table at the [IANA IPv6 Special-Purpose Address Registry]. + /// + /// Note that an address having global scope is not the same as being globally reachable, + /// and there is no direct relation between the two concepts: There exist addresses with global scope + /// that are not globally reachable (for example unique local addresses), + /// and addresses that are globally reachable without having global scope + /// (multicast addresses with non-global scope). + /// + /// [IANA IPv6 Special-Purpose Address Registry]: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml + /// [unspecified address]: Ipv6Addr::UNSPECIFIED + /// [loopback address]: Ipv6Addr::LOCALHOST + #[must_use] + #[inline] + pub(crate) const fn is_global(a: Ipv6Addr) -> bool { + !(a.is_unspecified() + || a.is_loopback() + // IPv4-mapped Address (`::ffff:0:0/96`) + || matches!(a.segments(), [0, 0, 0, 0, 0, 0xffff, _, _]) + // IPv4-IPv6 Translat. (`64:ff9b:1::/48`) + || matches!(a.segments(), [0x64, 0xff9b, 1, _, _, _, _, _]) + // Discard-Only Address Block (`100::/64`) + || matches!(a.segments(), [0x100, 0, 0, 0, _, _, _, _]) + // IETF Protocol Assignments (`2001::/23`) + || (matches!(a.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200) + && !( + // Port Control Protocol Anycast (`2001:1::1`) + u128::from_be_bytes(a.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001 + // Traversal Using Relays around NAT Anycast (`2001:1::2`) + || u128::from_be_bytes(a.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002 + // AMT (`2001:3::/32`) + || matches!(a.segments(), [0x2001, 3, _, _, _, _, _, _]) + // AS112-v6 (`2001:4:112::/48`) + || matches!(a.segments(), [0x2001, 4, 0x112, _, _, _, _, _]) + // ORCHIDv2 (`2001:20::/28`) + || matches!(a.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x2F) + )) + || is_documentation(a) + || is_unique_local(a) + || is_unicast_link_local(a)) + } +} diff --git a/protocols/autonat/src/v2/server/handler/dial_request.rs b/protocols/autonat/src/v2/server/handler/dial_request.rs index 9a3729d4ccf..a4927299c23 100644 --- a/protocols/autonat/src/v2/server/handler/dial_request.rs +++ b/protocols/autonat/src/v2/server/handler/dial_request.rs @@ -1,5 +1,6 @@ use std::{ io, + sync::OnceLock, task::{Context, Poll}, time::Duration, }; @@ -11,6 +12,7 @@ use futures::{ }; use futures_bounded::FuturesSet; use libp2p_core::{ + multiaddr::Protocol, upgrade::{DeniedUpgrade, ReadyUpgrade}, Multiaddr, }; @@ -19,10 +21,12 @@ use libp2p_swarm::{ handler::{ConnectionEvent, FullyNegotiatedInbound, ListenUpgradeError}, ConnectionHandler, ConnectionHandlerEvent, StreamProtocol, SubstreamProtocol, }; +use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig}; use rand_core::RngCore; use crate::v2::{ generated::structs::{mod_DialResponse::ResponseStatus, DialStatus}, + global_ip::{ipv4_global, ipv6_global}, protocol::{Coder, DialDataRequest, DialRequest, DialResponse, Request, Response}, server::behaviour::Event, Nonce, DIAL_REQUEST_PROTOCOL, @@ -265,7 +269,7 @@ async fn handle_request_internal( where I: AsyncRead + AsyncWrite + Unpin, { - let DialRequest { mut addrs, nonce } = match coder + let DialRequest { addrs, nonce } = match coder .next() .await .map_err(|_| HandleFail::InternalError(0))? @@ -276,8 +280,17 @@ where } }; all_addrs.clone_from(&addrs); - let idx = 0; - let addr = addrs.pop().ok_or(HandleFail::DialRefused)?; + let (idx, addr) = addrs + .into_iter() + .enumerate() + .find(|(_, addr)| { + addr.iter().any(|proto| match proto { + Protocol::Ip4(addr) if ipv4_global::is_global(addr) => support_ipv4(), + Protocol::Ip6(addr) if ipv6_global::is_global(addr) => support_ipv6(), + _ => false, + }) + }) + .ok_or(HandleFail::DialRefused)?; *tested_addrs = Some(addr.clone()); *data_amount = 0; if addr != observed_multiaddr { @@ -330,3 +343,33 @@ where dial_status: DialStatus::OK, }) } + +fn support_ipv4() -> bool { + static IPV4_SUPPORT: OnceLock = OnceLock::new(); + *IPV4_SUPPORT.get_or_init(|| { + NetworkInterface::show() + .unwrap_or_default() + .iter() + .any(|interface| { + interface + .addr + .iter() + .any(|addr| matches!(addr, Addr::V4(a) if ipv4_global::is_global(a.ip) )) + }) + }) +} + +fn support_ipv6() -> bool { + static IPV6_SUPPORT: OnceLock = OnceLock::new(); + *IPV6_SUPPORT.get_or_init(|| { + NetworkInterface::show() + .unwrap_or_default() + .iter() + .any(|interface| { + interface + .addr + .iter() + .any(|addr| matches!(addr, Addr::V6(a) if ipv6_global::is_global(a.ip) )) + }) + }) +}