Skip to content

Commit 7d1e75e

Browse files
fix: enable hole punching (#2618)
1 parent ead7f9e commit 7d1e75e

12 files changed

Lines changed: 545 additions & 56 deletions

File tree

.github/workflows/interop.yml

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,10 @@ jobs:
1717
name: Run transport interoperability tests
1818
runs-on: ubuntu-22.04
1919
steps:
20-
- name: Free Disk Space
21-
# For some reason we have space issues while running this action. Likely while building the image.
22-
# This action will free up some space to avoid the issue.
23-
uses: jlumbroso/free-disk-space@v1.3.1
24-
with:
25-
tool-cache: true
26-
2720
- uses: actions/checkout@v6
2821
- uses: docker/setup-buildx-action@v4
2922
- name: Build image
30-
run: docker buildx build --load -t nim-libp2p-transport-head -f interop/transport/Dockerfile .
23+
run: docker build --load -t nim-libp2p-transport-head -f interop/transport/Dockerfile .
3124
- name: Run tests
3225
uses: libp2p/test-plans/.github/actions/run-transport-interop-test@master
3326
with:
@@ -39,31 +32,23 @@ jobs:
3932
worker-count: 4
4033
known-errors: "^(c-v0\\.0\\.1 x nim-libp2p-transport-head \\(quic-v1\\)|eth-p2p-z-v0\\.0\\.1 x nim-libp2p-transport-head \\(quic-v1\\))$"
4134

42-
# run-hole-punching-interop:
43-
# name: Run hole punching interoperability tests
44-
# runs-on: ubuntu-22.04
45-
# steps:
46-
# - name: Free Disk Space
47-
# # For some reason we have space issues while running this action. Likely while building the image.
48-
# # This action will free up some space to avoid the issue.
49-
# uses: jlumbroso/free-disk-space@v1.3.1
50-
# with:
51-
# tool-cache: true
52-
53-
# - uses: actions/checkout@v6
54-
# - uses: docker/setup-buildx-action@v4
55-
# - name: Build image
56-
# run: docker buildx build --load -t nim-libp2p-hp-head -f interop/hole-punching/Dockerfile .
57-
# - name: Run tests
58-
# uses: libp2p/test-plans/.github/actions/run-interop-hole-punch-test@master
59-
# with:
60-
# test-filter: nim-libp2p-hp-head
61-
# test-ignore: rust
62-
# extra-versions: ${{ github.workspace }}/interop/hole-punching/version.json
63-
# s3-cache-bucket: ${{ vars.S3_LIBP2P_BUILD_CACHE_BUCKET_NAME }}
64-
# s3-access-key-id: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_ACCESS_KEY_ID }}
65-
# s3-secret-access-key: ${{ secrets.S3_LIBP2P_BUILD_CACHE_AWS_SECRET_ACCESS_KEY }}
66-
# aws-region: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_REGION }}
35+
run-hole-punching-interop:
36+
name: Run hole punching interoperability tests
37+
runs-on: ubuntu-22.04
38+
steps:
39+
- uses: actions/checkout@v6
40+
- uses: docker/setup-buildx-action@v4
41+
- name: Build image
42+
run: docker build --load -t nim-libp2p-hp-head -f interop/hole-punching/Dockerfile .
43+
- name: Run tests
44+
uses: libp2p/test-plans/.github/actions/run-interop-hole-punch-test@master
45+
with:
46+
test-filter: nim-libp2p-hp-head
47+
extra-versions: ${{ github.workspace }}/interop/hole-punching/version.json
48+
s3-cache-bucket: ${{ vars.S3_LIBP2P_BUILD_CACHE_BUCKET_NAME }}
49+
s3-access-key-id: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_ACCESS_KEY_ID }}
50+
s3-secret-access-key: ${{ secrets.S3_LIBP2P_BUILD_CACHE_AWS_SECRET_ACCESS_KEY }}
51+
aws-region: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_REGION }}
6752

6853
run-autonatv2-interop:
6954
name: Run AutoNATv2 interoperability tests
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# syntax=docker/dockerfile:1.5-labs
2+
FROM nimlang/nim:2.2.10 as builder
3+
4+
WORKDIR /app
5+
6+
COPY .pinned libp2p.nimble nim-libp2p/
7+
8+
RUN --mount=type=cache,target=/var/cache/apt apt-get update && apt-get install -y --no-install-recommends ca-certificates binutils libssl-dev \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
RUN cd nim-libp2p && nimble install -y redis && nimble install_pinned
12+
13+
COPY . nim-libp2p/
14+
15+
RUN cd nim-libp2p && \
16+
nim c -d:release --skipProjCfg --skipParentCfg --NimblePath:./nimbledeps/pkgs2 \
17+
--mm:refc --threads:on \
18+
-d:chronicles_log_level=INFO -d:chronicles_default_output_device=stderr -d:chronicles_colors=None \
19+
-o:/app/peer ./interop/hole-punching-v2/hole_punching.nim
20+
21+
FROM debian:trixie-slim
22+
RUN apt-get update && apt-get install -y iproute2 && rm -rf /var/lib/apt/lists/*
23+
COPY --from=builder /app/peer /usr/bin/peer
24+
COPY interop/hole-punching-v2/run-peer.sh /usr/bin/run-peer.sh
25+
RUN chmod +x /usr/bin/run-peer.sh
26+
ENTRYPOINT ["/usr/bin/run-peer.sh"]

interop/hole-punching-v2/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Hole-punch Interop Test App (v2)
2+
3+
Test app for [libp2p/unified-testing](https://github.com/libp2p/unified-testing) hole punch tests.
4+
5+
This preserves the unified-testing peer contract separately from `../hole-punching`, which targets the current `libp2p/test-plans` harness. It is not wired into this repository CI while unified-testing hole-punch runs remain external.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# SPDX-License-Identifier: Apache-2.0 OR MIT
2+
# Copyright (c) Status Research & Development GmbH
3+
4+
import chronos, chronicles, sequtils, tables
5+
import
6+
../../libp2p/[
7+
builders,
8+
switch,
9+
multicodec,
10+
observedaddrmanager,
11+
services/hpservice,
12+
services/autorelayservice,
13+
protocols/connectivity/relay/client,
14+
protocols/connectivity/relay/relay,
15+
protocols/connectivity/autonat/service,
16+
protocols/ping,
17+
]
18+
import ../../tests/stubs/autonatclientstub
19+
import ../unified_testing
20+
21+
logScope:
22+
topics = "hp interop peer"
23+
24+
proc createSwitch(
25+
config: BaseConfig, relayClient: Relay = nil, hpService: Service = nil
26+
): Switch =
27+
var s = buildBaseSwitch(config, tcpFlags = {ServerFlags.TcpNoDelay})
28+
.withObservedAddrManager(ObservedAddrManager.new(maxSize = 1, minCount = 1))
29+
.withAutonat()
30+
.withCircuitRelay(relayClient)
31+
.build()
32+
33+
s.add(hpService)
34+
s.mount(Ping.new(rng = rng()))
35+
s
36+
37+
proc isDirectlyConnected(switch: Switch, peerId: PeerId): bool =
38+
let conns = switch.connManager.getConnections()
39+
peerId in conns and conns[peerId].anyIt(not isRelayed(it.connection))
40+
41+
type HolePunchSwitches = object
42+
sw: Switch
43+
aux: Switch
44+
autonatService: AutonatService
45+
46+
proc createSwitches(config: BaseConfig): HolePunchSwitches =
47+
# Setup relay
48+
let relayClient = RelayClient.new()
49+
let autoRelayService = AutoRelayService.new(1, relayClient, nil, rng())
50+
51+
# Setup autonat
52+
let autonatClientStub = AutonatClientStub.new(expectedDials = 1)
53+
autonatClientStub.answer = NotReachable
54+
55+
# Setup hpservice
56+
let autonatService = AutonatService.new(autonatClientStub, rng(), maxQueueSize = 1)
57+
let hpservice = HPService.new(autonatService, autoRelayService)
58+
59+
# Setup switches
60+
let switches = HolePunchSwitches(
61+
sw: createSwitch(config, relayClient, hpservice),
62+
aux: createSwitch(config),
63+
autonatService: autonatService,
64+
)
65+
switches
66+
67+
proc connectToRelay(
68+
config: BaseConfig, redisClient: Redis, switches: HolePunchSwitches
69+
): Future[MultiAddress] {.async.} =
70+
# Connect to aux switch for AutoNAT stub to report NotReachable
71+
await switches.sw.connect(switches.aux.peerInfo.peerId, switches.aux.peerInfo.addrs)
72+
73+
# Wait for autonat to report NotReachable
74+
pollUntil(
75+
switches.autonatService.networkReachability == NetworkReachability.NotReachable,
76+
errorMsg = "Timeout waiting for AutoNAT NotReachable",
77+
)
78+
info "AutoNAT reports NotReachable"
79+
80+
# Connect to relay (triggers AutoRelay reservation)
81+
let relayMA = await fetchRelayMultiaddr(redisClient, config.testKey)
82+
info "Got relay address", relayMA
83+
84+
try:
85+
info "Dialing relay", relayMA
86+
let relayId = await switches.sw.connect(relayMA).wait(30.seconds)
87+
info "Connected to relay", relayId
88+
except AsyncTimeoutError as e:
89+
raise newException(CatchableError, "Connection to relay timed out: " & e.msg, e)
90+
91+
# Wait for our relay circuit address
92+
pollUntil(
93+
switches.sw.peerInfo.addrs.anyIt(it.contains(multiCodec("p2p-circuit")).tryGet()),
94+
errorMsg = "Timeout waiting for relay circuit address",
95+
)
96+
info "Got relay circuit address"
97+
relayMA
98+
99+
proc runDialer(config: BaseConfig) {.async.} =
100+
let
101+
redisClient = setupRedis(config.redisAddr)
102+
switches = createSwitches(config)
103+
104+
await allFutures(switches.sw.start(), switches.aux.start())
105+
defer:
106+
# Timeout the stop to avoid hanging on mplex teardown
107+
discard
108+
await allFutures(switches.sw.stop(), switches.aux.stop()).withTimeout(5.seconds)
109+
110+
let relayMA = await connectToRelay(config, redisClient, switches)
111+
112+
let listenerId = await fetchListenerPeerId(redisClient, config.testKey)
113+
info "Got listener peer ID", listenerId
114+
115+
let listenerRelayAddr = MultiAddress.init($relayMA & "/p2p-circuit").tryGet()
116+
117+
# Start DCUtR timer
118+
let dcutrStart = Moment.now()
119+
120+
info "Dialing listener via relay", listenerRelayAddr
121+
await switches.sw.connect(listenerId, @[listenerRelayAddr])
122+
123+
# Wait for DCUtR to complete (direct connection established)
124+
# HPService handles DCUtR in the background when the listener receives
125+
# the relayed connection. Poll for a direct connection.
126+
pollUntil(
127+
switches.sw.isDirectlyConnected(listenerId),
128+
errorMsg = "DCUtR failed: no direct connection established within timeout",
129+
)
130+
131+
let dcutrElapsed = Moment.now() - dcutrStart
132+
info "Direct connection established via DCUtR"
133+
134+
# Ping over the direct connection
135+
let channel = await switches.sw.dial(listenerId, PingCodec)
136+
defer:
137+
await channel.close()
138+
139+
let pingDelay = await Ping.new(rng = rng()).ping(channel)
140+
let pingRttMs = pingDelay.toMs()
141+
142+
printLatencyYaml(dcutrElapsed.toMs() + pingRttMs, pingRttMs)
143+
144+
proc runListener(config: BaseConfig) {.async.} =
145+
let
146+
redisClient = setupRedis(config.redisAddr)
147+
switches = createSwitches(config)
148+
149+
await allFutures(switches.sw.start(), switches.aux.start())
150+
defer:
151+
# Timeout the stop to avoid hanging on mplex teardown
152+
discard
153+
await allFutures(switches.sw.stop(), switches.aux.stop()).withTimeout(5.seconds)
154+
155+
discard await connectToRelay(config, redisClient, switches)
156+
157+
let listenerPeerId = publishListenerPeerId(redisClient, config.testKey, switches.sw)
158+
info "Published listener peer ID to Redis", listenerPeerId
159+
160+
# Wait to be killed (docker-compose will stop us after dialer exits)
161+
await sleepAsync(5.minutes)
162+
163+
proc main() {.async.} =
164+
let config = readBaseConfig()
165+
info "Test configuration", config
166+
if config.isDialer:
167+
await runDialer(config)
168+
else:
169+
await runListener(config)
170+
171+
let testTimeout = parseDurationEnv("TEST_TIMEOUT_SECS", 1.seconds, DefaultTestTimeout)
172+
runMain(main, testTimeout)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# Set route to relay subnet
5+
echo "Setting route to relay subnet ${WAN_SUBNET} via ${WAN_ROUTER_IP}" >&2
6+
ip route add "${WAN_SUBNET}" via "${WAN_ROUTER_IP}" dev lan0
7+
8+
# Execute the peer binary, passing through all arguments
9+
exec /usr/bin/peer "$@"

interop/hole-punching/Dockerfile

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ RUN cd nim-libp2p && \
1919
-o:/app/peer ./interop/hole-punching/hole_punching.nim
2020

2121
FROM debian:trixie-slim
22-
RUN apt-get update && apt-get install -y iproute2 && rm -rf /var/lib/apt/lists/*
23-
COPY --from=builder /app/peer /usr/bin/peer
24-
COPY interop/hole-punching/run-peer.sh /usr/bin/run-peer.sh
25-
RUN chmod +x /usr/bin/run-peer.sh
26-
ENTRYPOINT ["/usr/bin/run-peer.sh"]
22+
# The upstream harness runs these tools in participant containers before the client.
23+
RUN apt-get update && apt-get install -y --no-install-recommends \
24+
bind9-dnsutils curl iproute2 jq tcpdump \
25+
&& rm -rf /var/lib/apt/lists/*
26+
COPY --from=builder /app/peer /usr/bin/nim-hole-punch-client
27+
COPY interop/hole-punching/hole-punch-client.sh /usr/bin/hole-punch-client
28+
COPY interop/hole-punching/hole-punch-router-nat.sh /usr/bin/hole-punch-router-nat
29+
COPY interop/hole-punching/hole-punch-entrypoint.sh /usr/bin/hole-punch-entrypoint
30+
RUN chmod +x /usr/bin/hole-punch-client /usr/bin/hole-punch-router-nat /usr/bin/hole-punch-entrypoint
31+
ENTRYPOINT ["/usr/bin/hole-punch-entrypoint"]
32+
CMD ["hole-punch-client"]

interop/hole-punching/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
# Hole-punch Interop Test App
22

3-
Test app for [libp2p/unified-testing](https://github.com/libp2p/unified-testing) hole punch tests.
3+
Test app for the [libp2p/test-plans](https://github.com/libp2p/test-plans)
4+
hole-punch interop test.
5+
6+
The upstream harness runs `hole-punch-client` with `MODE=listen|dial`,
7+
`TRANSPORT=tcp|quic`, and a Redis server at `redis:6379`. The client reads the
8+
relay address from the `RELAY_TCP_ADDRESS` or `RELAY_QUIC_ADDRESS` Redis lists,
9+
the listener pushes `LISTEN_CLIENT_PEER_ID`, and the dialer prints a single JSON
10+
line:
11+
12+
```json
13+
{"rtt_to_holepunched_peer_millis":200.00}
14+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
3+
# SPDX-License-Identifier: Apache-2.0 OR MIT
4+
# Copyright (c) Status Research & Development GmbH
5+
6+
set -eu
7+
8+
# This is a harness compatibility wrapper; the real client is the Nim binary.
9+
if ! /usr/bin/hole-punch-router-nat; then
10+
echo "hole-punch-router-nat failed; continuing" >&2
11+
fi
12+
13+
exec /usr/bin/nim-hole-punch-client "$@"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/sh
2+
3+
# SPDX-License-Identifier: Apache-2.0 OR MIT
4+
# Copyright (c) Status Research & Development GmbH
5+
6+
set -eu
7+
8+
# Run before the harness command as well as before the wrapper starts Nim.
9+
if ! /usr/bin/hole-punch-router-nat; then
10+
echo "hole-punch-router-nat failed; continuing" >&2
11+
fi
12+
13+
if [ "$#" -eq 0 ]; then
14+
set -- hole-punch-client
15+
fi
16+
17+
exec "$@"

0 commit comments

Comments
 (0)