Skip to content

Commit 593ac97

Browse files
umgefahrenTimTinkers
authored andcommitted
feat(autonat): Implement AutoNATv2
Closes: libp2p#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 libp2p#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: libp2p#5526.
1 parent f7d7119 commit 593ac97

34 files changed

+3525
-80
lines changed

Cargo.lock

Lines changed: 196 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
members = [
33
"core",
44
"examples/autonat",
5+
"examples/autonatv2",
56
"examples/browser-webrtc",
67
"examples/chat",
78
"examples/dcutr",

examples/autonatv2/Cargo.toml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[package]
2+
name = "autonatv2"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
license = "MIT or Apache-2.0"
7+
8+
[package.metadata.release]
9+
release = false
10+
11+
[[bin]]
12+
name = "autonatv2_client"
13+
14+
[[bin]]
15+
name = "autonatv2_server"
16+
17+
[dependencies]
18+
libp2p = { workspace = true, features = ["macros", "tokio", "tcp", "noise", "yamux", "autonat", "identify", "dns", "quic"] }
19+
clap = { version = "4.4.18", features = ["derive"] }
20+
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
21+
tracing = "0.1.40"
22+
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
23+
rand = "0.8.5"
24+
opentelemetry = { version = "0.21.0", optional = true }
25+
opentelemetry_sdk = { version = "0.21.1", optional = true, features = ["rt-tokio"] }
26+
tracing-opentelemetry = { version = "0.22.0", optional = true }
27+
opentelemetry-jaeger = { version = "0.20.0", optional = true, features = ["rt-tokio"] }
28+
cfg-if = "1.0.0"
29+
30+
[features]
31+
jaeger = ["opentelemetry", "opentelemetry_sdk", "tracing-opentelemetry", "opentelemetry-jaeger"]
32+
opentelemetry = ["dep:opentelemetry"]
33+
opentelemetry_sdk = ["dep:opentelemetry_sdk"]
34+
tracing-opentelemetry = ["dep:tracing-opentelemetry"]
35+
opentelemetry-jaeger = ["dep:opentelemetry-jaeger"]
36+
37+
[lints]
38+
workspace = true

examples/autonatv2/Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM rust:1.75-alpine as builder
2+
3+
RUN apk add musl-dev
4+
5+
WORKDIR /workspace
6+
COPY . .
7+
RUN --mount=type=cache,target=./target \
8+
--mount=type=cache,target=/usr/local/cargo/registry \
9+
cargo build --release --package autonatv2 --bin autonatv2_server -F jaeger
10+
11+
RUN --mount=type=cache,target=./target \
12+
mv ./target/release/autonatv2_server /usr/local/bin/autonatv2_server
13+
14+
FROM alpine:latest
15+
16+
COPY --from=builder /usr/local/bin/autonatv2_server /app/autonatv2_server
17+
18+
EXPOSE 4884
19+
20+
ENTRYPOINT [ "/app/autonatv2_server", "-l", "4884" ]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: '3'
2+
3+
services:
4+
autonatv2:
5+
build:
6+
context: ../..
7+
dockerfile: examples/autonatv2/Dockerfile
8+
ports:
9+
- 4884:4884
10+
jaeger:
11+
image: jaegertracing/all-in-one
12+
ports:
13+
- 6831:6831/udp
14+
- 6832:6832/udp
15+
- 16686:16686
16+
- 14268:14268
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use std::{error::Error, net::Ipv4Addr, time::Duration};
2+
3+
use clap::Parser;
4+
use libp2p::{
5+
autonat,
6+
futures::StreamExt,
7+
identify, identity,
8+
multiaddr::Protocol,
9+
noise,
10+
swarm::{dial_opts::DialOpts, NetworkBehaviour, SwarmEvent},
11+
tcp, yamux, Multiaddr, SwarmBuilder,
12+
};
13+
use rand::rngs::OsRng;
14+
use tracing_subscriber::EnvFilter;
15+
16+
#[derive(Debug, Parser)]
17+
#[clap(name = "libp2p autonatv2 client")]
18+
struct Opt {
19+
/// Port where the client will listen for incoming connections.
20+
#[clap(short = 'p', long, default_value_t = 0)]
21+
listen_port: u16,
22+
23+
/// Address of the server where want to connect to.
24+
#[clap(short = 'a', long)]
25+
server_address: Multiaddr,
26+
27+
/// Probe interval in seconds.
28+
#[clap(short = 't', long, default_value = "2")]
29+
probe_interval: u64,
30+
}
31+
32+
#[tokio::main]
33+
async fn main() -> Result<(), Box<dyn Error>> {
34+
let _ = tracing_subscriber::fmt()
35+
.with_env_filter(EnvFilter::from_default_env())
36+
.try_init();
37+
38+
let opt = Opt::parse();
39+
40+
let mut swarm = SwarmBuilder::with_new_identity()
41+
.with_tokio()
42+
.with_tcp(
43+
tcp::Config::default(),
44+
noise::Config::new,
45+
yamux::Config::default,
46+
)?
47+
.with_quic()
48+
.with_dns()?
49+
.with_behaviour(|key| Behaviour::new(key.public(), opt.probe_interval))?
50+
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(10)))
51+
.build();
52+
53+
swarm.listen_on(
54+
Multiaddr::empty()
55+
.with(Protocol::Ip4(Ipv4Addr::UNSPECIFIED))
56+
.with(Protocol::Tcp(opt.listen_port)),
57+
)?;
58+
59+
swarm.dial(
60+
DialOpts::unknown_peer_id()
61+
.address(opt.server_address)
62+
.build(),
63+
)?;
64+
65+
loop {
66+
match swarm.select_next_some().await {
67+
SwarmEvent::NewListenAddr { address, .. } => {
68+
println!("Listening on {address:?}");
69+
}
70+
SwarmEvent::Behaviour(BehaviourEvent::Autonat(autonat::v2::client::Event {
71+
server,
72+
tested_addr,
73+
bytes_sent,
74+
result: Ok(()),
75+
})) => {
76+
println!("Tested {tested_addr} with {server}. Sent {bytes_sent} bytes for verification. Everything Ok and verified.");
77+
}
78+
SwarmEvent::Behaviour(BehaviourEvent::Autonat(autonat::v2::client::Event {
79+
server,
80+
tested_addr,
81+
bytes_sent,
82+
result: Err(e),
83+
})) => {
84+
println!("Tested {tested_addr} with {server}. Sent {bytes_sent} bytes for verification. Failed with {e:?}.");
85+
}
86+
SwarmEvent::ExternalAddrConfirmed { address } => {
87+
println!("External address confirmed: {address}");
88+
}
89+
_ => {}
90+
}
91+
}
92+
}
93+
94+
#[derive(NetworkBehaviour)]
95+
pub struct Behaviour {
96+
autonat: autonat::v2::client::Behaviour,
97+
identify: identify::Behaviour,
98+
}
99+
100+
impl Behaviour {
101+
pub fn new(key: identity::PublicKey, probe_interval: u64) -> Self {
102+
Self {
103+
autonat: autonat::v2::client::Behaviour::new(
104+
OsRng,
105+
autonat::v2::client::Config::default()
106+
.with_probe_interval(Duration::from_secs(probe_interval)),
107+
),
108+
identify: identify::Behaviour::new(identify::Config::new("/ipfs/0.1.0".into(), key)),
109+
}
110+
}
111+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::{error::Error, net::Ipv4Addr, time::Duration};
2+
3+
use cfg_if::cfg_if;
4+
use clap::Parser;
5+
use libp2p::{
6+
autonat,
7+
futures::StreamExt,
8+
identify, identity,
9+
multiaddr::Protocol,
10+
noise,
11+
swarm::{NetworkBehaviour, SwarmEvent},
12+
tcp, yamux, Multiaddr, SwarmBuilder,
13+
};
14+
use rand::rngs::OsRng;
15+
16+
#[derive(Debug, Parser)]
17+
#[clap(name = "libp2p autonatv2 server")]
18+
struct Opt {
19+
#[clap(short, long, default_value_t = 0)]
20+
listen_port: u16,
21+
}
22+
23+
#[tokio::main]
24+
async fn main() -> Result<(), Box<dyn Error>> {
25+
cfg_if! {
26+
if #[cfg(feature = "jaeger")] {
27+
use tracing_subscriber::layer::SubscriberExt;
28+
use opentelemetry_sdk::runtime::Tokio;
29+
let tracer = opentelemetry_jaeger::new_agent_pipeline()
30+
.with_endpoint("jaeger:6831")
31+
.with_service_name("autonatv2")
32+
.install_batch(Tokio)?;
33+
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
34+
let subscriber = tracing_subscriber::Registry::default()
35+
.with(telemetry);
36+
} else {
37+
let subscriber = tracing_subscriber::fmt()
38+
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
39+
.finish();
40+
}
41+
}
42+
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
43+
44+
let opt = Opt::parse();
45+
46+
let mut swarm = SwarmBuilder::with_new_identity()
47+
.with_tokio()
48+
.with_tcp(
49+
tcp::Config::default(),
50+
noise::Config::new,
51+
yamux::Config::default,
52+
)?
53+
.with_quic()
54+
.with_dns()?
55+
.with_behaviour(|key| Behaviour::new(key.public()))?
56+
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(60)))
57+
.build();
58+
59+
swarm.listen_on(
60+
Multiaddr::empty()
61+
.with(Protocol::Ip4(Ipv4Addr::UNSPECIFIED))
62+
.with(Protocol::Tcp(opt.listen_port)),
63+
)?;
64+
65+
loop {
66+
match swarm.select_next_some().await {
67+
SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"),
68+
SwarmEvent::Behaviour(event) => println!("{event:?}"),
69+
e => println!("{e:?}"),
70+
}
71+
}
72+
}
73+
74+
#[derive(NetworkBehaviour)]
75+
pub struct Behaviour {
76+
autonat: autonat::v2::server::Behaviour,
77+
identify: identify::Behaviour,
78+
}
79+
80+
impl Behaviour {
81+
pub fn new(key: identity::PublicKey) -> Self {
82+
Self {
83+
autonat: autonat::v2::server::Behaviour::new(OsRng),
84+
identify: identify::Behaviour::new(identify::Config::new("/ipfs/0.1.0".into(), key)),
85+
}
86+
}
87+
}

protocols/autonat/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
- Due to the refactor of `Transport` it's no longer required to create a seperate transport for
44
AutoNAT where port reuse is disabled. This information is now passed by the behaviour.
55
See [PR 4568](https://github.com/libp2p/rust-libp2p/pull/4568).
6+
- Introduce the new AutoNATv2 protocol.
7+
It's split into a client and a server part, represented in their respective modules
8+
Features:
9+
- The server now always dials back over a newly allocated port.
10+
This more accurately reflects the reachability state for other peers and avoids accidental hole punching.
11+
- 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.
12+
See [PR 5526](https://github.com/libp2p/rust-libp2p/pull/5526).
613

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

protocols/autonat/Cargo.toml

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,47 @@ name = "libp2p-autonat"
33
edition = "2021"
44
rust-version = { workspace = true }
55
description = "NAT and firewall detection for libp2p"
6-
authors = ["David Craven <[email protected]>", "Elena Frank <[email protected]>"]
76
version = "0.13.0"
7+
authors = ["David Craven <[email protected]>", "Elena Frank <[email protected]>", "Hannes Furmans <[email protected]>"]
88
license = "MIT"
99
repository = "https://github.com/libp2p/rust-libp2p"
1010
keywords = ["peer-to-peer", "libp2p", "networking"]
1111
categories = ["network-programming", "asynchronous"]
1212

13+
1314
[dependencies]
14-
async-trait = "0.1"
15+
async-trait = { version = "0.1", optional = true }
16+
asynchronous-codec = { workspace = true }
17+
bytes = { version = "1", optional = true }
18+
either = { version = "1.9.0", optional = true }
1519
futures = { workspace = true }
20+
futures-bounded = { workspace = true, optional = true }
1621
futures-timer = "3.0"
17-
web-time = { workspace = true }
22+
web-time = { workspace = true, optional = true }
1823
libp2p-core = { workspace = true }
19-
libp2p-swarm = { workspace = true }
20-
libp2p-request-response = { workspace = true }
2124
libp2p-identity = { workspace = true }
25+
libp2p-request-response = { workspace = true, optional = true }
26+
libp2p-swarm = { workspace = true }
2227
quick-protobuf = "0.8"
23-
rand = "0.8"
2428
tracing = { workspace = true }
2529
quick-protobuf-codec = { workspace = true }
26-
asynchronous-codec = { workspace = true }
30+
rand = "0.8"
31+
rand_core = { version = "0.6", optional = true }
32+
thiserror = { version = "1.0.52", optional = true }
33+
void = { version = "1", optional = true }
2734

2835
[dev-dependencies]
36+
tokio = { version = "1", features = ["macros", "rt", "sync"]}
2937
async-std = { version = "1.10", features = ["attributes"] }
3038
libp2p-swarm-test = { path = "../../swarm-test" }
31-
tracing-subscriber = { workspace = true, features = ["env-filter"] }
39+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
40+
libp2p-identify = { workspace = true }
41+
libp2p-swarm = { workspace = true, features = ["macros"]}
42+
43+
[features]
44+
default = ["v1", "v2"]
45+
v1 = ["dep:libp2p-request-response", "dep:web-time", "dep:async-trait"]
46+
v2 = ["dep:bytes", "dep:either", "dep:futures-bounded", "dep:thiserror", "dep:void", "dep:rand_core"]
3247

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

0 commit comments

Comments
 (0)