From 3be681ec4d221b443450a634cc1490d9d674b480 Mon Sep 17 00:00:00 2001 From: diegomrsantos Date: Tue, 5 Dec 2023 18:37:33 +0100 Subject: [PATCH] feat: add hole-punching interop tests (#998) --- .github/workflows/interop.yml | 14 +++ tests/hole-punching-interop/Dockerfile | 17 +++ tests/hole-punching-interop/hole_punching.nim | 114 ++++++++++++++++++ tests/hole-punching-interop/version.json | 7 ++ 4 files changed, 152 insertions(+) create mode 100644 tests/hole-punching-interop/Dockerfile create mode 100644 tests/hole-punching-interop/hole_punching.nim create mode 100644 tests/hole-punching-interop/version.json diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index 41054e298..70db62283 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -52,3 +52,17 @@ jobs: with: test-filter: nim-libp2p-head extra-versions: ${{ github.workspace }}/test_head.json + + run-hole-punching-interop: + name: Run hole-punching interoperability tests + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build image + run: docker buildx build --load -t nim-libp2p-head -f tests/hole-punching-interop/Dockerfile . + - name: Run tests + uses: libp2p/test-plans/.github/actions/run-interop-hole-punch-test@master + with: + test-filter: nim-libp2p-head + extra-versions: ${{ github.workspace }}/tests/hole-punching-interop/version.json diff --git a/tests/hole-punching-interop/Dockerfile b/tests/hole-punching-interop/Dockerfile new file mode 100644 index 000000000..f5a0c2681 --- /dev/null +++ b/tests/hole-punching-interop/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1.5-labs +FROM nimlang/nim:1.6.14 as builder + +WORKDIR /workspace + +COPY .pinned libp2p.nimble nim-libp2p/ + +RUN cd nim-libp2p && nimble install_pinned && nimble install redis -y + +COPY . nim-libp2p/ + +RUN cd nim-libp2p && nim c --skipParentCfg --NimblePath:./nimbledeps/pkgs -d:chronicles_log_level=DEBUG -d:chronicles_default_output_device=stderr -d:release --threads:off --skipProjCfg -o:hole-punching-tests ./tests/hole-punching-interop/hole_punching.nim + +FROM --platform=linux/amd64 debian:bookworm-slim +RUN --mount=type=cache,target=/var/cache/apt apt-get update && apt-get install -y dnsutils jq curl tcpdump iproute2 +COPY --from=builder /workspace/nim-libp2p/hole-punching-tests /usr/bin/hole-punch-client +ENV RUST_BACKTRACE=1 diff --git a/tests/hole-punching-interop/hole_punching.nim b/tests/hole-punching-interop/hole_punching.nim new file mode 100644 index 000000000..390c807e7 --- /dev/null +++ b/tests/hole-punching-interop/hole_punching.nim @@ -0,0 +1,114 @@ +import std/[os, options, strformat] +import redis +import chronos, chronicles +import ../../libp2p/[builders, + switch, + observedaddrmanager, + services/hpservice, + services/autorelayservice, + protocols/connectivity/autonat/client as aclient, + protocols/connectivity/relay/client as rclient, + protocols/connectivity/relay/relay, + protocols/connectivity/autonat/service, + protocols/ping] +import ../stubs/autonatclientstub + +proc createSwitch(r: Relay = nil, hpService: Service = nil): Switch = + let rng = newRng() + var builder = SwitchBuilder.new() + .withRng(rng) + .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ]) + .withObservedAddrManager(ObservedAddrManager.new(maxSize = 1, minCount = 1)) + .withTcpTransport({ServerFlags.TcpNoDelay}) + .withYamux() + .withAutonat() + .withNoise() + + if hpService != nil: + builder = builder.withServices(@[hpService]) + + if r != nil: + builder = builder.withCircuitRelay(r) + + let s = builder.build() + s.mount(Ping.new(rng=rng)) + return s + +proc main() {.async.} = + try: + let relayClient = RelayClient.new() + let autoRelayService = AutoRelayService.new(1, relayClient, nil, newRng()) + let autonatClientStub = AutonatClientStub.new(expectedDials = 1) + autonatClientStub.answer = NotReachable + let autonatService = AutonatService.new(autonatClientStub, newRng(), maxQueueSize = 1) + let hpservice = HPService.new(autonatService, autoRelayService) + + let + isListener = getEnv("MODE") == "listen" + switch = createSwitch(relayClient, hpservice) + auxSwitch = createSwitch() + redisClient = open("redis", 6379.Port) + + debug "Connected to redis" + + await switch.start() + await auxSwitch.start() + + let relayAddr = + try: + redisClient.bLPop(@["RELAY_TCP_ADDRESS"], 0) + except Exception as e: + raise newException(CatchableError, e.msg) + + # This is necessary to make the autonat service work. It will ask this peer for our reachability which the autonat + # client stub will answer NotReachable. + await switch.connect(auxSwitch.peerInfo.peerId, auxSwitch.peerInfo.addrs) + + # Wait for autonat to be NotReachable + while autonatService.networkReachability != NetworkReachability.NotReachable: + await sleepAsync(100.milliseconds) + + # This will trigger the autonat relay service to make a reservation. + let relayMA = MultiAddress.init(relayAddr[1]).tryGet() + debug "Got relay address", relayMA + let relayId = await switch.connect(relayMA) + debug "Connected to relay", relayId + + # Wait for our relay address to be published + while switch.peerInfo.addrs.len == 0: + await sleepAsync(100.milliseconds) + + if isListener: + let listenerPeerId = switch.peerInfo.peerId + discard redisClient.rPush("LISTEN_CLIENT_PEER_ID", $listenerPeerId) + debug "Pushed listener client peer id to redis", listenerPeerId + + # Nothing to do anymore, wait to be killed + await sleepAsync(2.minutes) + else: + let listenerId = + try: + PeerId.init(redisClient.bLPop(@["LISTEN_CLIENT_PEER_ID"], 0)[1]).tryGet() + except Exception as e: + raise newException(CatchableError, e.msg) + + debug "Got listener peer id", listenerId + let listenerRelayAddr = MultiAddress.init($relayMA & "/p2p-circuit").tryGet() + + debug "Dialing listener relay address", listenerRelayAddr + await switch.connect(listenerId, @[listenerRelayAddr]) + + # wait for hole-punching to complete in the background + await sleepAsync(5000.milliseconds) + + let conn = switch.connManager.selectMuxer(listenerId).connection + let channel = await switch.dial(listenerId, @[listenerRelayAddr], PingCodec) + let delay = await Ping.new().ping(channel) + await allFuturesThrowing(channel.close(), conn.close(), switch.stop(), auxSwitch.stop()) + echo &"""{{"rtt_to_holepunched_peer_millis":{delay.millis}}}""" + quit(0) + except CatchableError as e: + error "Unexpected error", msg = e.msg + +discard waitFor(main().withTimeout(4.minutes)) +quit(1) diff --git a/tests/hole-punching-interop/version.json b/tests/hole-punching-interop/version.json new file mode 100644 index 000000000..719343c59 --- /dev/null +++ b/tests/hole-punching-interop/version.json @@ -0,0 +1,7 @@ +{ + "id": "nim-libp2p-head", + "containerImageID": "nim-libp2p-head", + "transports": [ + "tcp" + ] +}