Skip to content

Commit

Permalink
feat(autonat): Implement AutoNATv2
Browse files Browse the repository at this point in the history
Closes: #4524

This is the implementation of the evolved AutoNAT protocol, named AutonatV2 as defined in the [spec](https://github.com/libp2p/specs/blob/03718ef0f2dea4a756a85ba716ee33f97e4a6d6c/autonat/autonat-v2.md).
The stabilization PR for the spec can be found under libp2p/specs#538.

The work on the Rust implementation can be found in the PR to my fork: umgefahren#1.

The implementation has been smoke-tested with the Go implementation (PR: libp2p/go-libp2p#2469).

The new protocol addresses shortcomings of the original AutoNAT protocol:

- Since the server now always dials back over a newly allocated port, this made #4568 necessary; the client can be sure of the reachability state for other peers, even if the connection to the server was made through a hole punch.
- The server can now test addresses different from the observed address (i.e., the connection to the server was made through a `p2p-circuit`). To mitigate against DDoS attacks, the client has to send more data to the server than the dial-back costs.

Pull-Request: #5526.
  • Loading branch information
umgefahren authored Aug 8, 2024
1 parent ddda3be commit 848e2e9
Show file tree
Hide file tree
Showing 34 changed files with 3,525 additions and 80 deletions.
227 changes: 196 additions & 31 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"core",
"examples/autonat",
"examples/autonatv2",
"examples/browser-webrtc",
"examples/chat",
"examples/dcutr",
Expand Down
38 changes: 38 additions & 0 deletions examples/autonatv2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[package]
name = "autonatv2"
version = "0.1.0"
edition = "2021"
publish = false
license = "MIT or Apache-2.0"

[package.metadata.release]
release = false

[[bin]]
name = "autonatv2_client"

[[bin]]
name = "autonatv2_server"

[dependencies]
libp2p = { workspace = true, features = ["macros", "tokio", "tcp", "noise", "yamux", "autonat", "identify", "dns", "quic"] }
clap = { version = "4.4.18", features = ["derive"] }
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
rand = "0.8.5"
opentelemetry = { version = "0.21.0", optional = true }
opentelemetry_sdk = { version = "0.21.1", optional = true, features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.22.0", optional = true }
opentelemetry-jaeger = { version = "0.20.0", optional = true, features = ["rt-tokio"] }
cfg-if = "1.0.0"

[features]
jaeger = ["opentelemetry", "opentelemetry_sdk", "tracing-opentelemetry", "opentelemetry-jaeger"]
opentelemetry = ["dep:opentelemetry"]
opentelemetry_sdk = ["dep:opentelemetry_sdk"]
tracing-opentelemetry = ["dep:tracing-opentelemetry"]
opentelemetry-jaeger = ["dep:opentelemetry-jaeger"]

[lints]
workspace = true
20 changes: 20 additions & 0 deletions examples/autonatv2/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM rust:1.75-alpine as builder

RUN apk add musl-dev

WORKDIR /workspace
COPY . .
RUN --mount=type=cache,target=./target \
--mount=type=cache,target=/usr/local/cargo/registry \
cargo build --release --package autonatv2 --bin autonatv2_server -F jaeger

RUN --mount=type=cache,target=./target \
mv ./target/release/autonatv2_server /usr/local/bin/autonatv2_server

FROM alpine:latest

COPY --from=builder /usr/local/bin/autonatv2_server /app/autonatv2_server

EXPOSE 4884

ENTRYPOINT [ "/app/autonatv2_server", "-l", "4884" ]
16 changes: 16 additions & 0 deletions examples/autonatv2/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: '3'

services:
autonatv2:
build:
context: ../..
dockerfile: examples/autonatv2/Dockerfile
ports:
- 4884:4884
jaeger:
image: jaegertracing/all-in-one
ports:
- 6831:6831/udp
- 6832:6832/udp
- 16686:16686
- 14268:14268
111 changes: 111 additions & 0 deletions examples/autonatv2/src/bin/autonatv2_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use std::{error::Error, net::Ipv4Addr, time::Duration};

use clap::Parser;
use libp2p::{
autonat,
futures::StreamExt,
identify, identity,
multiaddr::Protocol,
noise,
swarm::{dial_opts::DialOpts, NetworkBehaviour, SwarmEvent},
tcp, yamux, Multiaddr, SwarmBuilder,
};
use rand::rngs::OsRng;
use tracing_subscriber::EnvFilter;

#[derive(Debug, Parser)]
#[clap(name = "libp2p autonatv2 client")]
struct Opt {
/// Port where the client will listen for incoming connections.
#[clap(short = 'p', long, default_value_t = 0)]
listen_port: u16,

/// Address of the server where want to connect to.
#[clap(short = 'a', long)]
server_address: Multiaddr,

/// Probe interval in seconds.
#[clap(short = 't', long, default_value = "2")]
probe_interval: u64,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.try_init();

let opt = Opt::parse();

let mut swarm = SwarmBuilder::with_new_identity()
.with_tokio()
.with_tcp(
tcp::Config::default(),
noise::Config::new,
yamux::Config::default,
)?
.with_quic()
.with_dns()?
.with_behaviour(|key| Behaviour::new(key.public(), opt.probe_interval))?
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(10)))
.build();

swarm.listen_on(
Multiaddr::empty()
.with(Protocol::Ip4(Ipv4Addr::UNSPECIFIED))
.with(Protocol::Tcp(opt.listen_port)),
)?;

swarm.dial(
DialOpts::unknown_peer_id()
.address(opt.server_address)
.build(),
)?;

loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => {
println!("Listening on {address:?}");
}
SwarmEvent::Behaviour(BehaviourEvent::Autonat(autonat::v2::client::Event {
server,
tested_addr,
bytes_sent,
result: Ok(()),
})) => {
println!("Tested {tested_addr} with {server}. Sent {bytes_sent} bytes for verification. Everything Ok and verified.");
}
SwarmEvent::Behaviour(BehaviourEvent::Autonat(autonat::v2::client::Event {
server,
tested_addr,
bytes_sent,
result: Err(e),
})) => {
println!("Tested {tested_addr} with {server}. Sent {bytes_sent} bytes for verification. Failed with {e:?}.");
}
SwarmEvent::ExternalAddrConfirmed { address } => {
println!("External address confirmed: {address}");
}
_ => {}
}
}
}

#[derive(NetworkBehaviour)]
pub struct Behaviour {
autonat: autonat::v2::client::Behaviour,
identify: identify::Behaviour,
}

impl Behaviour {
pub fn new(key: identity::PublicKey, probe_interval: u64) -> Self {
Self {
autonat: autonat::v2::client::Behaviour::new(
OsRng,
autonat::v2::client::Config::default()
.with_probe_interval(Duration::from_secs(probe_interval)),
),
identify: identify::Behaviour::new(identify::Config::new("/ipfs/0.1.0".into(), key)),
}
}
}
87 changes: 87 additions & 0 deletions examples/autonatv2/src/bin/autonatv2_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::{error::Error, net::Ipv4Addr, time::Duration};

use cfg_if::cfg_if;
use clap::Parser;
use libp2p::{
autonat,
futures::StreamExt,
identify, identity,
multiaddr::Protocol,
noise,
swarm::{NetworkBehaviour, SwarmEvent},
tcp, yamux, Multiaddr, SwarmBuilder,
};
use rand::rngs::OsRng;

#[derive(Debug, Parser)]
#[clap(name = "libp2p autonatv2 server")]
struct Opt {
#[clap(short, long, default_value_t = 0)]
listen_port: u16,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
cfg_if! {
if #[cfg(feature = "jaeger")] {
use tracing_subscriber::layer::SubscriberExt;
use opentelemetry_sdk::runtime::Tokio;
let tracer = opentelemetry_jaeger::new_agent_pipeline()
.with_endpoint("jaeger:6831")
.with_service_name("autonatv2")
.install_batch(Tokio)?;
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = tracing_subscriber::Registry::default()
.with(telemetry);
} else {
let subscriber = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.finish();
}
}
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

let opt = Opt::parse();

let mut swarm = SwarmBuilder::with_new_identity()
.with_tokio()
.with_tcp(
tcp::Config::default(),
noise::Config::new,
yamux::Config::default,
)?
.with_quic()
.with_dns()?
.with_behaviour(|key| Behaviour::new(key.public()))?
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(60)))
.build();

swarm.listen_on(
Multiaddr::empty()
.with(Protocol::Ip4(Ipv4Addr::UNSPECIFIED))
.with(Protocol::Tcp(opt.listen_port)),
)?;

loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"),
SwarmEvent::Behaviour(event) => println!("{event:?}"),
e => println!("{e:?}"),
}
}
}

#[derive(NetworkBehaviour)]
pub struct Behaviour {
autonat: autonat::v2::server::Behaviour,
identify: identify::Behaviour,
}

impl Behaviour {
pub fn new(key: identity::PublicKey) -> Self {
Self {
autonat: autonat::v2::server::Behaviour::new(OsRng),
identify: identify::Behaviour::new(identify::Config::new("/ipfs/0.1.0".into(), key)),
}
}
}
7 changes: 7 additions & 0 deletions protocols/autonat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
- Due to the refactor of `Transport` it's no longer required to create a seperate transport for
AutoNAT where port reuse is disabled. This information is now passed by the behaviour.
See [PR 4568](https://github.com/libp2p/rust-libp2p/pull/4568).
- Introduce the new AutoNATv2 protocol.
It's split into a client and a server part, represented in their respective modules
Features:
- The server now always dials back over a newly allocated port.
This more accurately reflects the reachability state for other peers and avoids accidental hole punching.
- The server can now test addresses different from the observed address (i.e., the connection to the server was made through a `p2p-circuit`). To mitigate against DDoS attacks, the client has to send more data to the server than the dial-back costs.
See [PR 5526](https://github.com/libp2p/rust-libp2p/pull/5526).

<!-- Update to libp2p-swarm v0.45.0 -->

Expand Down
31 changes: 23 additions & 8 deletions protocols/autonat/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,47 @@ name = "libp2p-autonat"
edition = "2021"
rust-version = { workspace = true }
description = "NAT and firewall detection for libp2p"
authors = ["David Craven <david@craven.ch>", "Elena Frank <elena.frank@protonmail.com>"]
version = "0.13.0"
authors = ["David Craven <david@craven.ch>", "Elena Frank <elena.frank@protonmail.com>", "Hannes Furmans <hannes@umgefahren.xyz>"]
license = "MIT"
repository = "https://github.com/libp2p/rust-libp2p"
keywords = ["peer-to-peer", "libp2p", "networking"]
categories = ["network-programming", "asynchronous"]


[dependencies]
async-trait = "0.1"
async-trait = { version = "0.1", optional = true }
asynchronous-codec = { workspace = true }
bytes = { version = "1", optional = true }
either = { version = "1.9.0", optional = true }
futures = { workspace = true }
futures-bounded = { workspace = true, optional = true }
futures-timer = "3.0"
web-time = { workspace = true }
web-time = { workspace = true, optional = true }
libp2p-core = { workspace = true }
libp2p-swarm = { workspace = true }
libp2p-request-response = { workspace = true }
libp2p-identity = { workspace = true }
libp2p-request-response = { workspace = true, optional = true }
libp2p-swarm = { workspace = true }
quick-protobuf = "0.8"
rand = "0.8"
tracing = { workspace = true }
quick-protobuf-codec = { workspace = true }
asynchronous-codec = { workspace = true }
rand = "0.8"
rand_core = { version = "0.6", optional = true }
thiserror = { version = "1.0.52", optional = true }
void = { version = "1", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt", "sync"]}
async-std = { version = "1.10", features = ["attributes"] }
libp2p-swarm-test = { path = "../../swarm-test" }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
libp2p-identify = { workspace = true }
libp2p-swarm = { workspace = true, features = ["macros"]}

[features]
default = ["v1", "v2"]
v1 = ["dep:libp2p-request-response", "dep:web-time", "dep:async-trait"]
v2 = ["dep:bytes", "dep:either", "dep:futures-bounded", "dep:thiserror", "dep:void", "dep:rand_core"]

# Passing arguments to the docsrs builder in order to properly document cfg's.
# More information: https://docs.rs/about/builds#cross-compiling
Expand Down
Loading

0 comments on commit 848e2e9

Please sign in to comment.