aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config/mod.rs10
-rw-r--r--src/main.rs5
-rw-r--r--src/simtest/identity.rs1
-rw-r--r--src/transport/libp2p.rs243
4 files changed, 248 insertions, 11 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs
index cb4bf19..85d71d2 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -271,6 +271,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[
"With the libp2p transport: the addresses to publish in the member list instead of those listened on, e.g. a public address forwarded to this node's listen port. Without, the listen addresses are published, loopback and link-local ones left out.",
),
option(
+ "libp2p-ignore",
+ Kind::List,
+ "RANGE,[RANGE...]",
+ None,
+ "With the libp2p transport: address ranges libp2p leaves out, e.g. 10.244.0.0/16 and 42::/16 (VPNs): addresses there are neither published (but announced ones) nor offered for hole punching nor dialled, and connections there are refused, whoever dials.",
+ ),
+ option(
"libp2p-relay",
Kind::Flag,
"BOOL",
@@ -468,6 +475,9 @@ pub(crate) struct Settings {
pub(crate) libp2p_upnp: bool,
#[serde(default)]
#[cfg_attr(not(feature = "libp2p"), allow(dead_code))]
+ pub(crate) libp2p_ignore: Vec<String>,
+ #[serde(default)]
+ #[cfg_attr(not(feature = "libp2p"), allow(dead_code))]
pub(crate) libp2p_public: bool,
#[cfg_attr(not(feature = "libp2p"), allow(dead_code))]
pub(crate) libp2p_key_file: Option<PathBuf>,
diff --git a/src/main.rs b/src/main.rs
index 1803f15..1cfd173 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -392,6 +392,11 @@ fn libp2p_config(
continuum: !relay_only,
mdns: args.libp2p_mdns,
upnp: args.libp2p_upnp,
+ ignore: args
+ .libp2p_ignore
+ .iter()
+ .map(|range| range.parse().context("libp2p-ignore"))
+ .collect::<Result<Vec<_>>>()?,
public: args.libp2p_public,
memory: false,
})
diff --git a/src/simtest/identity.rs b/src/simtest/identity.rs
index 13c596a..e0dd6f2 100644
--- a/src/simtest/identity.rs
+++ b/src/simtest/identity.rs
@@ -222,6 +222,7 @@ async fn nodes_over_libp2p() {
continuum: true,
mdns: false,
upnp: false,
+ ignore: Vec::new(),
public: false,
memory: true,
})
diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs
index 2808fae..5419f46 100644
--- a/src/transport/libp2p.rs
+++ b/src/transport/libp2p.rs
@@ -40,6 +40,9 @@
//! AutoNAT asks connected peers (nodes of the swarm) to dial it back. Once AutoNAT says
//! it can be dialled, it lets the relays it picked go.
//!
+//! With `--libp2p-ignore`, address ranges are left out (VPNs): not published, not
+//! offered for hole punching, not dialled, and connections there refused (`Ignoring`).
+//!
//! With `--libp2p-upnp`, a node asks its router to forward its listen ports (UPnP), and
//! publishes the router's public address for them with its own (`Book::direct`): then
//! it can be dialled from elsewhere, whatever the other side's NAT.
@@ -179,6 +182,8 @@ pub(crate) struct Libp2pConfig {
pub(crate) mdns: bool,
/// Ask the router to forward our listen ports (UPnP), and publish where.
pub(crate) upnp: bool,
+ /// Address ranges neither published nor dialled, nor connected over (`Ignoring`).
+ pub(crate) ignore: Vec<IpRange>,
/// Use public infrastructure: the IPFS DHT, public relays.
pub(crate) public: bool,
/// libp2p's in-memory transport instead of TCP and QUIC: for tests.
@@ -265,6 +270,8 @@ struct Book {
mapped: Vec<Multiaddr>,
/// `Libp2pConfig::announce`.
announced: Vec<Multiaddr>,
+ /// `Libp2pConfig::ignore`: not published (but announced), not dialled.
+ ignored: Vec<IpRange>,
/// `Libp2pConfig::relay`: this node relays, and says so.
serving: bool,
/// Relays named to pick (configured, listed), by peer: picked first.
@@ -610,6 +617,7 @@ impl Book {
}
direct.retain(|address| !first.contains(address));
first.extend(direct);
+ first.retain(|address| !ignored(&self.ignored, address));
first
}
}
@@ -822,6 +830,148 @@ fn relay_of(address: &Multiaddr) -> Option<PeerId> {
/// The addresses of `known` this node can dial: not those through itself as the relay
/// (a relay reaches the nodes it relays for only while they are connected to it).
+/// An address range (`10.244.0.0/16`, `42::/16`; a bare address is a range of one), for
+/// `Libp2pConfig::ignore`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct IpRange {
+ network: std::net::IpAddr,
+ prefix: u8,
+}
+
+impl FromStr for IpRange {
+ type Err = anyhow::Error;
+
+ fn from_str(text: &str) -> Result<Self> {
+ let (address, prefix) = match text.split_once('/') {
+ Some((address, prefix)) => (address, Some(prefix)),
+ None => (text, None),
+ };
+ let network: std::net::IpAddr = address
+ .parse()
+ .with_context(|| format!("{text:?} is no address range (e.g. 10.244.0.0/16)"))?;
+ let bits = if network.is_ipv4() { 32 } else { 128 };
+ let prefix = match prefix {
+ Some(prefix) => prefix
+ .parse::<u8>()
+ .ok()
+ .filter(|prefix| *prefix <= bits)
+ .with_context(|| format!("{text:?}: the prefix is 0 to {bits}"))?,
+ None => bits,
+ };
+ Ok(Self { network, prefix })
+ }
+}
+
+impl IpRange {
+ fn contains(&self, ip: std::net::IpAddr) -> bool {
+ use std::net::IpAddr;
+ match (self.network, ip) {
+ (IpAddr::V4(network), IpAddr::V4(ip)) => {
+ let mask = u32::MAX
+ .checked_shl(32 - u32::from(self.prefix))
+ .unwrap_or(0);
+ u32::from(network) & mask == u32::from(ip) & mask
+ }
+ (IpAddr::V6(network), IpAddr::V6(ip)) => {
+ let mask = u128::MAX
+ .checked_shl(128 - u32::from(self.prefix))
+ .unwrap_or(0);
+ u128::from(network) & mask == u128::from(ip) & mask
+ }
+ _ => false,
+ }
+ }
+}
+
+/// Whether `address` is in one of `ranges`: a direct address there (one through a relay
+/// is judged by the connection to the relay).
+fn ignored(ranges: &[IpRange], address: &Multiaddr) -> bool {
+ if ranges.is_empty() || is_relayed(address) {
+ return false;
+ }
+ let ip = match address.iter().next() {
+ Some(Protocol::Ip4(ip)) => std::net::IpAddr::V4(ip),
+ Some(Protocol::Ip6(ip)) => std::net::IpAddr::V6(ip),
+ _ => return false,
+ };
+ ranges.iter().any(|range| range.contains(ip))
+}
+
+/// Keeps connections off the address ranges `libp2p-ignore` names, whoever dialled:
+/// hole punching and identify bring addresses there too, and a peer may dial us there.
+struct Ignoring {
+ ranges: Vec<IpRange>,
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("{0} is in a range left out (libp2p-ignore)")]
+struct IgnoredAddress(Multiaddr);
+
+impl ::libp2p::swarm::NetworkBehaviour for Ignoring {
+ type ConnectionHandler = ::libp2p::swarm::dummy::ConnectionHandler;
+ type ToSwarm = std::convert::Infallible;
+
+ fn handle_pending_inbound_connection(
+ &mut self,
+ _connection_id: ConnectionId,
+ _local_addr: &Multiaddr,
+ remote_addr: &Multiaddr,
+ ) -> Result<(), ::libp2p::swarm::ConnectionDenied> {
+ if ignored(&self.ranges, remote_addr) {
+ return Err(::libp2p::swarm::ConnectionDenied::new(IgnoredAddress(
+ remote_addr.clone(),
+ )));
+ }
+ Ok(())
+ }
+
+ fn handle_established_inbound_connection(
+ &mut self,
+ _connection_id: ConnectionId,
+ _peer: PeerId,
+ _local_addr: &Multiaddr,
+ _remote_addr: &Multiaddr,
+ ) -> Result<::libp2p::swarm::THandler<Self>, ::libp2p::swarm::ConnectionDenied> {
+ Ok(::libp2p::swarm::dummy::ConnectionHandler)
+ }
+
+ fn handle_established_outbound_connection(
+ &mut self,
+ _connection_id: ConnectionId,
+ _peer: PeerId,
+ addr: &Multiaddr,
+ _role_override: ::libp2p::core::Endpoint,
+ _port_use: ::libp2p::core::transport::PortUse,
+ ) -> Result<::libp2p::swarm::THandler<Self>, ::libp2p::swarm::ConnectionDenied> {
+ if ignored(&self.ranges, addr) {
+ return Err(::libp2p::swarm::ConnectionDenied::new(IgnoredAddress(
+ addr.clone(),
+ )));
+ }
+ Ok(::libp2p::swarm::dummy::ConnectionHandler)
+ }
+
+ fn on_swarm_event(&mut self, _event: ::libp2p::swarm::FromSwarm) {}
+
+ fn on_connection_handler_event(
+ &mut self,
+ _peer_id: PeerId,
+ _connection_id: ConnectionId,
+ event: ::libp2p::swarm::THandlerOutEvent<Self>,
+ ) {
+ match event {}
+ }
+
+ fn poll(
+ &mut self,
+ _cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<
+ ::libp2p::swarm::ToSwarm<Self::ToSwarm, ::libp2p::swarm::THandlerInEvent<Self>>,
+ > {
+ std::task::Poll::Pending
+ }
+}
+
fn dialable(known: &[Multiaddr], local: PeerId) -> Vec<Multiaddr> {
known
.iter()
@@ -858,9 +1008,9 @@ impl Libp2pTransport {
let peer_id = config.key.public().to_peer_id();
let features = Features::of(&config);
let mut swarm = if config.memory {
- memory_swarm(config.key, features)?
+ memory_swarm(config.key, features, config.ignore.clone())?
} else {
- network_swarm(config.key, features)?
+ network_swarm(config.key, features, config.ignore.clone())?
};
for address in config.listen {
swarm
@@ -884,6 +1034,7 @@ impl Libp2pTransport {
let (commands, receiver) = mpsc::unbounded_channel();
let mut book = Book {
announced: config.announce,
+ ignored: config.ignore.clone(),
serving: config.relay,
..Book::default()
};
@@ -922,11 +1073,12 @@ impl Libp2pTransport {
async fn connect(&self, peer: PeerId) -> Result<()> {
let addresses = {
let book = lock(&self.book);
- let known = book
+ let mut known = book
.addresses
.get(&peer)
.map(|known| dialable(known, self.peer_id))
.unwrap_or_default();
+ known.retain(|address| !ignored(&book.ignored, address));
if known.is_empty() {
book.through_relays(peer, self.peer_id)
} else {
@@ -955,6 +1107,8 @@ const IDENTIFY_VERSION: &str = "/continuum/1";
#[derive(::libp2p::swarm::NetworkBehaviour)]
struct Behaviour {
+ /// First: connections it denies need nothing from the others.
+ ignoring: Ignoring,
stream: libp2p_stream::Behaviour,
identify: ::libp2p::identify::Behaviour,
relay_client: ::libp2p::relay::client::Behaviour,
@@ -986,6 +1140,8 @@ struct HolePunching {
inner: ::libp2p::dcutr::Behaviour,
/// Where we listen (`offered`).
listening: Vec<Multiaddr>,
+ /// `Libp2pConfig::ignore`: not offered.
+ ignored: Vec<IpRange>,
}
/// Whether DCUtR offers our listen address `address` too: neither loopback nor
@@ -995,10 +1151,11 @@ fn offered(address: &Multiaddr) -> bool {
}
impl HolePunching {
- fn new(local: PeerId) -> Self {
+ fn new(local: PeerId, ignored: Vec<IpRange>) -> Self {
Self {
inner: ::libp2p::dcutr::Behaviour::new(local),
listening: Vec::new(),
+ ignored,
}
}
@@ -1086,7 +1243,9 @@ impl ::libp2p::swarm::NetworkBehaviour for HolePunching {
use ::libp2p::swarm::FromSwarm;
self.inner.on_swarm_event(event);
match event {
- FromSwarm::NewListenAddr(new) if offered(new.addr) => {
+ FromSwarm::NewListenAddr(new)
+ if offered(new.addr) && !ignored(&self.ignored, new.addr) =>
+ {
if !self.listening.contains(new.addr) {
debug!(address = %new.addr, "Offering where we listen for hole punching");
self.listening.push(new.addr.clone());
@@ -1158,6 +1317,7 @@ impl Behaviour {
key: &Keypair,
relay_client: ::libp2p::relay::client::Behaviour,
features: Features,
+ ignored: Vec<IpRange>,
) -> std::io::Result<Self> {
let Features {
relay,
@@ -1203,11 +1363,14 @@ impl Behaviour {
false => None,
};
Ok(Self {
+ ignoring: Ignoring {
+ ranges: ignored.clone(),
+ },
stream: libp2p_stream::Behaviour::new(),
identify: ::libp2p::identify::Behaviour::new(identify),
relay_client,
relay: relay.into(),
- dcutr: HolePunching::new(peer_id),
+ dcutr: HolePunching::new(peer_id, ignored),
ping: ::libp2p::ping::Behaviour::new(::libp2p::ping::Config::new()),
autonat: ::libp2p::autonat::Behaviour::new(
peer_id,
@@ -1224,7 +1387,11 @@ impl Behaviour {
}
}
-fn network_swarm(key: Keypair, features: Features) -> Result<Swarm<Behaviour>> {
+fn network_swarm(
+ key: Keypair,
+ features: Features,
+ ignored: Vec<IpRange>,
+) -> Result<Swarm<Behaviour>> {
Ok(::libp2p::SwarmBuilder::with_existing_identity(key)
.with_tokio()
.with_tcp(
@@ -1239,7 +1406,7 @@ fn network_swarm(key: Keypair, features: Features) -> Result<Swarm<Behaviour>> {
::libp2p::yamux::Config::default,
)?
.with_behaviour(|key, relay_client| {
- Behaviour::new(key, relay_client, features)
+ Behaviour::new(key, relay_client, features, ignored.clone())
.map_err(|error| Box::new(error) as Box<dyn std::error::Error + Send + Sync>)
})?
.with_swarm_config(|config| swarm_config(config, features))
@@ -1263,7 +1430,11 @@ fn swarm_config(config: ::libp2p::swarm::Config, features: Features) -> ::libp2p
}
}
-fn memory_swarm(key: Keypair, features: Features) -> Result<Swarm<Behaviour>> {
+fn memory_swarm(
+ key: Keypair,
+ features: Features,
+ ignored: Vec<IpRange>,
+) -> Result<Swarm<Behaviour>> {
use ::libp2p::Transport as _;
use ::libp2p::core::transport::{MemoryTransport, OrTransport};
use ::libp2p::core::upgrade::Version;
@@ -1279,7 +1450,7 @@ fn memory_swarm(key: Keypair, features: Features) -> Result<Swarm<Behaviour>> {
.boxed();
Ok(Swarm::new(
transport,
- Behaviour::new(&key, relay_client, features)?,
+ Behaviour::new(&key, relay_client, features, ignored)?,
peer_id,
swarm_config(::libp2p::swarm::Config::with_tokio_executor(), features),
))
@@ -1414,7 +1585,7 @@ fn upgrade(swarm: &mut Swarm<Behaviour>, book: &Mutex<Book>, peer: PeerId, local
.map(|known| dialable(known, local))
.unwrap_or_default()
.into_iter()
- .filter(|address| !is_relayed(address))
+ .filter(|address| !is_relayed(address) && !ignored(&book.ignored, address))
.collect();
if direct.is_empty() {
return;
@@ -2303,6 +2474,7 @@ mod tests {
continuum: true,
mdns: false,
upnp: false,
+ ignore: Vec::new(),
public: false,
memory: true,
})
@@ -2450,6 +2622,7 @@ mod tests {
continuum: true,
mdns: false,
upnp: false,
+ ignore: Vec::new(),
public: false,
memory: true,
})
@@ -2460,6 +2633,53 @@ mod tests {
);
}
+ #[test]
+ fn address_ranges() {
+ let range = |text: &str| text.parse::<IpRange>().unwrap();
+ let address = |text: &str| text.parse::<Multiaddr>().unwrap();
+ let ranges = [range("10.244.0.0/16"), range("42::/16"), range("192.0.2.7")];
+ for left_out in [
+ "/ip4/10.244.3.2/udp/9661/quic-v1",
+ "/ip6/42:1:a46e:be4a:cb58:e460:bf0:142c/tcp/9661",
+ "/ip4/192.0.2.7/tcp/1",
+ ] {
+ assert!(ignored(&ranges, &address(left_out)), "{left_out}");
+ }
+ for kept in [
+ "/ip4/10.23.1.144/udp/9661/quic-v1",
+ "/ip4/10.245.0.1/tcp/1",
+ "/ip6/2a0a:4cc0:c1:5eb0::1/udp/9661/quic-v1",
+ "/ip4/192.0.2.8/tcp/1",
+ "/dns4/example.org/tcp/1",
+ ] {
+ assert!(!ignored(&ranges, &address(kept)), "{kept}");
+ }
+ // Through a relay: judged by the connection to the relay, not here.
+ let relayed = format!("/ip4/10.244.0.1/tcp/1/p2p/{}/p2p-circuit", peer());
+ assert!(!ignored(&ranges, &address(&relayed)));
+ assert!(range("0.0.0.0/0").contains("203.0.113.1".parse().unwrap()));
+ assert!(!range("0.0.0.0/0").contains("::1".parse().unwrap()));
+ for wrong in ["10.244.0.0/33", "42::/129", "nowhere", "10.0.0.0/x"] {
+ assert!(wrong.parse::<IpRange>().is_err(), "{wrong}");
+ }
+ }
+
+ /// Ranges left out aren't published; announced addresses are, whatever they are.
+ #[test]
+ fn ignored_addresses_are_not_published() {
+ let parse = |text: &str| text.parse::<Multiaddr>().unwrap();
+ let lan = parse("/ip4/10.23.1.5/udp/9663/quic-v1");
+ let vpn = parse("/ip4/10.244.3.2/udp/9663/quic-v1");
+ let mut book = Book {
+ listening: vec![lan.clone(), vpn.clone()],
+ ignored: vec!["10.244.0.0/16".parse().unwrap()],
+ ..Book::default()
+ };
+ assert_eq!(book.direct(), [lan]);
+ book.announced = vec![vpn.clone()];
+ assert_eq!(book.direct(), [vpn]);
+ }
+
/// A port forwarded by hand: once AutoNAT found the node dialable at the router's
/// address, that is published with the listen addresses; announced ones still win.
#[test]
@@ -2607,6 +2827,7 @@ mod tests {
continuum: false,
mdns: false,
upnp: false,
+ ignore: Vec::new(),
public: false,
memory: true,
})