Skip to content

Commit

Permalink
feat(hole-punch): add hole-punch interoperability test suite (#304)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaseizinger authored Oct 16, 2023
1 parent 4ed47fe commit 1e37b93
Show file tree
Hide file tree
Showing 32 changed files with 9,183 additions and 0 deletions.
155 changes: 155 additions & 0 deletions .github/actions/run-interop-hole-punch-test/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: "libp2p hole-punch interop test"
description: "Run the libp2p hole-punch interoperability test suite"
inputs:
test-filter:
description: "Filter which tests to run out of the created matrix"
required: false
default: ""
test-ignore:
description: "Exclude tests from the created matrix that include this string in their name"
required: false
default: ""
extra-versions:
description: "Space-separated paths to JSON files describing additional images"
required: false
default: ""
s3-cache-bucket:
description: "Which S3 bucket to use for container layer caching"
required: false
default: ""
s3-access-key-id:
description: "S3 Access key id for the cache"
required: false
default: ""
s3-secret-access-key:
description: "S3 secret key id for the cache"
required: false
default: ""
aws-region:
description: "Which AWS region to use"
required: false
default: "us-east-1"
worker-count:
description: "How many workers to use for the test"
required: false
default: "2"
runs:
using: "composite"
steps:
- name: Configure AWS credentials for S3 build cache
if: inputs.s3-access-key-id != '' && inputs.s3-secret-access-key != ''
run: |
echo "PUSH_CACHE=true" >> $GITHUB_ENV
shell: bash

# This depends on where this file is within this repository. This walks up
# from here to the hole-punch-interop folder
- run: |
WORK_DIR=$(realpath "$GITHUB_ACTION_PATH/../../../hole-punch-interop")
echo "WORK_DIR=$WORK_DIR" >> $GITHUB_OUTPUT
shell: bash
id: find-workdir
- uses: actions/setup-node@v3
with:
node-version: 18

# Existence of /etc/buildkit/buildkitd.toml indicates that this is a
# self-hosted runner. If so, we need to pass the config to the buildx
# action. The config enables docker.io proxy which is required to
# work around docker hub rate limiting.
- run: |
if test -f /etc/buildkit/buildkitd.toml; then
echo "config=/etc/buildkit/buildkitd.toml" >> $GITHUB_OUTPUT
fi
shell: bash
id: buildkit
- name: Install more recent docker-compose version # https://stackoverflow.com/questions/54331949/having-networking-issues-with-docker-compose
shell: bash
run: |
mkdir -p $HOME/.docker/cli-plugins
wget -q -O- https://github.com/docker/compose/releases/download/v2.21.0/docker-compose-linux-x86_64 > $HOME/.docker/cli-plugins/docker-compose
chmod +x $HOME/.docker/cli-plugins/docker-compose
docker compose version
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
config: ${{ steps.buildkit.outputs.config }}

- name: Install deps
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
run: npm ci
shell: bash

- name: Load cache and build
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
run: npm run cache -- load
shell: bash

- name: Assert Git tree is clean.
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
shell: bash
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "Git tree is dirty. This means that building an impl generated something that should probably be .gitignore'd"
git status
exit 1
fi
- name: Push the image cache
if: env.PUSH_CACHE == 'true'
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
env:
AWS_BUCKET: ${{ inputs.s3-cache-bucket }}
AWS_REGION: ${{ inputs.aws-region }}
AWS_ACCESS_KEY_ID: ${{ inputs.s3-access-key-id }}
AWS_SECRET_ACCESS_KEY: ${{ inputs.s3-secret-access-key }}
run: npm run cache -- push
shell: bash

- name: Run the test
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
env:
WORKER_COUNT: ${{ inputs.worker-count }}
EXTRA_VERSION: ${{ inputs.extra-versions }}
NAME_FILTER: ${{ inputs.test-filter }}
NAME_IGNORE: ${{ inputs.test-ignore }}
run: npm run test -- --extra-version=$EXTRA_VERSION --name-filter=$NAME_FILTER --name-ignore=$NAME_IGNORE
shell: bash

- name: Print the results
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
run: cat results.csv
shell: bash

- name: Render results
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
run: npm run renderResults > ./dashboard.md
shell: bash

- name: Show Dashboard Output
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
run: cat ./dashboard.md >> $GITHUB_STEP_SUMMARY
shell: bash

- name: Exit with Error
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
run: |
if grep -q ":red_circle:" ./dashboard.md; then
exit 1
else
exit 0
fi
shell: bash

- uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: test-plans-output
path: |
${{ steps.find-workdir.outputs.WORK_DIR }}/results.csv
${{ steps.find-workdir.outputs.WORK_DIR }}/dashboard.md
${{ steps.find-workdir.outputs.WORK_DIR }}/runs
24 changes: 24 additions & 0 deletions .github/workflows/hole-punch-interop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
on:
workflow_dispatch:
pull_request:
paths:
- 'hole-punch-interop/**'
push:
branches:
- "master"
paths:
- 'hole-punch-interop/**'

name: libp2p holepunching interop test

jobs:
run-hole-punch-interop:
runs-on: ['self-hosted', 'linux', 'x64', '4xlarge'] # https://github.com/pl-strflt/tf-aws-gh-runner/blob/main/runners.tf
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/run-interop-hole-punch-test
with:
s3-cache-bucket: libp2p-by-tf-aws-bootstrap
s3-access-key-id: ${{ vars.S3_AWS_ACCESS_KEY_ID }}
s3-secret-access-key: ${{ secrets.S3_AWS_SECRET_ACCESS_KEY }}
worker-count: 16
7 changes: 7 additions & 0 deletions hole-punch-interop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# For now, not committing image.json files
image.json

results.csv
runs/

node_modules/
19 changes: 19 additions & 0 deletions hole-punch-interop/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
RUST_SUBDIRS := $(wildcard impl/rust/*/.)
GO_SUBDIRS := $(wildcard impl/go/*/.)

all: rust-relay router $(RUST_SUBDIRS) $(GO_SUBDIRS)
rust-relay:
$(MAKE) -C rust-relay
router:
$(MAKE) -C router
$(RUST_SUBDIRS):
$(MAKE) -C $@
$(GO_SUBDIRS):
$(MAKE) -C $@
clean:
$(MAKE) -C rust-relay clean
$(MAKE) -C router clean
$(MAKE) -C $(RUST_SUBDIRS) clean
$(MAKE) -C $(GO_SUBDIRS) clean

.PHONY: rust-relay router all $(RUST_SUBDIRS) $(GO_SUBDIRS)
84 changes: 84 additions & 0 deletions hole-punch-interop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Hole punch tests

## How to run locally

1. `npm install`
2. `make`
3. `npm run test`

## Client configuration

| env variable | possible values |
|--------------|-----------------|
| MODE | listen \| dial |
| TRANSPORT | tcp \| quic |

- For TCP, the client MUST use noise + yamux to upgrade the connection.
- The relayed connection MUST use noise + yamux.

## Test flow

1. The relay starts and pushes its address to the following redis keys:
- `RELAY_TCP_ADDRESS` for the TCP test
- `RELAY_QUIC_ADDRESS` for the QUIC test
1. Upon start-up, clients connect to a redis server at `redis:6379` and block until this redis key comes available.
They then dial the relay on the provided address.
1. The relay supports identify.
Implementations SHOULD use that to figure out their external address next.
1. Once connected to the relay, a client in `MODE=listen` should listen on the relay and make a reservation.
Once the reservation is made, it pushes its `PeerId` to the redis key `LISTEN_CLIENT_PEER_ID`.
1. A client in `MODE=dial` blocks on the availability of `LISTEN_CLIENT_PEER_ID`.
Once available, it dials `<relay_addr>/p2p-circuit/<listen-client-peer-id>`.
1. Upon a successful hole-punch, the peer in `MODE=dial` measures the RTT across the newly established connection.
1. The RTT MUST be printed to stdout in the following format:
```json
{ "rtt_to_holepunched_peer_millis": 12 }
```
1. Once printed, the dialer MUST exit with `0`.

## Requirements for implementations

- Docker containers MUST have a binary called `hole-punch-client` in their $PATH
- MUST have `dig`, `curl`, `jq` and `tcpdump` installed
- Listener MUST NOT early-exit but wait to be killed by test runner
- Logs MUST go to stderr, RTT json MUST go to stdout
- Dialer and lister both MUST use 0RTT negotiation for protocols
- Implementations SHOULD disable timeouts on the redis client, i.e. use `0`
- Implementations SHOULD exit early with a non-zero exit code if anything goes wrong
- Implementations MUST set `TCP_NODELAY` for the TCP transport
- Implements MUST make sure connections are being kept alive

## Design notes

The design of this test runner is heavily influenced by [multidim-interop](../multidim-interop) but differs in several ways.

All files related to test runs will be written to the [./runs](./runs) directory.
This includes the `docker-compose.yml` files of each individual run as well as logs and `tcpdump`'s for the dialer and listener.

The docker-compose file uses 6 containers in total:

- 1 redis container for orchestrating the test
- 1 [relay](./rust-relay)
- 1 hole-punch client in `MODE=dial`
- 1 hole-punch client in `MODE=listen`
- 2 [routers](./router): 1 per client

The networks are allocated by docker-compose.
We dynamically fetch the IPs and subnets as part of a startup script to set the correct IP routes.

In total, we have three networks:

1. `lan_dialer`
2. `lan_listener`
3. `internet`

The two LANs host a router and a client each whereas the relay is connected (without a router) to the `internet` network.
On startup of the clients, we add an `ip route` that redirects all traffic to the corresponding `router` container.
The router container masquerades all traffic upon forwarding, see the [README](./router/README.md) for details.

## Running a single test

1. Build all containers using `make`
1. Generate all test definitions using `npm run test -- --no-run`
1. Pick the desired test from the [runs](./runs) directory
1. Execute it using `docker compose up`
Loading

0 comments on commit 1e37b93

Please sign in to comment.