Skip to content

Commit 3fde132

Browse files
authored
feat(transport): Add GlobalOnly Transport implementation
Upstreams the `GlobalOnly` transport from @mxinden's kademlia-exporter to rust-libp2p. Source: https://github.com/mxinden/kademlia-exporter/blob/master/src/exporter/client/global_only.rs. Resolves: #3669. Pull-Request: #3814.
1 parent 541eba3 commit 3fde132

File tree

3 files changed

+351
-0
lines changed

3 files changed

+351
-0
lines changed

core/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
- Remove `SingletonMuxer`.
1717
See [PR 3883].
1818

19+
- Add `global_only::Transport` that refuses to dial IP addresses from private ranges.
20+
See [PR 3814].
21+
1922
[spec]: https://github.com/libp2p/specs/blob/master/connections/README.md#multistream-select
2023
[PR 3746]: https://github.com/libp2p/rust-libp2p/pull/3746
2124
[PR 3883]: https://github.com/libp2p/rust-libp2p/pull/3883
25+
[PR 3814]: https://github.com/libp2p/rust-libp2p/pull/3814
2226

2327
## 0.39.2
2428

core/src/transport.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use std::{
3737
pub mod and_then;
3838
pub mod choice;
3939
pub mod dummy;
40+
pub mod global_only;
4041
pub mod map;
4142
pub mod map_err;
4243
pub mod memory;

core/src/transport/global_only.rs

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
// Copyright 2023 Protocol Labs
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the "Software"),
5+
// to deal in the Software without restriction, including without limitation
6+
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
7+
// and/or sell copies of the Software, and to permit persons to whom the
8+
// Software is furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
// DEALINGS IN THE SOFTWARE.
20+
21+
use crate::{
22+
multiaddr::{Multiaddr, Protocol},
23+
transport::{ListenerId, TransportError, TransportEvent},
24+
};
25+
use log::debug;
26+
use std::{
27+
pin::Pin,
28+
task::{Context, Poll},
29+
};
30+
31+
/// Dropping all dial requests to non-global IP addresses.
32+
#[derive(Debug, Clone, Default)]
33+
pub struct Transport<T> {
34+
inner: T,
35+
}
36+
37+
/// This module contains an implementation of the `is_global` IPv4 address space.
38+
///
39+
/// Credit for this implementation goes to the Rust standard library team.
40+
///
41+
/// Unstable tracking issue: [#27709](https://github.com/rust-lang/rust/issues/27709)
42+
mod ipv4_global {
43+
use std::net::Ipv4Addr;
44+
45+
/// Returns [`true`] if this address is reserved by IANA for future use. [IETF RFC 1112]
46+
/// defines the block of reserved addresses as `240.0.0.0/4`. This range normally includes the
47+
/// broadcast address `255.255.255.255`, but this implementation explicitly excludes it, since
48+
/// it is obviously not reserved for future use.
49+
///
50+
/// [IETF RFC 1112]: https://tools.ietf.org/html/rfc1112
51+
///
52+
/// # Warning
53+
///
54+
/// As IANA assigns new addresses, this method will be
55+
/// updated. This may result in non-reserved addresses being
56+
/// treated as reserved in code that relies on an outdated version
57+
/// of this method.
58+
#[must_use]
59+
#[inline]
60+
const fn is_reserved(a: Ipv4Addr) -> bool {
61+
a.octets()[0] & 240 == 240 && !a.is_broadcast()
62+
}
63+
64+
/// Returns [`true`] if this address part of the `198.18.0.0/15` range, which is reserved for
65+
/// network devices benchmarking. This range is defined in [IETF RFC 2544] as `192.18.0.0`
66+
/// through `198.19.255.255` but [errata 423] corrects it to `198.18.0.0/15`.
67+
///
68+
/// [IETF RFC 2544]: https://tools.ietf.org/html/rfc2544
69+
/// [errata 423]: https://www.rfc-editor.org/errata/eid423
70+
#[must_use]
71+
#[inline]
72+
const fn is_benchmarking(a: Ipv4Addr) -> bool {
73+
a.octets()[0] == 198 && (a.octets()[1] & 0xfe) == 18
74+
}
75+
76+
/// Returns [`true`] if this address is part of the Shared Address Space defined in
77+
/// [IETF RFC 6598] (`100.64.0.0/10`).
78+
///
79+
/// [IETF RFC 6598]: https://tools.ietf.org/html/rfc6598
80+
#[must_use]
81+
#[inline]
82+
const fn is_shared(a: Ipv4Addr) -> bool {
83+
a.octets()[0] == 100 && (a.octets()[1] & 0b1100_0000 == 0b0100_0000)
84+
}
85+
86+
/// Returns [`true`] if this is a private address.
87+
///
88+
/// The private address ranges are defined in [IETF RFC 1918] and include:
89+
///
90+
/// - `10.0.0.0/8`
91+
/// - `172.16.0.0/12`
92+
/// - `192.168.0.0/16`
93+
///
94+
/// [IETF RFC 1918]: https://tools.ietf.org/html/rfc1918
95+
#[must_use]
96+
#[inline]
97+
const fn is_private(a: Ipv4Addr) -> bool {
98+
match a.octets() {
99+
[10, ..] => true,
100+
[172, b, ..] if b >= 16 && b <= 31 => true,
101+
[192, 168, ..] => true,
102+
_ => false,
103+
}
104+
}
105+
106+
/// Returns [`true`] if the address appears to be globally reachable
107+
/// as specified by the [IANA IPv4 Special-Purpose Address Registry].
108+
/// Whether or not an address is practically reachable will depend on your network configuration.
109+
///
110+
/// Most IPv4 addresses are globally reachable;
111+
/// unless they are specifically defined as *not* globally reachable.
112+
///
113+
/// Non-exhaustive list of notable addresses that are not globally reachable:
114+
///
115+
/// - The [unspecified address] ([`is_unspecified`](Ipv4Addr::is_unspecified))
116+
/// - Addresses reserved for private use ([`is_private`](Ipv4Addr::is_private))
117+
/// - Addresses in the shared address space ([`is_shared`](Ipv4Addr::is_shared))
118+
/// - Loopback addresses ([`is_loopback`](Ipv4Addr::is_loopback))
119+
/// - Link-local addresses ([`is_link_local`](Ipv4Addr::is_link_local))
120+
/// - Addresses reserved for documentation ([`is_documentation`](Ipv4Addr::is_documentation))
121+
/// - Addresses reserved for benchmarking ([`is_benchmarking`](Ipv4Addr::is_benchmarking))
122+
/// - Reserved addresses ([`is_reserved`](Ipv4Addr::is_reserved))
123+
/// - The [broadcast address] ([`is_broadcast`](Ipv4Addr::is_broadcast))
124+
///
125+
/// For the complete overview of which addresses are globally reachable, see the table at the [IANA IPv4 Special-Purpose Address Registry].
126+
///
127+
/// [IANA IPv4 Special-Purpose Address Registry]: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
128+
/// [unspecified address]: Ipv4Addr::UNSPECIFIED
129+
/// [broadcast address]: Ipv4Addr::BROADCAST
130+
#[must_use]
131+
#[inline]
132+
pub(crate) const fn is_global(a: Ipv4Addr) -> bool {
133+
!(a.octets()[0] == 0 // "This network"
134+
|| is_private(a)
135+
|| is_shared(a)
136+
|| a.is_loopback()
137+
|| a.is_link_local()
138+
// addresses reserved for future protocols (`192.0.0.0/24`)
139+
||(a.octets()[0] == 192 && a.octets()[1] == 0 && a.octets()[2] == 0)
140+
|| a.is_documentation()
141+
|| is_benchmarking(a)
142+
|| is_reserved(a)
143+
|| a.is_broadcast())
144+
}
145+
}
146+
147+
/// This module contains an implementation of the `is_global` IPv6 address space.
148+
///
149+
/// Credit for this implementation goes to the Rust standard library team.
150+
///
151+
/// Unstable tracking issue: [#27709](https://github.com/rust-lang/rust/issues/27709)
152+
mod ipv6_global {
153+
use std::net::Ipv6Addr;
154+
155+
/// Returns `true` if the address is a unicast address with link-local scope,
156+
/// as defined in [RFC 4291].
157+
///
158+
/// A unicast address has link-local scope if it has the prefix `fe80::/10`, as per [RFC 4291 section 2.4].
159+
/// Note that this encompasses more addresses than those defined in [RFC 4291 section 2.5.6],
160+
/// which describes "Link-Local IPv6 Unicast Addresses" as having the following stricter format:
161+
///
162+
/// ```text
163+
/// | 10 bits | 54 bits | 64 bits |
164+
/// +----------+-------------------------+----------------------------+
165+
/// |1111111010| 0 | interface ID |
166+
/// +----------+-------------------------+----------------------------+
167+
/// ```
168+
/// So while currently the only addresses with link-local scope an application will encounter are all in `fe80::/64`,
169+
/// this might change in the future with the publication of new standards. More addresses in `fe80::/10` could be allocated,
170+
/// and those addresses will have link-local scope.
171+
///
172+
/// 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",
173+
/// this does not mean that the loopback address actually has link-local scope and this method will return `false` on it.
174+
///
175+
/// [RFC 4291]: https://tools.ietf.org/html/rfc4291
176+
/// [RFC 4291 section 2.4]: https://tools.ietf.org/html/rfc4291#section-2.4
177+
/// [RFC 4291 section 2.5.3]: https://tools.ietf.org/html/rfc4291#section-2.5.3
178+
/// [RFC 4291 section 2.5.6]: https://tools.ietf.org/html/rfc4291#section-2.5.6
179+
/// [loopback address]: Ipv6Addr::LOCALHOST
180+
#[must_use]
181+
#[inline]
182+
const fn is_unicast_link_local(a: Ipv6Addr) -> bool {
183+
(a.segments()[0] & 0xffc0) == 0xfe80
184+
}
185+
186+
/// Returns [`true`] if this is a unique local address (`fc00::/7`).
187+
///
188+
/// This property is defined in [IETF RFC 4193].
189+
///
190+
/// [IETF RFC 4193]: https://tools.ietf.org/html/rfc4193
191+
#[must_use]
192+
#[inline]
193+
const fn is_unique_local(a: Ipv6Addr) -> bool {
194+
(a.segments()[0] & 0xfe00) == 0xfc00
195+
}
196+
197+
/// Returns [`true`] if this is an address reserved for documentation
198+
/// (`2001:db8::/32`).
199+
///
200+
/// This property is defined in [IETF RFC 3849].
201+
///
202+
/// [IETF RFC 3849]: https://tools.ietf.org/html/rfc3849
203+
#[must_use]
204+
#[inline]
205+
const fn is_documentation(a: Ipv6Addr) -> bool {
206+
(a.segments()[0] == 0x2001) && (a.segments()[1] == 0xdb8)
207+
}
208+
209+
/// Returns [`true`] if the address appears to be globally reachable
210+
/// as specified by the [IANA IPv6 Special-Purpose Address Registry].
211+
/// Whether or not an address is practically reachable will depend on your network configuration.
212+
///
213+
/// Most IPv6 addresses are globally reachable;
214+
/// unless they are specifically defined as *not* globally reachable.
215+
///
216+
/// Non-exhaustive list of notable addresses that are not globally reachable:
217+
/// - The [unspecified address] ([`is_unspecified`](Ipv6Addr::is_unspecified))
218+
/// - The [loopback address] ([`is_loopback`](Ipv6Addr::is_loopback))
219+
/// - IPv4-mapped addresses
220+
/// - Addresses reserved for benchmarking
221+
/// - Addresses reserved for documentation ([`is_documentation`](Ipv6Addr::is_documentation))
222+
/// - Unique local addresses ([`is_unique_local`](Ipv6Addr::is_unique_local))
223+
/// - Unicast addresses with link-local scope ([`is_unicast_link_local`](Ipv6Addr::is_unicast_link_local))
224+
///
225+
/// For the complete overview of which addresses are globally reachable, see the table at the [IANA IPv6 Special-Purpose Address Registry].
226+
///
227+
/// Note that an address having global scope is not the same as being globally reachable,
228+
/// and there is no direct relation between the two concepts: There exist addresses with global scope
229+
/// that are not globally reachable (for example unique local addresses),
230+
/// and addresses that are globally reachable without having global scope
231+
/// (multicast addresses with non-global scope).
232+
///
233+
/// [IANA IPv6 Special-Purpose Address Registry]: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
234+
/// [unspecified address]: Ipv6Addr::UNSPECIFIED
235+
/// [loopback address]: Ipv6Addr::LOCALHOST
236+
#[must_use]
237+
#[inline]
238+
pub(crate) const fn is_global(a: Ipv6Addr) -> bool {
239+
!(a.is_unspecified()
240+
|| a.is_loopback()
241+
// IPv4-mapped Address (`::ffff:0:0/96`)
242+
|| matches!(a.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
243+
// IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
244+
|| matches!(a.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
245+
// Discard-Only Address Block (`100::/64`)
246+
|| matches!(a.segments(), [0x100, 0, 0, 0, _, _, _, _])
247+
// IETF Protocol Assignments (`2001::/23`)
248+
|| (matches!(a.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
249+
&& !(
250+
// Port Control Protocol Anycast (`2001:1::1`)
251+
u128::from_be_bytes(a.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
252+
// Traversal Using Relays around NAT Anycast (`2001:1::2`)
253+
|| u128::from_be_bytes(a.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
254+
// AMT (`2001:3::/32`)
255+
|| matches!(a.segments(), [0x2001, 3, _, _, _, _, _, _])
256+
// AS112-v6 (`2001:4:112::/48`)
257+
|| matches!(a.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
258+
// ORCHIDv2 (`2001:20::/28`)
259+
|| matches!(a.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x2F)
260+
))
261+
|| is_documentation(a)
262+
|| is_unique_local(a)
263+
|| is_unicast_link_local(a))
264+
}
265+
}
266+
267+
impl<T> Transport<T> {
268+
pub fn new(transport: T) -> Self {
269+
Transport { inner: transport }
270+
}
271+
}
272+
273+
impl<T: crate::Transport + Unpin> crate::Transport for Transport<T> {
274+
type Output = <T as crate::Transport>::Output;
275+
type Error = <T as crate::Transport>::Error;
276+
type ListenerUpgrade = <T as crate::Transport>::ListenerUpgrade;
277+
type Dial = <T as crate::Transport>::Dial;
278+
279+
fn listen_on(&mut self, addr: Multiaddr) -> Result<ListenerId, TransportError<Self::Error>> {
280+
self.inner.listen_on(addr)
281+
}
282+
283+
fn remove_listener(&mut self, id: ListenerId) -> bool {
284+
self.inner.remove_listener(id)
285+
}
286+
287+
fn dial(&mut self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
288+
match addr.iter().next() {
289+
Some(Protocol::Ip4(a)) => {
290+
if !ipv4_global::is_global(a) {
291+
debug!("Not dialing non global IP address {:?}.", a);
292+
return Err(TransportError::MultiaddrNotSupported(addr));
293+
}
294+
self.inner.dial(addr)
295+
}
296+
Some(Protocol::Ip6(a)) => {
297+
if !ipv6_global::is_global(a) {
298+
debug!("Not dialing non global IP address {:?}.", a);
299+
return Err(TransportError::MultiaddrNotSupported(addr));
300+
}
301+
self.inner.dial(addr)
302+
}
303+
_ => {
304+
debug!("Not dialing unsupported Multiaddress {:?}.", addr);
305+
Err(TransportError::MultiaddrNotSupported(addr))
306+
}
307+
}
308+
}
309+
310+
fn dial_as_listener(
311+
&mut self,
312+
addr: Multiaddr,
313+
) -> Result<Self::Dial, TransportError<Self::Error>> {
314+
match addr.iter().next() {
315+
Some(Protocol::Ip4(a)) => {
316+
if !ipv4_global::is_global(a) {
317+
debug!("Not dialing non global IP address {:?}.", a);
318+
return Err(TransportError::MultiaddrNotSupported(addr));
319+
}
320+
self.inner.dial_as_listener(addr)
321+
}
322+
Some(Protocol::Ip6(a)) => {
323+
if !ipv6_global::is_global(a) {
324+
debug!("Not dialing non global IP address {:?}.", a);
325+
return Err(TransportError::MultiaddrNotSupported(addr));
326+
}
327+
self.inner.dial_as_listener(addr)
328+
}
329+
_ => {
330+
debug!("Not dialing unsupported Multiaddress {:?}.", addr);
331+
Err(TransportError::MultiaddrNotSupported(addr))
332+
}
333+
}
334+
}
335+
336+
fn address_translation(&self, listen: &Multiaddr, observed: &Multiaddr) -> Option<Multiaddr> {
337+
self.inner.address_translation(listen, observed)
338+
}
339+
340+
fn poll(
341+
mut self: Pin<&mut Self>,
342+
cx: &mut Context<'_>,
343+
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
344+
Pin::new(&mut self.inner).poll(cx)
345+
}
346+
}

0 commit comments

Comments
 (0)