Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions crates/app/src/node/behaviour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ pub(crate) struct WireP2PParams {
}

/// Composes the core behaviours and builds the libp2p [`Node`].
// TODO(#402 part B): QUIC transport (featureset-gated off at v1.7.1) and
// bandwidth metrics.
// TODO(#402 part B): bandwidth metrics.
pub(crate) async fn wire_p2p(
params: WireP2PParams,
) -> Result<(Node<CoreBehaviour>, CoreHandles), AppError> {
Expand Down Expand Up @@ -215,10 +214,17 @@ pub(crate) async fn wire_p2p(
// checker observes the same shared peer/connection state the swarm updates.
let p2p_context_for_handle = p2p_context.clone();

// Mirrors Charon's `wireP2P`.
let node_type = if feature_set.enabled(pluto_featureset::Feature::Quic) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Differs from Charon. The node type decides both the transports installed and whether QuicUpgradeBehaviour acts, so a QUIC node with no --p2p-udp-address still dials peers' QUIC addresses (outbound QUIC works without a listener). Charon's upgrade loop instead re-checks every tick whether the host has a QUIC listen or advertised address and stays idle otherwise. Deliberate: the construction-time gate is simpler and strictly more capable, which is also why the unused address-based is_quic_enabled helper is gone.

NodeType::QUIC
} else {
NodeType::TCP
};

let node = Node::new(
p2p_config,
key,
NodeType::TCP,
node_type,
false,
p2p_context,
|builder, _keypair, relay_client| {
Expand Down
42 changes: 8 additions & 34 deletions crates/p2p/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
//! # Charon P2P Configuration

use std::{
fmt,
net::{IpAddr, SocketAddr},
str::FromStr,
time::Duration,
};
use std::{fmt, net::SocketAddr, str::FromStr, time::Duration};

use libp2p::{Multiaddr, multiaddr, ping};
use url::Url;

use crate::utils::TransportProtocol;
use crate::utils::{TransportProtocol, with_transport};

/// Shared default relay endpoints used by commands and P2P-facing configs.
pub const DEFAULT_RELAYS: [&str; 5] = [
Expand Down Expand Up @@ -122,10 +117,6 @@ pub enum P2PConfigError {
/// Failed to parse the UDP addresses.
#[error("Failed to parse the UDP addresses")]
FailedToParseUdpAddresses(std::net::AddrParseError),

/// Failed to parse the multiaddress.
#[error("Failed to parse the multiaddress")]
FailedToParseMultiaddr(#[from] multiaddr::Error),
}

// Note: this is only for testing purposes!
Expand All @@ -141,10 +132,6 @@ impl PartialEq for P2PConfigError {
P2PConfigError::FailedToParseUdpAddresses(x),
P2PConfigError::FailedToParseUdpAddresses(y),
) if x == y => true,
(
P2PConfigError::FailedToParseMultiaddr(x),
P2PConfigError::FailedToParseMultiaddr(y),
) if x.to_string() == y.to_string() => true,
_ => false,
}
}
Expand Down Expand Up @@ -202,10 +189,11 @@ impl P2PConfig {

/// Returns the configured listen multiaddresses for `proto`.
pub fn multiaddrs(&self, proto: TransportProtocol) -> Result<Vec<Multiaddr>> {
self.parse_addrs(proto)?
Ok(self
.parse_addrs(proto)?
.into_iter()
.map(|addr| multi_addr_from_socket_addr(addr, proto))
.collect()
.collect())
}
}

Expand Down Expand Up @@ -240,27 +228,13 @@ fn resolve_listen_addr(addr: impl AsRef<str>, proto: TransportProtocol) -> Resul
}

/// Renders `socket_addr` as a `proto` multiaddr.
pub(crate) fn multi_addr_from_socket_addr(
socket_addr: SocketAddr,
proto: TransportProtocol,
) -> Result<Multiaddr> {
let typ = match socket_addr.ip() {
IpAddr::V4(_) => "ip4",
IpAddr::V6(_) => "ip6",
};

let transport = match proto {
TransportProtocol::Tcp => format!("tcp/{}", socket_addr.port()),
TransportProtocol::Quic => format!("udp/{}/quic-v1", socket_addr.port()),
};

Multiaddr::from_str(&format!("/{}/{}/{}", typ, socket_addr.ip(), transport))
.map_err(P2PConfigError::FailedToParseMultiaddr)
fn multi_addr_from_socket_addr(socket_addr: SocketAddr, proto: TransportProtocol) -> Multiaddr {
with_transport(Multiaddr::from(socket_addr.ip()), socket_addr.port(), proto)
}

#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use super::*;

Expand Down
107 changes: 73 additions & 34 deletions crates/p2p/src/p2p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
//! Client nodes may include relay client to support connecting via relays.

use std::{
net::IpAddr,
pin::Pin,
task::{Context, Poll},
time::Duration,
Expand Down Expand Up @@ -291,6 +292,18 @@ pub struct Node<B: NetworkBehaviour> {

/// Listeners registered through [`Node::listen_on`], in registration order.
listener_ids: Vec<ListenerId>,

/// External IP to advertise on the bound ports.
external_ip: Option<IpAddr>,

/// External hostname to advertise on the bound ports.
external_host: Option<String>,

/// Whether private listen addresses are withheld from advertisement.
filter_private_addrs: bool,

/// Addresses this node currently registers as external on the swarm.
advertised_addrs: Vec<Multiaddr>,
}

impl<B: NetworkBehaviour> Node<B> {
Expand Down Expand Up @@ -394,13 +407,25 @@ impl<B: NetworkBehaviour> Node<B> {
Ok(node)
}

/// Listens on and advertises the configured addresses of every transport
/// this node's [`NodeType`] installs.
/// Listens on the configured addresses of every transport this node's
/// [`NodeType`] installs.
///
/// Transport and address selection are driven by the same
/// [`NodeType::transports`] list, so a node can never listen on a transport
/// it did not install, nor install one it never listens on.
///
/// Nothing is advertised yet: a configured port of 0 means the kernel picks
/// one, so the advertised set is derived from the addresses libp2p reports
/// as bound (see [`Node::readvertise`]).
fn apply_config(&mut self, cfg: &P2PConfig, filter_private_addrs: bool) -> Result<()> {
self.external_ip = cfg
.external_ip
.as_deref()
.map(str::parse::<IpAddr>)
.transpose()?;
self.external_host = cfg.external_host.clone();
self.filter_private_addrs = filter_private_addrs;

let mut addrs = Vec::new();

for &proto in self.node_type.transports() {
Expand All @@ -419,48 +444,51 @@ impl<B: NetworkBehaviour> Node<B> {
);
}

// Listen on internal addresses only
for addr in &addrs {
self.listen_on(addr.clone())?;
for addr in addrs {
self.listen_on(addr)?;
}

self.set_advertised_addrs(cfg, filter_private_addrs, &addrs)
Ok(())
}

/// Advertises the external IP / hostname from `cfg` on the ports of
/// `listen_addrs`, together with `listen_addrs` themselves.
///
/// Replaces everything the node advertises, including addresses added
/// through [`Node::add_external_address`].
///
/// Callers that listen on port 0 should call this again once libp2p has
/// reported the kernel-assigned ports: the configured addresses advertise
/// port 0, which is not dialable.
pub fn set_advertised_addrs(
&mut self,
cfg: &P2PConfig,
filter_private_addrs: bool,
listen_addrs: &[Multiaddr],
) -> Result<()> {
let external_addrs = utils::external_multiaddrs(cfg, listen_addrs)?;
/// Re-derives the advertised set from the listen addresses libp2p reports:
/// the external IP / hostname on their ports plus the addresses themselves,
/// private ones withheld when configured. Relay circuit listeners are
/// skipped: their ports are the relay's, and identify lists them anyway.
/// Only the difference to the previous set is applied, so addresses the
/// swarm confirmed by other means (AutoNAT) survive.
fn readvertise(&mut self) {
let listen_addrs: Vec<Multiaddr> = self
.swarm
.listeners()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-obvious. The listener table is the source of truth here because the swarm updates it before yielding NewListenAddr/ExpiredListenAddr/ListenerClosed, and Node::poll_next runs handle_event only after the swarm has yielded, so it is always current at this point. Circuit listeners are skipped because addr_port would pick up the relay's port and, with an external IP configured, advertise /ip4/<external>/tcp/<relay-port>.

.filter(|addr| !utils::is_relay_addr(addr))
.cloned()
.collect();

let external_addrs = utils::external_multiaddrs_on(
self.external_ip,
self.external_host.as_deref(),
&listen_addrs,
);

// Advertise filtered addresses (external + optionally filtered
// internal)
let advertised_addrs = utils::filter_advertised_addresses(
let next = utils::filter_advertised_addresses(
utils::ExternalAddresses(external_addrs),
utils::InternalAddresses(listen_addrs.to_vec()),
filter_private_addrs,
utils::InternalAddresses(listen_addrs),
self.filter_private_addrs,
);

for addr in self.swarm.external_addresses().cloned().collect::<Vec<_>>() {
self.swarm.remove_external_address(&addr);
for addr in &self.advertised_addrs {
if !next.contains(addr) {
self.swarm.remove_external_address(addr);
}
}

for addr in advertised_addrs {
self.swarm.add_external_address(addr);
for addr in &next {
if !self.advertised_addrs.contains(addr) {
self.swarm.add_external_address(addr.clone());
}
}

Ok(())
self.advertised_addrs = next;
}

fn bind_local_peer_id(p2p_context: &P2PContext, local_peer_id: PeerId) -> Result<()> {
Expand Down Expand Up @@ -517,6 +545,10 @@ impl<B: NetworkBehaviour> Node<B> {
node_type,
p2p_context,
listener_ids: Vec::new(),
external_ip: None,
external_host: None,
filter_private_addrs: false,
advertised_addrs: Vec::new(),
})
}

Expand Down Expand Up @@ -552,6 +584,10 @@ impl<B: NetworkBehaviour> Node<B> {
node_type,
p2p_context,
listener_ids: Vec::new(),
external_ip: None,
external_host: None,
filter_private_addrs: false,
advertised_addrs: Vec::new(),
})
}

Expand Down Expand Up @@ -669,13 +705,16 @@ impl<B: NetworkBehaviour> Node<B> {
}
}

// Listen address changes
// Listener changes drive what the node advertises.
SwarmEvent::NewListenAddr { address, .. } => {
info!(%address, "listening on new address");
self.readvertise();
}
SwarmEvent::ExpiredListenAddr { address, .. } => {
info!(%address, "listen address expired");
self.readvertise();
}
SwarmEvent::ListenerClosed { .. } => self.readvertise(),

// External address discovery
SwarmEvent::ExternalAddrConfirmed { address } => {
Expand Down
Loading
Loading