Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/actions/run-bash-hole-punch-test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ inputs:
description: "Ignore routers (pipe-separated substrings)"
required: false
default: ""
transport-ignore:
description: "Ignore transports (pipe-separated patterns). Default excludes ws and webrtc-direct which are incompatible with DCUtR hole-punching."
required: false
default: "ws|webrtc-direct"
cache-dir:
description: "Local directory to use for build cache"
required: false
Expand Down Expand Up @@ -99,6 +103,7 @@ runs:
--relay-ignore '${{ inputs.relay-ignore }}' \
--router-select '${{ inputs.router-select }}' \
--router-ignore '${{ inputs.router-ignore }}' \
--transport-ignore '${{ inputs.transport-ignore }}' \
--cache-dir '${{ inputs.cache-dir }}' \
$SNAPSHOT_FLAG \
$EXPORT_DOCKER_FLAG \
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/daily-full-interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ on:
required: false
default: '~failing'
type: string
hole-punch-transport-ignore:
description: '[Hole-punch] Ignore transports (pipe-separated patterns)'
required: false
default: 'ws|webrtc-direct'
type: string
hole-punch-force-matrix-rebuild:
description: '[Hole-punch] Force test matrix regeneration'
required: false
Expand Down Expand Up @@ -200,6 +205,7 @@ jobs:
relay-ignore: ${{ steps.resolve.outputs.relay-ignore }}
router-select: ${{ steps.resolve.outputs.router-select }}
router-ignore: ${{ steps.resolve.outputs.router-ignore }}
transport-ignore: ${{ steps.resolve.outputs.transport-ignore }}
force-matrix-rebuild: ${{ steps.resolve.outputs.force-matrix-rebuild }}
force-image-rebuild: ${{ steps.resolve.outputs.force-image-rebuild }}
debug: ${{ steps.resolve.outputs.debug }}
Expand All @@ -222,6 +228,7 @@ jobs:
echo "relay-ignore=${{ github.event.inputs.hole-punch-relay-ignore }}" >> $GITHUB_OUTPUT
echo "router-select=${{ github.event.inputs.hole-punch-router-select }}" >> $GITHUB_OUTPUT
echo "router-ignore=${{ github.event.inputs.hole-punch-router-ignore }}" >> $GITHUB_OUTPUT
echo "transport-ignore=${{ github.event.inputs.hole-punch-transport-ignore }}" >> $GITHUB_OUTPUT
echo "force-matrix-rebuild=${{ github.event.inputs.hole-punch-force-matrix-rebuild }}" >> $GITHUB_OUTPUT
echo "force-image-rebuild=${{ github.event.inputs.hole-punch-force-image-rebuild }}" >> $GITHUB_OUTPUT
echo "debug=${{ github.event.inputs.hole-punch-debug }}" >> $GITHUB_OUTPUT
Expand All @@ -238,6 +245,7 @@ jobs:
echo "relay-ignore=" >> $GITHUB_OUTPUT
echo "router-select=" >> $GITHUB_OUTPUT
echo "router-ignore=" >> $GITHUB_OUTPUT
echo "transport-ignore=ws|webrtc-direct" >> $GITHUB_OUTPUT
echo "force-image-rebuild=false" >> $GITHUB_OUTPUT
echo "debug=false" >> $GITHUB_OUTPUT

Expand Down Expand Up @@ -359,6 +367,7 @@ jobs:
relay-ignore: '${{ needs.resolve-hole-punch-parameters.outputs.relay-ignore }}'
router-select: '${{ needs.resolve-hole-punch-parameters.outputs.router-select }}'
router-ignore: '${{ needs.resolve-hole-punch-parameters.outputs.router-ignore }}'
transport-ignore: '${{ needs.resolve-hole-punch-parameters.outputs.transport-ignore }}'
cache-dir: /srv/cache
snapshot: true
export-docker-images: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.export-docker-images == 'true' }}
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/hole-punch-interop-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ on:
required: false
default: ''
type: string
transport-ignore:
description: 'Ignore transports (pipe-separated patterns, e.g., "ws|webrtc-direct")'
required: false
default: 'ws|webrtc-direct'
type: string
force-matrix-rebuild:
description: 'Force test matrix regeneration (bypass cache)'
required: false
Expand Down Expand Up @@ -79,6 +84,7 @@ jobs:
relay-ignore: ${{ steps.resolve.outputs.relay-ignore }}
router-select: ${{ steps.resolve.outputs.router-select }}
router-ignore: ${{ steps.resolve.outputs.router-ignore }}
transport-ignore: ${{ steps.resolve.outputs.transport-ignore }}
force-matrix-rebuild: ${{ steps.resolve.outputs.force-matrix-rebuild }}
force-image-rebuild: ${{ steps.resolve.outputs.force-image-rebuild }}
debug: ${{ steps.resolve.outputs.debug }}
Expand Down Expand Up @@ -110,6 +116,7 @@ jobs:
echo "relay-ignore=${{ github.event.inputs.relay-ignore }}" >> $GITHUB_OUTPUT
echo "router-select=${{ github.event.inputs.router-select }}" >> $GITHUB_OUTPUT
echo "router-ignore=${{ github.event.inputs.router-ignore }}" >> $GITHUB_OUTPUT
echo "transport-ignore=${{ github.event.inputs.transport-ignore }}" >> $GITHUB_OUTPUT
echo "force-matrix-rebuild=${{ github.event.inputs.force-matrix-rebuild }}" >> $GITHUB_OUTPUT
echo "force-image-rebuild=${{ github.event.inputs.force-image-rebuild }}" >> $GITHUB_OUTPUT
echo "debug=${{ github.event.inputs.debug }}" >> $GITHUB_OUTPUT
Expand Down Expand Up @@ -155,6 +162,7 @@ jobs:
echo "router-ignore=~failing" >> $GITHUB_OUTPUT
fi

echo "transport-ignore=ws|webrtc-direct" >> $GITHUB_OUTPUT
echo "force-matrix-rebuild=${{ steps.detect.outputs.impls-yaml-changed }}" >> $GITHUB_OUTPUT
echo "force-image-rebuild=${{ steps.detect.outputs.images-folder-changed }}" >> $GITHUB_OUTPUT
echo "debug=false" >> $GITHUB_OUTPUT
Expand All @@ -181,6 +189,7 @@ jobs:
relay-ignore: '${{ needs.resolve-parameters.outputs.relay-ignore }}'
router-select: '${{ needs.resolve-parameters.outputs.router-select }}'
router-ignore: '${{ needs.resolve-parameters.outputs.router-ignore }}'
transport-ignore: '${{ needs.resolve-parameters.outputs.transport-ignore }}'
cache-dir: /srv/cache
snapshot: ${{ needs.resolve-parameters.outputs.snapshot }}
export-docker-images: ${{ needs.resolve-parameters.outputs.export-docker-images }}
Expand Down
6 changes: 6 additions & 0 deletions hole-punch/images/linux/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ iptables -P FORWARD DROP
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT

# Drop unsolicited inbound TCP on WAN interface to prevent RST during hole-punch.
# Without this, incoming SYNs that arrive before conntrack entries are created
# get accepted by INPUT, find no listener, and generate RST - killing hole-punch.
iptables -A INPUT -i "$WAN_IF" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i "$WAN_IF" -j DROP

# Configure NAT (MASQUERADE)
# Translate source IP from LAN subnet to WAN IP when forwarding to WAN
iptables -t nat -A POSTROUTING -s "$LAN_SUBNET" -o "$WAN_IF" -j MASQUERADE
Expand Down
85 changes: 66 additions & 19 deletions hole-punch/images/rust/v0.56/src/bin/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ use libp2p::{
use libp2p_mplex as mplex;
use libp2p_webrtc as webrtc;
use redis::AsyncCommands;
use std::{env, str};
use std::{env, str, time::{Duration, Instant}};
use strum::{Display, EnumString};
use tokio::time::Duration;

#[tokio::main]
async fn main() -> Result<()> {
Expand Down Expand Up @@ -221,20 +220,11 @@ async fn run_listener(
swarm
.listen_on(relayed_listener_addr.clone())
.expect("failed to listen on p2p circuit");

// Publish to Redis with TEST_KEY namespacing
let listener_peer_id_key = format!("{test_key}_listener_peer_id");
let _: () = con
.set(&listener_peer_id_key, relayed_listener_addr.to_string())
.await
.expect(&format!(
"Failed to publish peer id to Redis: (key: {listener_peer_id_key})"
));
eprintln!("Published peer id to Redis (key: {listener_peer_id_key})");

let mut published_to_redis = false;
let mut hole_punch_connection_id = None;

// Wait for our listen to be ready and publish multiaddr
// Wait for reservation acceptance, then publish peer id and wait for hole-punch
loop {
if let Some(event) = swarm.next().await {
match event {
Expand All @@ -244,10 +234,25 @@ async fn run_listener(
} => {
eprintln!("Listener_id: {listener_id}, address: {address}");
}
swarm::SwarmEvent::NewExternalAddrCandidate { address } => {
eprintln!("New external address candidate: {address}");
swarm.add_external_address(address);
}
swarm::SwarmEvent::Behaviour(BehaviourEvent::RelayClient(
relay::client::Event::ReservationReqAccepted { .. },
)) => {
eprintln!("Relay accepted our reservation request");
if !published_to_redis {
let listener_peer_id_key = format!("{test_key}_listener_peer_id");
let _: () = con
.set(&listener_peer_id_key, peer_id.to_string())
.await
.expect(&format!(
"Failed to publish peer id to Redis: (key: {listener_peer_id_key})"
));
eprintln!("Published peer id to Redis (key: {listener_peer_id_key})");
published_to_redis = true;
}
}
swarm::SwarmEvent::Behaviour(BehaviourEvent::RelayClient(
relay::client::Event::InboundCircuitEstablished { src_peer_id, .. },
Expand Down Expand Up @@ -282,7 +287,8 @@ async fn run_listener(
"Recevied ping over hole-punch connection: {}",
rtt.as_micros() as f32 / 1000.
);
return Ok(());
// Don't return - let the event loop continue.
// The dialer will exit first, triggering docker-compose to stop us.
}
Err(e) if Some(connection) == hole_punch_connection_id => {
bail!("Ping failed over hole-punch connection {e:?}");
Expand All @@ -295,6 +301,12 @@ async fn run_listener(
} => {
eprintln!("New connection from {peer_id} details: {endpoint:?}");
}
swarm::SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
eprintln!("Outgoing connection error to {peer_id:?}: {error:?}");
}
swarm::SwarmEvent::ConnectionClosed { peer_id, cause, .. } => {
eprintln!("Connection closed with {peer_id}: {cause:?}");
}
other => {
if debug {
eprintln!("{other:?}");
Expand Down Expand Up @@ -412,6 +424,10 @@ async fn run_dialer(
})) => {
sent_observed_addr = true;
}
swarm::SwarmEvent::NewExternalAddrCandidate { address } => {
eprintln!("Dialer new external address candidate: {address}");
my_observed_addr = Some(address);
}
swarm::SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received {
info: identify::Info { observed_addr, .. },
..
Expand All @@ -431,24 +447,30 @@ async fn run_dialer(
}
};

eprintln!("Listener observed multiaddr: {external_addr}");
eprintln!("Dialer observed multiaddr: {external_addr}");

// Wait for listener peer id (with retries)
let listener_peer_id = wait_for_peer_id(&mut con, &format!("{test_key}_listener_peer_id")).await?;

// Step 4: Listen on the relayed circuit
// Step 4: Dial the listener through the relay circuit
let dcutr_start = Instant::now();
swarm
.dial(relay_addr
.with(Protocol::P2pCircuit)
.with(Protocol::P2p(listener_peer_id)))
.expect("failed to dial on p2p circuit");

let mut hole_punch_connection_id = None;
let mut dcutr_elapsed: Option<std::time::Duration> = None;

// Wait for our listen to be ready and publish multiaddr
// Wait for hole-punch to complete
loop {
if let Some(event) = swarm.next().await {
match event {
swarm::SwarmEvent::NewExternalAddrCandidate { address } => {
eprintln!("Dialer new external address candidate: {address}");
swarm.add_external_address(address);
}
swarm::SwarmEvent::Behaviour(BehaviourEvent::RelayClient(
relay::client::Event::ReservationReqAccepted { .. },
)) => {
Expand All @@ -467,6 +489,7 @@ async fn run_dialer(
)) => {
match result {
Ok(connection_id) => {
dcutr_elapsed = Some(dcutr_start.elapsed());
eprintln!("dcutr to {remote_peer_id} succeeded!!");
hole_punch_connection_id = Some(connection_id);
}
Expand All @@ -483,10 +506,18 @@ async fn run_dialer(
)) => {
match result {
Ok(rtt) if Some(connection) == hole_punch_connection_id => {
let ping_rtt_ms = rtt.as_micros() as f64 / 1000.0;
let handshake_ms = dcutr_elapsed
.map(|d| d.as_micros() as f64 / 1000.0)
.unwrap_or(0.0) + ping_rtt_ms;
eprintln!(
"Recevied ping over hole-punch connection: {}",
rtt.as_micros() as f32 / 1000.
"Received ping over hole-punch connection: {:.2}ms",
ping_rtt_ms,
);
println!("latency:");
println!(" handshake_plus_one_rtt: {:.2}", handshake_ms);
println!(" ping_rtt: {:.2}", ping_rtt_ms);
println!(" unit: ms");
return Ok(());
}
Err(e) if Some(connection) == hole_punch_connection_id => {
Expand All @@ -500,6 +531,12 @@ async fn run_dialer(
} => {
eprintln!("New connection from {peer_id} details: {endpoint:?}");
}
swarm::SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
eprintln!("Outgoing connection error to {peer_id:?}: {error:?}");
}
swarm::SwarmEvent::ConnectionClosed { peer_id, cause, .. } => {
eprintln!("Connection closed with {peer_id}: {cause:?}");
}
other => {
if debug {
eprintln!("{other:?}");
Expand All @@ -524,6 +561,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
.with_quic()
.with_relay_client(noise::Config::new, yamux::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/udp/0/quic-v1").parse().ok()),
Expand All @@ -538,6 +576,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
)?
.with_relay_client(tls::Config::new, mplex::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0").parse().ok()),
Expand All @@ -552,6 +591,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
)?
.with_relay_client(tls::Config::new, yamux::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0").parse().ok()),
Expand All @@ -566,6 +606,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
)?
.with_relay_client(noise::Config::new, mplex::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0").parse().ok()),
Expand All @@ -580,6 +621,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
)?
.with_relay_client(noise::Config::new, yamux::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0").parse().ok()),
Expand All @@ -591,6 +633,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
.await?
.with_relay_client(tls::Config::new, mplex::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0/ws").parse().ok()),
Expand All @@ -602,6 +645,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
.await?
.with_relay_client(tls::Config::new, yamux::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0/ws").parse().ok()),
Expand All @@ -613,6 +657,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
.await?
.with_relay_client(noise::Config::new, mplex::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0/ws").parse().ok()),
Expand All @@ -624,6 +669,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
.await?
.with_relay_client(noise::Config::new, yamux::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/tcp/0/ws").parse().ok()),
Expand All @@ -639,6 +685,7 @@ async fn build_swarm<B: swarm::NetworkBehaviour>(
})?
.with_relay_client(noise::Config::new, yamux::Config::default)?
.with_behaviour(behaviour_constructor)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(30)))
.build(),
listen_ip
.and_then(|ip| format!("/ip4/{ip}/udp/0/webrtc-direct").parse().ok()),
Expand Down
Loading
Loading