From 471d45110ca1a429ab1040ce6c7e159831258764 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:58:31 +0200 Subject: [PATCH 1/2] fix(p2p): unify TCP/QUIC transport building Collapse the four near-identical swarm builders into one transport builder parameterized by `NodeType`, so transport and address selection are made together as Charon does in `p2p.go`. This fixes a `NodeType::QUIC` server running `QuicUpgradeBehaviour` with `quic_enabled = false` and installing both transports regardless of node type, and lets the relay server pass `NodeType::QUIC` (as Charon's relay does) instead of `NodeType::TCP` plus a manual UDP `listen_on` loop. Folds the six copy-paste `_tcp_`/`_udp_` helper pairs into single functions taking a `TransportProtocol`. Closes #621. Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- crates/cli/src/commands/relay.rs | 18 +- crates/p2p/src/config.rs | 104 +++----- crates/p2p/src/p2p.rs | 251 ++++++++---------- crates/p2p/src/peer.rs | 10 +- crates/p2p/src/utils.rs | 90 +++---- crates/relay-server/src/p2p.rs | 11 +- crates/relay-server/src/utils.rs | 193 +++++++------- crates/relay-server/src/web.rs | 14 +- crates/relay-server/tests/http_integration.rs | 15 +- 9 files changed, 335 insertions(+), 371 deletions(-) diff --git a/crates/cli/src/commands/relay.rs b/crates/cli/src/commands/relay.rs index f6e371a1..2cf5420c 100644 --- a/crates/cli/src/commands/relay.rs +++ b/crates/cli/src/commands/relay.rs @@ -399,8 +399,20 @@ mod tests { assert_eq!(enr.ip(), Some(Ipv4Addr::new(222, 222, 222, 222))); // The external IP is advertised on the ports libp2p bound, not on the // port 0 that was configured — which would be undialable. - assert_eq!(enr.tcp(), Some(relay.p2p_port(pluto_p2p::utils::tcp_port))); - assert_eq!(enr.udp(), Some(relay.p2p_port(pluto_p2p::utils::udp_port))); + assert_eq!( + enr.tcp(), + Some(relay.p2p_port(|addr| pluto_p2p::utils::addr_port( + addr, + pluto_p2p::utils::TransportProtocol::Tcp + ))) + ); + assert_eq!( + enr.udp(), + Some(relay.p2p_port(|addr| pluto_p2p::utils::addr_port( + addr, + pluto_p2p::utils::TransportProtocol::Quic + ))) + ); } #[tokio::test] @@ -701,7 +713,7 @@ mod tests { } /// Port of the relay's libp2p listen address selected by `port_of`, - /// e.g. [`pluto_p2p::utils::tcp_port`]. + /// e.g. [`pluto_p2p::utils::addr_port`]. fn p2p_port(&self, port_of: impl Fn(&libp2p::Multiaddr) -> Option) -> u16 { self.p2p_addrs .iter() diff --git a/crates/p2p/src/config.rs b/crates/p2p/src/config.rs index 2ebd72c1..f568ac8e 100644 --- a/crates/p2p/src/config.rs +++ b/crates/p2p/src/config.rs @@ -10,6 +10,8 @@ use std::{ use libp2p::{Multiaddr, multiaddr, ping}; use url::Url; +use crate::utils::TransportProtocol; + /// Shared default relay endpoints used by commands and P2P-facing configs. pub const DEFAULT_RELAYS: [&str; 5] = [ "https://pluto-relay-0.ovh.dev-nethermind.xyz", @@ -173,28 +175,25 @@ pub struct P2PConfig { } impl P2PConfig { - /// Returns the TCP addresses of the node. - pub fn parse_tcp_addrs(&self) -> Result> { - self.tcp_addrs.iter().map(resolve_listen_tcp_addr).collect() - } - - /// Returns the UDP addresses of the node. - pub fn parse_udp_addrs(&self) -> Result> { - self.udp_addrs.iter().map(resolve_listen_udp_addr).collect() - } - - /// Returns the UDP multiaddresses of the node. - pub fn udp_multiaddrs(&self) -> Result> { - let addrs = self.parse_udp_addrs()?; + /// Returns the configured listen addresses for `proto`. + pub fn parse_addrs(&self, proto: TransportProtocol) -> Result> { + let configured = match proto { + TransportProtocol::Tcp => &self.tcp_addrs, + TransportProtocol::Quic => &self.udp_addrs, + }; - addrs.into_iter().map(multi_addr_from_ip_udp_port).collect() + configured + .iter() + .map(|addr| resolve_listen_addr(addr, proto)) + .collect() } - /// Returns the TCP multiaddresses of the node. - pub fn tcp_multiaddrs(&self) -> Result> { - let addrs = self.parse_tcp_addrs()?; - - addrs.into_iter().map(multi_addr_from_ip_tcp_port).collect() + /// Returns the configured listen multiaddresses for `proto`. + pub fn multiaddrs(&self, proto: TransportProtocol) -> Result> { + self.parse_addrs(proto)? + .into_iter() + .map(|addr| multi_addr_from_socket_addr(addr, proto)) + .collect() } /// Returns a new builder for configuring a P2P configuration. @@ -279,54 +278,33 @@ pub fn default_ping_config() -> ping::Config { .with_timeout(DEFAULT_PING_TIMEOUT) } -/// Resolves a TCP address string to a [`SocketAddr`]. -fn resolve_listen_tcp_addr(addr: impl AsRef) -> Result { - let socket_addr: SocketAddr = addr - .as_ref() - .parse() - .map_err(P2PConfigError::FailedToParseTcpAddresses)?; +/// Resolves a `proto` listen address string to a [`SocketAddr`]. +fn resolve_listen_addr(addr: impl AsRef, proto: TransportProtocol) -> Result { + let socket_addr: SocketAddr = addr.as_ref().parse().map_err(match proto { + TransportProtocol::Tcp => P2PConfigError::FailedToParseTcpAddresses, + TransportProtocol::Quic => P2PConfigError::FailedToParseUdpAddresses, + })?; Ok(socket_addr) } -/// Resolves a UDP address string to a [`SocketAddr`]. -fn resolve_listen_udp_addr(addr: impl AsRef) -> Result { - let socket_addr: SocketAddr = addr - .as_ref() - .parse() - .map_err(P2PConfigError::FailedToParseUdpAddresses)?; - - Ok(socket_addr) -} - -pub(crate) fn multi_addr_from_ip_udp_port(socket_addr: SocketAddr) -> Result { +/// Renders `socket_addr` as a `proto` multiaddr. +pub(crate) fn multi_addr_from_socket_addr( + socket_addr: SocketAddr, + proto: TransportProtocol, +) -> Result { let typ = match socket_addr.ip() { IpAddr::V4(_) => "ip4", IpAddr::V6(_) => "ip6", }; - Multiaddr::from_str(&format!( - "/{}/{}/udp/{}/quic-v1", - typ, - socket_addr.ip(), - socket_addr.port() - )) - .map_err(P2PConfigError::FailedToParseMultiaddr) -} - -pub(crate) fn multi_addr_from_ip_tcp_port(socket_addr: SocketAddr) -> Result { - 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!( - "/{}/{}/tcp/{}", - typ, - socket_addr.ip(), - socket_addr.port() - )) - .map_err(P2PConfigError::FailedToParseMultiaddr) + Multiaddr::from_str(&format!("/{}/{}/{}", typ, socket_addr.ip(), transport)) + .map_err(P2PConfigError::FailedToParseMultiaddr) } #[cfg(test)] @@ -337,13 +315,13 @@ mod tests { #[test] fn resolve_listen_addr_p2p_bind_tcp_ip_not_specified() { - let err = resolve_listen_tcp_addr(":1234").unwrap_err(); + let err = resolve_listen_addr(":1234", TransportProtocol::Tcp).unwrap_err(); assert!(matches!(err, P2PConfigError::FailedToParseTcpAddresses(_))); } #[test] fn resolve_listen_addr_ip() { - let addr = resolve_listen_tcp_addr("10.4.3.3:1234").unwrap(); + let addr = resolve_listen_addr("10.4.3.3:1234", TransportProtocol::Tcp).unwrap(); assert_eq!( addr, SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 4, 3, 3)), 1234) @@ -352,13 +330,13 @@ mod tests { #[test] fn resolve_listen_addr_all_interfaces() { - let tcp_addr = resolve_listen_tcp_addr("0.0.0.0:0").unwrap(); + let tcp_addr = resolve_listen_addr("0.0.0.0:0", TransportProtocol::Tcp).unwrap(); assert_eq!( tcp_addr, SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) ); - let udp_addr = resolve_listen_udp_addr("0.0.0.0:0").unwrap(); + let udp_addr = resolve_listen_addr("0.0.0.0:0", TransportProtocol::Quic).unwrap(); assert_eq!( udp_addr, SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) @@ -381,8 +359,8 @@ mod tests { ..Default::default() }; - let tcp_multiaddrs = config.tcp_multiaddrs().unwrap(); - let udp_multiaddrs = config.udp_multiaddrs().unwrap(); + let tcp_multiaddrs = config.multiaddrs(TransportProtocol::Tcp).unwrap(); + let udp_multiaddrs = config.multiaddrs(TransportProtocol::Quic).unwrap(); let tcp_addrs_str = tcp_multiaddrs .iter() @@ -515,6 +493,6 @@ mod tests { ..Default::default() }; - assert!(config.tcp_multiaddrs().is_err()); + assert!(config.multiaddrs(TransportProtocol::Tcp).is_err()); } } diff --git a/crates/p2p/src/p2p.rs b/crates/p2p/src/p2p.rs index 89ae58e4..74905734 100644 --- a/crates/p2p/src/p2p.rs +++ b/crates/p2p/src/p2p.rs @@ -95,10 +95,15 @@ use std::{ use futures::{Stream, StreamExt, stream::FusedStream}; use libp2p::{ Multiaddr, PeerId, Swarm, SwarmBuilder, autonat, - core::transport::ListenerId, - identify, + core::{ + Transport as _, + muxing::StreamMuxerBox, + transport::{Boxed, ListenerId, OptionalTransport}, + upgrade::Version, + }, + dns, identify, identity::Keypair, - noise, ping, relay, + noise, ping, quic, relay, swarm::{ListenError, NetworkBehaviour, SwarmEvent}, tcp, yamux, }; @@ -110,7 +115,7 @@ use crate::{ metrics::P2P_METRICS, name::peer_name, p2p_context::P2PContext, - utils, + utils::{self, TransportProtocol}, }; const YAMUX_MAX_NUM_STREAMS: usize = 2_048; @@ -165,10 +170,6 @@ pub enum P2PError { #[error("Failed to configure DNS transport: {0}")] FailedToConfigureDns(Box), - /// Failed to configure TCP transport (includes Noise and Yamux). - #[error("Failed to configure TCP transport: {0}")] - FailedToConfigureTcp(Box), - /// Failed to configure relay client. #[error("Failed to configure relay client: {0}")] FailedToConfigureRelayClient(Box), @@ -191,11 +192,6 @@ impl P2PError { Self::FailedToConfigureDns(Box::new(error)) } - /// Failed to configure TCP transport. - pub fn failed_to_configure_tcp(error: impl std::error::Error + Send + Sync + 'static) -> Self { - Self::FailedToConfigureTcp(Box::new(error)) - } - /// Failed to configure relay client. pub fn failed_to_configure_relay_client( error: impl std::error::Error + Send + Sync + 'static, @@ -222,6 +218,65 @@ pub enum NodeType { QUIC, } +impl NodeType { + /// Whether this node runs the QUIC transport alongside TCP. + /// + /// Mirrors Charon, which chains the QUIC transport onto the TCP one for + /// `NodeTypeQUIC` and installs TCP alone otherwise — QUIC nodes accommodate + /// both kinds of connection. + pub fn quic_enabled(self) -> bool { + matches!(self, NodeType::QUIC) + } + + /// The transports this node listens on and advertises, TCP first. + fn transports(self) -> &'static [TransportProtocol] { + if self.quic_enabled() { + &[TransportProtocol::Tcp, TransportProtocol::Quic] + } else { + &[TransportProtocol::Tcp] + } + } +} + +/// Builds the transport stack for `node_type`. +/// +/// TCP (Noise + Yamux) is always installed; QUIC is chained onto it only for +/// [`NodeType::QUIC`], matching Charon. The result is resolved through the +/// system DNS resolver and, when `bandwidth` is set, metered per peer. +fn base_transport( + keypair: &Keypair, + node_type: NodeType, + bandwidth: Option, +) -> Result> { + let tcp_transport = tcp::tokio::Transport::new(tcp::Config::default()) + .upgrade(Version::V1Lazy) + .authenticate(noise::Config::new(keypair).map_err(P2PError::failed_to_configure_noise)?) + .multiplex(yamux_config()) + .map(|(p, c), _| (p, StreamMuxerBox::new(c))); + + let quic_transport = if node_type.quic_enabled() { + OptionalTransport::some( + quic::tokio::Transport::new(quic::Config::new(keypair)) + .map(|(peer_id, conn), _| (peer_id, StreamMuxerBox::new(conn))), + ) + } else { + OptionalTransport::none() + }; + + let combined = tcp_transport + .or_transport(quic_transport) + .map(|either, _| either.into_inner()); + + let dns = dns::tokio::Transport::system(combined).map_err(P2PError::failed_to_configure_dns)?; + + Ok(match bandwidth { + Some(factory) => crate::bandwidth::PeerBandwidthTransport::new(dns, factory) + .map(|(peer_id, conn), _| (peer_id, StreamMuxerBox::new(conn))) + .boxed(), + None => dns.boxed(), + }) +} + /// Node. pub struct Node { /// Swarm. @@ -290,10 +345,7 @@ impl Node { Self::bind_local_peer_id(&p2p_context, keypair.public().to_peer_id())?; init_ping_metrics(&p2p_context); - let mut node = match node_type { - NodeType::TCP => Self::build_tcp_client(keypair, p2p_context, behaviour_fn), - NodeType::QUIC => Self::build_quic_client(keypair, p2p_context, behaviour_fn), - }?; + let mut node = Self::build_client(keypair, node_type, p2p_context, behaviour_fn)?; node.apply_config(&cfg, filter_private_addrs)?; @@ -333,29 +385,31 @@ impl Node { // No-op for a relay server, which tracks no cluster peers. init_ping_metrics(&p2p_context); - let mut node = match node_type { - NodeType::TCP => Self::build_tcp_server(keypair, p2p_context, bandwidth, behaviour_fn), - NodeType::QUIC => { - Self::build_quic_server(keypair, p2p_context, bandwidth, behaviour_fn) - } - }?; + let mut node = + Self::build_server(keypair, node_type, p2p_context, bandwidth, behaviour_fn)?; node.apply_config(&cfg, filter_private_addrs)?; Ok(node) } + /// Listens on and advertises 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. fn apply_config(&mut self, cfg: &P2PConfig, filter_private_addrs: bool) -> Result<()> { - let mut addrs = cfg.tcp_multiaddrs()?; + let mut addrs = Vec::new(); - if self.node_type == NodeType::QUIC { - let udp_addrs = cfg.udp_multiaddrs()?; + for &proto in self.node_type.transports() { + let proto_addrs = cfg.multiaddrs(proto)?; - if udp_addrs.is_empty() { + if proto_addrs.is_empty() && proto == TransportProtocol::Quic { warn!("LibP2P QUIC is enabled, but no UDP addresses are configured"); } - addrs.extend(udp_addrs); + addrs.extend(proto_addrs); } if addrs.is_empty() { @@ -424,8 +478,11 @@ impl Node { } } - fn build_quic_client( + /// Builds a client node: [`base_transport`] for `node_type`, plus the relay + /// client transport and behaviour. + fn build_client( keypair: Keypair, + node_type: NodeType, p2p_context: P2PContext, behaviour_fn: F, ) -> Result @@ -436,18 +493,18 @@ impl Node { relay::client::Behaviour, ) -> PlutoBehaviourBuilder, { + // Only the relay server meters bandwidth per peer. + let transport = base_transport(&keypair, node_type, None)?; + let swarm = SwarmBuilder::with_existing_identity(keypair) .with_tokio() - .with_tcp(tcp::Config::default(), noise::Config::new, yamux_config) - .map_err(P2PError::failed_to_configure_tcp)? - .with_quic() - .with_dns() - .map_err(P2PError::failed_to_configure_dns)? + .with_other_transport(move |_| transport) + .expect("returning an already built transport cannot fail") .with_relay_client(noise::Config::new, yamux_config) .map_err(P2PError::failed_to_configure_relay_client)? .with_behaviour(|key, relay_client| { - let builder = - PlutoBehaviourBuilder::new(p2p_context.clone()).with_quic_enabled(true); + let builder = PlutoBehaviourBuilder::new(p2p_context.clone()) + .with_quic_enabled(node_type.quic_enabled()); behaviour_fn(builder, key, relay_client).build(key) }) .map_err(P2PError::failed_to_build_behaviour)? @@ -456,35 +513,34 @@ impl Node { Ok(Node { swarm, - node_type: NodeType::QUIC, + node_type, p2p_context, listener_ids: Vec::new(), }) } - fn build_tcp_client( + /// Builds a server node: [`base_transport`] for `node_type`, optionally + /// metered per peer, and no relay client. + fn build_server( keypair: Keypair, + node_type: NodeType, p2p_context: P2PContext, + bandwidth: Option, behaviour_fn: F, ) -> Result where - F: FnOnce( - PlutoBehaviourBuilder, - &Keypair, - relay::client::Behaviour, - ) -> PlutoBehaviourBuilder, + F: FnOnce(PlutoBehaviourBuilder, &Keypair) -> PlutoBehaviourBuilder, { + let transport = base_transport(&keypair, node_type, bandwidth)?; + let swarm = SwarmBuilder::with_existing_identity(keypair) .with_tokio() - .with_tcp(tcp::Config::default(), noise::Config::new, yamux_config) - .map_err(P2PError::failed_to_configure_tcp)? - .with_dns() - .map_err(P2PError::failed_to_configure_dns)? - .with_relay_client(noise::Config::new, yamux_config) - .map_err(P2PError::failed_to_configure_relay_client)? - .with_behaviour(|key, relay_client| { - let builder = PlutoBehaviourBuilder::new(p2p_context.clone()); - behaviour_fn(builder, key, relay_client).build(key) + .with_other_transport(move |_| transport) + .expect("returning an already built transport cannot fail") + .with_behaviour(|key| { + let builder = PlutoBehaviourBuilder::new(p2p_context.clone()) + .with_quic_enabled(node_type.quic_enabled()); + behaviour_fn(builder, key).build(key) }) .map_err(P2PError::failed_to_build_behaviour)? .with_swarm_config(utils::default_swarm_config) @@ -492,101 +548,12 @@ impl Node { Ok(Node { swarm, - node_type: NodeType::TCP, + node_type, p2p_context, listener_ids: Vec::new(), }) } - fn build_quic_server( - keypair: Keypair, - p2p_context: P2PContext, - bandwidth: Option, - behaviour_fn: F, - ) -> Result - where - F: FnOnce(PlutoBehaviourBuilder, &Keypair) -> PlutoBehaviourBuilder, - { - let swarm = - Self::build_server_swarm(keypair, p2p_context.clone(), bandwidth, behaviour_fn)?; - Ok(Node { - swarm, - node_type: NodeType::QUIC, - p2p_context, - listener_ids: Vec::new(), - }) - } - - fn build_tcp_server( - keypair: Keypair, - p2p_context: P2PContext, - bandwidth: Option, - behaviour_fn: F, - ) -> Result - where - F: FnOnce(PlutoBehaviourBuilder, &Keypair) -> PlutoBehaviourBuilder, - { - let swarm = - Self::build_server_swarm(keypair, p2p_context.clone(), bandwidth, behaviour_fn)?; - Ok(Node { - swarm, - node_type: NodeType::TCP, - p2p_context, - listener_ids: Vec::new(), - }) - } - - fn build_server_swarm( - keypair: Keypair, - p2p_context: P2PContext, - bandwidth: Option, - behaviour_fn: F, - ) -> Result>> - where - F: FnOnce(PlutoBehaviourBuilder, &Keypair) -> PlutoBehaviourBuilder, - { - use libp2p::{ - core::{Transport as _, muxing::StreamMuxerBox, upgrade::Version}, - dns, quic, - }; - let local_peer_id = keypair.public().to_peer_id(); - - let tcp_transport = tcp::tokio::Transport::new(tcp::Config::default()) - .upgrade(Version::V1Lazy) - .authenticate( - noise::Config::new(&keypair).map_err(P2PError::failed_to_configure_noise)?, - ) - .multiplex(yamux_config()) - .map(|(p, c), _| (p, StreamMuxerBox::new(c))); - - let quic_transport = quic::tokio::Transport::new(quic::Config::new(&keypair)) - .map(|(peer_id, conn), _| (peer_id, StreamMuxerBox::new(conn))); - - let combined = tcp_transport - .or_transport(quic_transport) - .map(|either, _| either.into_inner()); - - let dns = - dns::tokio::Transport::system(combined).map_err(P2PError::failed_to_configure_dns)?; - - let transport = match bandwidth { - Some(factory) => crate::bandwidth::PeerBandwidthTransport::new(dns, factory) - .map(|(peer_id, conn), _| (peer_id, StreamMuxerBox::new(conn))) - .boxed(), - None => dns.boxed(), - }; - - let behaviour = - behaviour_fn(PlutoBehaviourBuilder::new(p2p_context), &keypair).build(&keypair); - - Ok(Swarm::new( - transport, - behaviour, - local_peer_id, - utils::default_swarm_config(libp2p::swarm::Config::with_tokio_executor()), - )) - } - /// Returns the node type. pub fn node_type(&self) -> NodeType { self.node_type diff --git a/crates/p2p/src/peer.rs b/crates/p2p/src/peer.rs index 1b752c67..37453f9f 100644 --- a/crates/p2p/src/peer.rs +++ b/crates/p2p/src/peer.rs @@ -331,7 +331,10 @@ mod tests { assert!(crate::utils::is_tcp_addr(&addr), "bound {addr} is not TCP"); assert!(!crate::utils::is_quic_addr(&addr)); // Port 0 was configured, so the kernel picked the listening port. - assert!(crate::utils::tcp_port(&addr).is_some_and(|port| port != 0)); + assert!( + crate::utils::addr_port(&addr, crate::utils::TransportProtocol::Tcp) + .is_some_and(|port| port != 0) + ); } #[tokio::test] @@ -350,7 +353,10 @@ mod tests { "bound {addr} is not QUIC" ); assert!(!crate::utils::is_tcp_addr(&addr)); - assert!(crate::utils::udp_port(&addr).is_some_and(|port| port != 0)); + assert!( + crate::utils::addr_port(&addr, crate::utils::TransportProtocol::Quic) + .is_some_and(|port| port != 0) + ); } #[tokio::test] diff --git a/crates/p2p/src/utils.rs b/crates/p2p/src/utils.rs index 143f19ba..fdf646ca 100644 --- a/crates/p2p/src/utils.rs +++ b/crates/p2p/src/utils.rs @@ -27,45 +27,36 @@ use crate::{ manet::Manet, }; -/// Returns the external IP and Hostname fields as TCP multiaddrs on `ports`. +/// A transport a node can listen on and advertise. /// -/// `ports` must be the ports the node actually listens on: a configured port of -/// 0 means the kernel picks one, so the configured value would advertise -/// nothing dialable. -fn external_tcp_multiaddrs(cfg: &P2PConfig, ports: &[u16]) -> crate::p2p::Result> { - let mut resp = vec![]; - - if let Some(external_ip) = cfg.external_ip.as_ref() { - let ip = external_ip.parse::()?; - - for port in ports { - let maddr = config::multi_addr_from_ip_tcp_port(SocketAddr::new(ip, *port))?; - - resp.push(maddr); - } - } - - if let Some(external_host) = cfg.external_host.as_ref() { - for port in ports { - resp.push(multiaddr::multiaddr!(Dns(external_host), Tcp(*port))); - } - } - - Ok(resp) +/// Distinct from [`crate::p2p::NodeType`], which says which transports a node +/// installs: a QUIC node installs both. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TransportProtocol { + /// TCP, addressed as `/tcp/`. + Tcp, + /// QUIC over UDP, addressed as `/udp//quic-v1`. + Quic, } -/// Returns the external IP and Hostname fields as QUIC multiaddrs on `ports`. +/// Returns the external IP and Hostname fields as `proto` multiaddrs on +/// `ports`. /// -/// `ports` must be the ports the node actually listens on, as in -/// [`external_tcp_multiaddrs`]. -fn external_udp_multiaddrs(cfg: &P2PConfig, ports: &[u16]) -> crate::p2p::Result> { +/// `ports` must be the ports the node actually listens on: a configured port of +/// 0 means the kernel picks one, so the configured value would advertise +/// nothing dialable. +fn external_proto_multiaddrs( + cfg: &P2PConfig, + ports: &[u16], + proto: TransportProtocol, +) -> crate::p2p::Result> { let mut resp = vec![]; if let Some(external_ip) = cfg.external_ip.as_ref() { let ip = external_ip.parse::()?; for port in ports { - let maddr = config::multi_addr_from_ip_udp_port(SocketAddr::new(ip, *port))?; + let maddr = config::multi_addr_from_socket_addr(SocketAddr::new(ip, *port), proto)?; resp.push(maddr); } @@ -73,11 +64,12 @@ fn external_udp_multiaddrs(cfg: &P2PConfig, ports: &[u16]) -> crate::p2p::Result if let Some(external_host) = cfg.external_host.as_ref() { for port in ports { - resp.push(multiaddr::multiaddr!( - Dns(external_host), - Udp(*port), - QuicV1 - )); + resp.push(match proto { + TransportProtocol::Tcp => multiaddr::multiaddr!(Dns(external_host), Tcp(*port)), + TransportProtocol::Quic => { + multiaddr::multiaddr!(Dns(external_host), Udp(*port), QuicV1) + } + }); } } @@ -90,27 +82,25 @@ pub fn external_multiaddrs( cfg: &P2PConfig, listen_addrs: &[Multiaddr], ) -> crate::p2p::Result> { - let tcp_ports: Vec = listen_addrs.iter().filter_map(tcp_port).collect(); - let udp_ports: Vec = listen_addrs.iter().filter_map(udp_port).collect(); + let mut addrs = Vec::new(); - let mut addrs = external_tcp_multiaddrs(cfg, &tcp_ports)?; - addrs.extend(external_udp_multiaddrs(cfg, &udp_ports)?); + for proto in [TransportProtocol::Tcp, TransportProtocol::Quic] { + let ports: Vec = listen_addrs + .iter() + .filter_map(|addr| addr_port(addr, proto)) + .collect(); - Ok(addrs) -} + addrs.extend(external_proto_multiaddrs(cfg, &ports, proto)?); + } -/// Returns the TCP port of a multiaddr. -pub fn tcp_port(addr: &Multiaddr) -> Option { - addr.iter().find_map(|protocol| match protocol { - MaProtocol::Tcp(port) => Some(port), - _ => None, - }) + Ok(addrs) } -/// Returns the UDP port of a multiaddr. -pub fn udp_port(addr: &Multiaddr) -> Option { - addr.iter().find_map(|protocol| match protocol { - MaProtocol::Udp(port) => Some(port), +/// Returns the port `addr` carries for `proto`, if any. +pub fn addr_port(addr: &Multiaddr, proto: TransportProtocol) -> Option { + addr.iter().find_map(|protocol| match (protocol, proto) { + (MaProtocol::Tcp(port), TransportProtocol::Tcp) + | (MaProtocol::Udp(port), TransportProtocol::Quic) => Some(port), _ => None, }) } diff --git a/crates/relay-server/src/p2p.rs b/crates/relay-server/src/p2p.rs index d90a77e6..5420440f 100644 --- a/crates/relay-server/src/p2p.rs +++ b/crates/relay-server/src/p2p.rs @@ -229,11 +229,13 @@ pub async fn bind_relay(config: &Config, key: SecretKey) -> Result { sent: RELAY_METRICS.network_sent_bytes_total[&relay_labels(peer_id)].clone(), received: RELAY_METRICS.network_receive_bytes_total[&relay_labels(peer_id)].clone(), }); - // Binds the configured TCP listeners; `listen_on` below binds the UDP ones. + // A QUIC relay serves both transports: it binds the configured TCP and UDP + // listeners, and its ENR needs the UDP one. Matches Charon's relay, which + // also passes `NodeTypeQUIC`. let mut node = Node::new_server( config.p2p_config.clone(), key.clone(), - NodeType::TCP, + NodeType::QUIC, config.filter_private_addrs, // Relay servers don't track cluster peers - they serve all connections. P2PContext::default(), @@ -246,11 +248,6 @@ pub async fn bind_relay(config: &Config, key: SecretKey) -> Result { }, )?; - for udp_addr in config.p2p_config.udp_multiaddrs()? { - debug!("Listening on UDP address {}", udp_addr); - node.listen_on(udp_addr)?; - } - // First poll of the swarm, and so the first point at which this relay // services anything. Every other listener is already bound. let listen_addrs = Arc::new(RwLock::new(Vec::new())); diff --git a/crates/relay-server/src/utils.rs b/crates/relay-server/src/utils.rs index 7f7d8b36..6e279eeb 100644 --- a/crates/relay-server/src/utils.rs +++ b/crates/relay-server/src/utils.rs @@ -3,7 +3,7 @@ use std::net::Ipv4Addr; use libp2p::{Multiaddr, multiaddr::Protocol}; /// Re-export utilities from the p2p crate. -pub(crate) use pluto_p2p::utils::{is_quic_addr, is_tcp_addr}; +pub(crate) use pluto_p2p::utils::{TransportProtocol, addr_port, is_quic_addr, is_tcp_addr}; /// Returns true if the multiaddr is a public address. pub(crate) fn is_public_addr(addr: &Multiaddr) -> bool { @@ -24,86 +24,37 @@ pub(crate) fn is_public_addr(addr: &Multiaddr) -> bool { false } -/// Extracts IP and TCP port from a multiaddr. -pub(crate) fn extract_ip_and_tcp_port(addr: &Multiaddr) -> Option<(Ipv4Addr, u16)> { +/// Extracts the IPv4 address and `proto` port from a multiaddr. +pub(crate) fn extract_ip_and_port( + addr: &Multiaddr, + proto: TransportProtocol, +) -> Option<(Ipv4Addr, u16)> { let mut ip: Option = None; - let mut port: Option = None; for protocol in addr.iter() { - match protocol { - Protocol::Ip4(i) => ip = Some(i), - Protocol::Tcp(p) => port = Some(p), - _ => {} + if let Protocol::Ip4(i) = protocol { + ip = Some(i); } } - match (ip, port) { - (Some(i), Some(p)) => Some((i, p)), - _ => None, - } + Some((ip?, addr_port(addr, proto)?)) } -/// Extracts IP and UDP port from a QUIC multiaddr. -pub(crate) fn extract_ip_and_udp_port(addr: &Multiaddr) -> Option<(Ipv4Addr, u16)> { - let mut ip: Option = None; - let mut port: Option = None; - - for protocol in addr.iter() { - match protocol { - Protocol::Ip4(i) => ip = Some(i), - Protocol::Udp(p) => port = Some(p), - _ => {} - } - } - - match (ip, port) { - (Some(i), Some(p)) => Some((i, p)), - _ => None, - } -} - -/// Extracts DNS hostname and TCP port from a `/dns(4|6)//tcp/` +/// Extracts the DNS hostname and `proto` port from a `/dns(4|6)//...` /// multiaddr. -pub(crate) fn extract_dns_and_tcp_port(addr: &Multiaddr) -> Option<(String, u16)> { - let mut host: Option = None; - let mut port: Option = None; - - for protocol in addr.iter() { - match protocol { - Protocol::Dns(h) | Protocol::Dns4(h) | Protocol::Dns6(h) => { - host = Some(h.into_owned()); - } - Protocol::Tcp(p) => port = Some(p), - _ => {} - } - } - - match (host, port) { - (Some(h), Some(p)) => Some((h, p)), - _ => None, - } -} - -/// Extracts DNS hostname and UDP port from a -/// `/dns(4|6)//udp//quic-v1` multiaddr. -pub(crate) fn extract_dns_and_udp_port(addr: &Multiaddr) -> Option<(String, u16)> { +pub(crate) fn extract_dns_and_port( + addr: &Multiaddr, + proto: TransportProtocol, +) -> Option<(String, u16)> { let mut host: Option = None; - let mut port: Option = None; for protocol in addr.iter() { - match protocol { - Protocol::Dns(h) | Protocol::Dns4(h) | Protocol::Dns6(h) => { - host = Some(h.into_owned()); - } - Protocol::Udp(p) => port = Some(p), - _ => {} + if let Protocol::Dns(h) | Protocol::Dns4(h) | Protocol::Dns6(h) = protocol { + host = Some(h.into_owned()); } } - match (host, port) { - (Some(h), Some(p)) => Some((h, p)), - _ => None, - } + Some((host?, addr_port(addr, proto)?)) } #[cfg(test)] @@ -141,81 +92,129 @@ mod tests { } #[test] - fn extract_ip_and_tcp_port_happy() { + fn extract_ip_and_port_tcp_happy() { let ip = Ipv4Addr::new(1, 2, 3, 4); - let got = extract_ip_and_tcp_port(&ma("/ip4/1.2.3.4/tcp/8000")).unwrap(); + let got = + extract_ip_and_port(&ma("/ip4/1.2.3.4/tcp/8000"), TransportProtocol::Tcp).unwrap(); assert_eq!(got, (ip, 8000)); } #[test] - fn extract_ip_and_tcp_port_missing_ip() { - assert!(extract_ip_and_tcp_port(&ma("/dns/example.com/tcp/8000")).is_none()); + fn extract_ip_and_port_tcp_missing_ip() { + assert!( + extract_ip_and_port(&ma("/dns/example.com/tcp/8000"), TransportProtocol::Tcp).is_none() + ); } #[test] - fn extract_ip_and_tcp_port_missing_tcp() { - assert!(extract_ip_and_tcp_port(&ma("/ip4/1.2.3.4/udp/8000/quic-v1")).is_none()); + fn extract_ip_and_port_tcp_missing_tcp() { + assert!( + extract_ip_and_port(&ma("/ip4/1.2.3.4/udp/8000/quic-v1"), TransportProtocol::Tcp) + .is_none() + ); } #[test] - fn extract_ip_and_udp_port_quic_v1() { + fn extract_ip_and_port_quic_quic_v1() { let ip = Ipv4Addr::new(5, 6, 7, 8); - let got = extract_ip_and_udp_port(&ma("/ip4/5.6.7.8/udp/9000/quic-v1")).unwrap(); + let got = extract_ip_and_port( + &ma("/ip4/5.6.7.8/udp/9000/quic-v1"), + TransportProtocol::Quic, + ) + .unwrap(); assert_eq!(got, (ip, 9000)); } #[test] - fn extract_ip_and_udp_port_ignores_tcp() { - assert!(extract_ip_and_udp_port(&ma("/ip4/1.2.3.4/tcp/8000")).is_none()); + fn extract_ip_and_port_quic_ignores_tcp() { + assert!( + extract_ip_and_port(&ma("/ip4/1.2.3.4/tcp/8000"), TransportProtocol::Quic).is_none() + ); } #[test] - fn extract_dns_and_tcp_port_dns() { - let got = extract_dns_and_tcp_port(&ma("/dns/relay.example.com/tcp/3610")).unwrap(); + fn extract_dns_and_port_tcp_dns() { + let got = extract_dns_and_port( + &ma("/dns/relay.example.com/tcp/3610"), + TransportProtocol::Tcp, + ) + .unwrap(); assert_eq!(got, ("relay.example.com".to_string(), 3610)); } #[test] - fn extract_dns_and_tcp_port_dns4() { - let got = extract_dns_and_tcp_port(&ma("/dns4/relay.example.com/tcp/3610")).unwrap(); + fn extract_dns_and_port_tcp_dns4() { + let got = extract_dns_and_port( + &ma("/dns4/relay.example.com/tcp/3610"), + TransportProtocol::Tcp, + ) + .unwrap(); assert_eq!(got, ("relay.example.com".to_string(), 3610)); } #[test] - fn extract_dns_and_tcp_port_dns6() { - let got = extract_dns_and_tcp_port(&ma("/dns6/relay.example.com/tcp/3610")).unwrap(); + fn extract_dns_and_port_tcp_dns6() { + let got = extract_dns_and_port( + &ma("/dns6/relay.example.com/tcp/3610"), + TransportProtocol::Tcp, + ) + .unwrap(); assert_eq!(got, ("relay.example.com".to_string(), 3610)); } #[test] - fn extract_dns_and_tcp_port_skips_ip4() { - assert!(extract_dns_and_tcp_port(&ma("/ip4/1.2.3.4/tcp/3610")).is_none()); + fn extract_dns_and_port_tcp_skips_ip4() { + assert!( + extract_dns_and_port(&ma("/ip4/1.2.3.4/tcp/3610"), TransportProtocol::Tcp).is_none() + ); } #[test] - fn extract_dns_and_tcp_port_missing_tcp() { - assert!(extract_dns_and_tcp_port(&ma("/dns/relay.example.com/udp/3610/quic-v1")).is_none()); + fn extract_dns_and_port_tcp_missing_tcp() { + assert!( + extract_dns_and_port( + &ma("/dns/relay.example.com/udp/3610/quic-v1"), + TransportProtocol::Tcp + ) + .is_none() + ); } #[test] - fn extract_dns_and_udp_port_quic_v1() { - let got = extract_dns_and_udp_port(&ma("/dns/relay.example.com/udp/3610/quic-v1")).unwrap(); + fn extract_dns_and_port_quic_quic_v1() { + let got = extract_dns_and_port( + &ma("/dns/relay.example.com/udp/3610/quic-v1"), + TransportProtocol::Quic, + ) + .unwrap(); assert_eq!(got, ("relay.example.com".to_string(), 3610)); } #[test] - fn extract_dns_and_udp_port_skips_tcp() { - assert!(extract_dns_and_udp_port(&ma("/dns/relay.example.com/tcp/3610")).is_none()); + fn extract_dns_and_port_quic_skips_tcp() { + assert!( + extract_dns_and_port( + &ma("/dns/relay.example.com/tcp/3610"), + TransportProtocol::Quic + ) + .is_none() + ); } #[test] - fn extract_dns_and_udp_port_dns4_dns6() { - let got4 = - extract_dns_and_udp_port(&ma("/dns4/relay.example.com/udp/3610/quic-v1")).unwrap(); + fn extract_dns_and_port_quic_dns4_dns6() { + let got4 = extract_dns_and_port( + &ma("/dns4/relay.example.com/udp/3610/quic-v1"), + TransportProtocol::Quic, + ) + .unwrap(); assert_eq!(got4, ("relay.example.com".to_string(), 3610)); - let got6 = - extract_dns_and_udp_port(&ma("/dns6/relay.example.com/udp/3610/quic-v1")).unwrap(); + let got6 = extract_dns_and_port( + &ma("/dns6/relay.example.com/udp/3610/quic-v1"), + TransportProtocol::Quic, + ) + .unwrap(); assert_eq!(got6, ("relay.example.com".to_string(), 3610)); } @@ -226,7 +225,7 @@ mod tests { let addr: Multiaddr = format!("/ip6/{}/tcp/8000", Ipv6Addr::LOCALHOST) .parse() .unwrap(); - assert!(extract_ip_and_tcp_port(&addr).is_none()); - assert!(extract_ip_and_udp_port(&addr).is_none()); + assert!(extract_ip_and_port(&addr, TransportProtocol::Tcp).is_none()); + assert!(extract_ip_and_port(&addr, TransportProtocol::Quic).is_none()); } } diff --git a/crates/relay-server/src/web.rs b/crates/relay-server/src/web.rs index dcbdd6a8..895b4e72 100644 --- a/crates/relay-server/src/web.rs +++ b/crates/relay-server/src/web.rs @@ -235,9 +235,12 @@ pub async fn enr_handler( for addr in &sorted_addrs { if tcp_addr.is_none() && utils::is_tcp_addr(addr) { - if let Some((ip, port)) = utils::extract_ip_and_tcp_port(addr) { + if let Some((ip, port)) = + utils::extract_ip_and_port(addr, utils::TransportProtocol::Tcp) + { tcp_addr = Some((apply_ip_override(&state, ip).await, port)); - } else if let Some((_host, port)) = utils::extract_dns_and_tcp_port(addr) + } else if let Some((_host, port)) = + utils::extract_dns_and_port(addr, utils::TransportProtocol::Tcp) && let Some(resolved) = state.get_external_host_ip().await { tcp_addr = Some((resolved, port)); @@ -245,9 +248,12 @@ pub async fn enr_handler( } if udp_addr.is_none() && utils::is_quic_addr(addr) { - if let Some((ip, port)) = utils::extract_ip_and_udp_port(addr) { + if let Some((ip, port)) = + utils::extract_ip_and_port(addr, utils::TransportProtocol::Quic) + { udp_addr = Some((apply_ip_override(&state, ip).await, port)); - } else if let Some((_host, port)) = utils::extract_dns_and_udp_port(addr) + } else if let Some((_host, port)) = + utils::extract_dns_and_port(addr, utils::TransportProtocol::Quic) && let Some(resolved) = state.get_external_host_ip().await { udp_addr = Some((resolved, port)); diff --git a/crates/relay-server/tests/http_integration.rs b/crates/relay-server/tests/http_integration.rs index 3944611a..0570ecd4 100644 --- a/crates/relay-server/tests/http_integration.rs +++ b/crates/relay-server/tests/http_integration.rs @@ -14,7 +14,10 @@ use std::{net::Ipv4Addr, sync::Arc, time::Duration}; use k256::SecretKey; use libp2p::{Multiaddr, identity::Keypair}; use pluto_eth2util::enr::Record; -use pluto_p2p::{config::P2PConfig, utils::external_multiaddrs}; +use pluto_p2p::{ + config::P2PConfig, + utils::{TransportProtocol, external_multiaddrs}, +}; use rand::rngs::OsRng; use tokio::{net::TcpListener, sync::RwLock}; use tokio_util::sync::CancellationToken; @@ -51,8 +54,14 @@ async fn spawn_server( // No swarm runs here, so the configured listen addresses stand in for the // ones libp2p would report having bound. let bound_addrs = { - let mut v = p2p_config.tcp_multiaddrs().expect("tcp listen addrs"); - v.extend(p2p_config.udp_multiaddrs().expect("udp listen addrs")); + let mut v = p2p_config + .multiaddrs(TransportProtocol::Tcp) + .expect("tcp listen addrs"); + v.extend( + p2p_config + .multiaddrs(TransportProtocol::Quic) + .expect("udp listen addrs"), + ); v }; let external_addrs = external_multiaddrs(&p2p_config, &bound_addrs).expect("externals"); From 7591dc0feb85a1dbde92e1352fdaceaadf125884 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:43:52 +0200 Subject: [PATCH 2/2] test(p2p): cover the QUIC server path and fix the relay doc example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Node::new_server` is called with `NodeType::TCP` everywhere except the production relay, leaving the QUIC server path — the one that ships — uncovered. Adds tests that a QUIC server binds both a TCP and a QUIC listener, that a TCP server ignores its configured UDP address, and that a client completes a real QUIC handshake against the QUIC server. Also corrects the module doc's relay-server example, which showed `NodeType::TCP` and omitted the `bandwidth` argument. Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- crates/p2p/src/p2p.rs | 3 +- crates/p2p/tests/quic_server_node.rs | 188 +++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 crates/p2p/tests/quic_server_node.rs diff --git a/crates/p2p/src/p2p.rs b/crates/p2p/src/p2p.rs index 74905734..1c6f45ef 100644 --- a/crates/p2p/src/p2p.rs +++ b/crates/p2p/src/p2p.rs @@ -65,9 +65,10 @@ //! let node = Node::new_server( //! P2PConfig::default(), //! secret_key, -//! NodeType::TCP, +//! NodeType::QUIC, //! false, // filter_private_addrs //! P2PContext::default(), +//! None, // bandwidth //! |builder, keypair| { //! builder.with_inner( //! relay::Behaviour::new(keypair.public().to_peer_id(), relay_config) diff --git a/crates/p2p/tests/quic_server_node.rs b/crates/p2p/tests/quic_server_node.rs new file mode 100644 index 00000000..c8436ac3 --- /dev/null +++ b/crates/p2p/tests/quic_server_node.rs @@ -0,0 +1,188 @@ +//! End-to-end coverage for a *server* node built with [`NodeType::QUIC`]. +//! +//! Every [`Node::new_server`] call in the repo passes [`NodeType::TCP`] except +//! the production relay in `pluto-relay-server`, so the QUIC server path — the +//! one that actually ships — is otherwise untested. These tests drive it +//! directly: they assert the node installs *both* transports and binds a +//! listener for each configured address, that a [`NodeType::TCP`] server binds +//! only TCP even when UDP addresses are configured, and that a real client can +//! complete a QUIC handshake against the QUIC server. + +use std::time::Duration; + +use futures::StreamExt as _; +use libp2p::{Multiaddr, multiaddr::Protocol, relay, swarm::SwarmEvent}; +use pluto_p2p::{ + config::P2PConfig, + p2p::{Node, NodeType}, + p2p_context::P2PContext, + peer::peer_id_from_key, +}; +use pluto_testutil::random::generate_insecure_k1_key; +use tokio::time::timeout; + +const TEST_TIMEOUT: Duration = Duration::from_secs(20); + +/// A relay server node — the shape `pluto-relay-server` builds in production. +type ServerNode = Node; + +/// Loopback config with one TCP and one UDP address, both on kernel-assigned +/// ports. +fn loopback_config() -> P2PConfig { + P2PConfig::builder() + .with_tcp_addrs(vec!["127.0.0.1:0".to_owned()]) + .with_udp_addrs(vec!["127.0.0.1:0".to_owned()]) + .build() +} + +/// Builds a relay server node of `node_type` on [`loopback_config`]. +fn build_server(key: k256::SecretKey, node_type: NodeType) -> ServerNode { + Node::new_server( + loopback_config(), + key, + node_type, + // Keep loopback addresses: the tests connect over 127.0.0.1. + false, + // Relay servers don't track cluster peers - they serve all connections. + P2PContext::default(), + None, + |builder, keypair| { + builder.with_inner(relay::Behaviour::new( + keypair.public().to_peer_id(), + relay::Config::default(), + )) + }, + ) + .expect("build relay server node") +} + +fn is_quic(addr: &Multiaddr) -> bool { + addr.iter().any(|p| matches!(p, Protocol::QuicV1)) +} + +fn is_tcp(addr: &Multiaddr) -> bool { + addr.iter().any(|p| matches!(p, Protocol::Tcp(_))) +} + +/// Drives `node` until it has reported `want` listen addresses. +async fn listen_addrs(node: &mut ServerNode, want: usize) -> Vec { + let wait = async { + let mut addrs = Vec::with_capacity(want); + while addrs.len() < want { + if let SwarmEvent::NewListenAddr { address, .. } = node.select_next_some().await { + addrs.push(address); + } + } + addrs + }; + + timeout(TEST_TIMEOUT, wait) + .await + .expect("timed out waiting for the listen addresses") +} + +#[tokio::test] +async fn quic_server_binds_tcp_and_quic_listeners() { + let mut node = build_server(generate_insecure_k1_key(1), NodeType::QUIC); + + // `listen_on` binds before it returns, so the listener count is already + // final here: one per configured address of every installed transport. + assert_eq!( + node.listener_ids().len(), + 2, + "a QUIC server must bind both its TCP and its UDP address", + ); + + let addrs = listen_addrs(&mut node, 2).await; + + assert!( + addrs.iter().any(is_tcp), + "no TCP listen address among {addrs:?}", + ); + assert!( + addrs.iter().any(is_quic), + "no QUIC listen address among {addrs:?}", + ); +} + +#[tokio::test] +async fn tcp_server_binds_no_quic_listener() { + let mut node = build_server(generate_insecure_k1_key(2), NodeType::TCP); + + assert_eq!( + node.listener_ids().len(), + 1, + "a TCP server must ignore its configured UDP address", + ); + + let addrs = listen_addrs(&mut node, 1).await; + + assert!( + !addrs.iter().any(is_quic), + "a TCP server must not listen on QUIC, got {addrs:?}", + ); +} + +#[tokio::test] +async fn client_connects_to_quic_server_over_quic() { + let server_key = generate_insecure_k1_key(3); + let client_key = generate_insecure_k1_key(4); + + let server_peer = peer_id_from_key(server_key.public_key()).expect("derive server peer id"); + let client_peer = peer_id_from_key(client_key.public_key()).expect("derive client peer id"); + + let mut server = build_server(server_key, NodeType::QUIC); + let mut client: Node = Node::new( + P2PConfig::default(), + client_key, + NodeType::QUIC, + false, + P2PContext::new(vec![server_peer]), + |builder, _keypair, relay_client| builder.with_inner(relay_client), + ) + .expect("build production client node"); + + let quic_addr = listen_addrs(&mut server, 2) + .await + .into_iter() + .find(is_quic) + .expect("server must expose a QUIC listen address"); + + client.dial(quic_addr.clone()).expect("client dial server"); + + // Drive both swarms until the client reports the connection. Only an + // established connection proves the QUIC transport is really installed on + // the *server*: the dial address is QUIC-only, so a TCP-only server would + // never complete the handshake. + let drive = async { + loop { + tokio::select! { + event = server.select_next_some() => { + if let SwarmEvent::ConnectionEstablished { peer_id, .. } = event { + assert!( + peer_id == client_peer, + "server connected to unexpected peer {peer_id}", + ); + } + } + event = client.select_next_some() => { + if let SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } = event { + assert!( + peer_id == server_peer, + "client connected to unexpected peer {peer_id}", + ); + assert!( + is_quic(endpoint.get_remote_address()), + "connection was not negotiated over QUIC: {endpoint:?}", + ); + return; + } + } + } + } + }; + + timeout(TEST_TIMEOUT, drive) + .await + .expect("timed out before the client connected over QUIC"); +}