Skip to content
Open
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
23 changes: 23 additions & 0 deletions hole-punch-interop/impl/python/v0.4.0/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.12-slim

# Install system dependencies
RUN apt-get update && apt-get install -y git build-essential iptables libgmp-dev && rm -rf /var/lib/apt/lists/*

# Upgrade pip and build tools
RUN python -m pip install --upgrade pip setuptools wheel

WORKDIR /app

# Copy only pyproject.toml first to leverage Docker cache
COPY pyproject.toml .
RUN pip install --no-cache-dir -e .

# Copy rest of the code
COPY hole_punch.py .

# Startup script for iptables + python app
COPY start.sh /start.sh
RUN chmod +x /start.sh
ENTRYPOINT ["/start.sh"]

VOLUME /results
16 changes: 16 additions & 0 deletions hole-punch-interop/impl/python/v0.4.0/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
image_name := python-v0.4.0

.PHONY: all build clean

all: image.json

image.json: Dockerfile
IMAGE_NAME=${image_name} ../../../dockerBuildWrapper.sh .
docker image inspect ${image_name} -f "{{.Id}}" | \
xargs -I {} echo "{\"imageID\": \"{}\"}" > $@

build: image.json

clean:
rm -f image.json
docker rmi ${image_name} || true
12 changes: 12 additions & 0 deletions hole-punch-interop/impl/python/v0.4.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Python Hole-Punch Interop Test (py-libp2p v0.4.0)

Implements DCUtR hole punching for py-libp2p v0.4.0.

## Local Testing

```bash
cd hole-punch-interop/impl/python/v0.4.0
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
make build
Empty file.
171 changes: 171 additions & 0 deletions hole-punch-interop/impl/python/v0.4.0/hole_punch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
Hole-Punch Interop Test for py-libp2p v0.3.x
Supports initiator & receiver roles.
Writes to /results/results.csv
"""

import asyncio
import json
import logging
import os
import time
from typing import Sequence, Literal

from libp2p import new_host
from libp2p.abc import IHost
from libp2p.crypto.secp256k1 import create_new_key_pair
from libp2p.peer.id import ID
from libp2p.peer.peerinfo import info_from_p2p_addr
from libp2p.transport.upgrader import TransportUpgrader
from multiaddr import Multiaddr
from libp2p.custom_types import (
TProtocol,
)

DCUTR_PROTOCOL = TProtocol("/libp2p/dcutr/1.0.0")
PING_PROTOCOL = TProtocol("/test/ping/1.0.0")

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("hole-punch")

async def create_host(listen_addrs: Sequence[Multiaddr] | None = None) -> IHost:
key_pair = create_new_key_pair()

host: IHost = new_host(
key_pair=key_pair,
muxer_preference="MPLEX",
listen_addrs=listen_addrs or [Multiaddr("/ip4/0.0.0.0/tcp/0")]
)
return host

async def main() -> None:
mode = os.environ["MODE"].lower()
relay_addr_str = os.environ["RELAY_MULTIADDR"]
target_id_str = os.environ["TARGET_ID"]

# Create and start listening
host = await create_host()
addrs = host.get_addrs()
log.info(f"Listening on: {[str(a) for a in addrs]}")

# Connect to the relay
relay_ma = Multiaddr(relay_addr_str)
relay_info = info_from_p2p_addr(relay_ma)
await host.connect(relay_info)
log.info(f"Connected to relay: {relay_ma}")

# Connect to the *target* via the relay (control channel)
target_id = ID(target_id_str.encode())
relayed_target_ma = Multiaddr(f"{relay_addr_str}/p2p-circuit/p2p/{target_id}")
target_info = info_from_p2p_addr(relayed_target_ma)
await host.connect(target_info)
log.info(f"Connected to target via relay: {relayed_target_ma}")

if mode == "initiator":
await initiator_role(host, target_id)
else:
await receiver_role(host)

async def initiator_role(host: IHost, target_id: ID) -> None:
stream = await host.new_stream(target_id, [DCUTR_PROTOCOL])

# Send CONNECT
my_addrs = [str(a) for a in host.get_addrs()]
await stream.write(json.dumps({"type": "CONNECT", "addrs": my_addrs}).encode())
log.info(f"Sent CONNECT with {len(my_addrs)} addrs")

# Receive SYNC
try:
raw = await asyncio.wait_for(stream.read(4096), timeout=15)
msg = json.loads(raw.decode())
if msg.get("type") != "SYNC":
raise ValueError("expected SYNC")
peer_addrs = msg.get("addrs", [])
log.info(f"Received SYNC with {len(peer_addrs)} addrs")
except Exception as e:
log.error(f"SYNC failed: {e}")
await write_result("failure")
await stream.close()
return
finally:
await stream.close()

# Direct dial the first address
if peer_addrs:
try:
direct_ma = Multiaddr(peer_addrs[0])
direct_info = info_from_p2p_addr(direct_ma)
await host.connect(direct_info)
log.info(f"Direct connection SUCCESS → {direct_ma}")
except Exception as e:
log.warning(f"Direct dial failed: {e}")
await write_result("failure")
return

# Ping test
try:
ping = await host.new_stream(target_id, [PING_PROTOCOL])
start = time.time()
await ping.write(b"ping")
resp = await asyncio.wait_for(ping.read(4), timeout=5)
rtt = (time.time() - start) * 1000
if resp == b"pong":
log.info(f"Ping RTT: {rtt:.1f} ms → SUCCESS")
await write_result("success")
else:
log.error("Ping failed – bad response")
await write_result("failure")
await ping.close()
except Exception as e:
log.error(f"Ping failed: {e}")
await write_result("failure")

async def receiver_role(host: IHost) -> None:
async def dcutr_handler(stream):
try:
data = await stream.read(4096)
msg = json.loads(data.decode())
if msg.get("type") != "CONNECT":
return

my_addrs = [str(a) for a in host.get_addrs()]
await stream.write(json.dumps({"type": "SYNC", "addrs": my_addrs}).encode())
log.info(f"Sent SYNC with {len(my_addrs)} addrs")

if msg.get("addrs"):
addr = Multiaddr(msg["addrs"][0])
info = info_from_p2p_addr(addr)
asyncio.create_task(host.connect(info))
except Exception as e:
log.error(f"DCUtR handler error: {e}")
finally:
await stream.close()

async def ping_handler(stream):
try:
data = await stream.read(4)
if data == b"ping":
await stream.write(b"pong")
finally:
await stream.close()

host.set_stream_handler(DCUTR_PROTOCOL, dcutr_handler)
host.set_stream_handler(PING_PROTOCOL, ping_handler)
log.info("Receiver ready – awaiting initiator…")
await asyncio.Event().wait()

async def write_result(result: str) -> None:
impl = os.environ.get("TARGET_IMPL", "go")
os.makedirs("/results", exist_ok=True)
line = f'"python-v0.3.x x {impl}-v0.42 (dcutr,tcp,noise)",{result}\n'
with open("/results/results.csv", "a") as f:
f.write(line)
log.info(f"RESULT: {result.upper()}")

if __name__ == "__main__":
asyncio.run(main())
10 changes: 10 additions & 0 deletions hole-punch-interop/impl/python/v0.4.0/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "hole-punch-python"
version = "0.4.0"
dependencies = [
"libp2p @ git+https://github.com/libp2p/py-libp2p.git@v0.4.0"
]
5 changes: 5 additions & 0 deletions hole-punch-interop/impl/python/v0.4.0/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# Setup NAT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# Start Python app
exec python hole_punch.py
6 changes: 3 additions & 3 deletions hole-punch-interop/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ function canonicalImagePath(id: string): string {
// Drop the patch version
const [major, minor, patch] = version.split(".")
let versionFolder = `v${major}.${minor}`
if (major === "0" && minor === "0") {
// We're still in the 0.0.x phase, so we use the patch version
versionFolder = `v0.0.${patch}`
if (major === "0" && patch !== undefined) {
// We're still in the 0.x.y phase, so we use the full version
versionFolder = `v${major}.${minor}.${patch}`
}
// Read the image ID from the JSON file on the filesystem
return `./impl/${impl}/${versionFolder}/image.json`
Expand Down
6 changes: 6 additions & 0 deletions hole-punch-interop/versionsInput.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
"transports": ["tcp", "quic"],
"secureChannels": [],
"muxers": []
},
{
"id": "python-v0.4.0",
"transports": ["tcp", "quic"],
"secureChannels": [],
"muxers": []
}
]
Loading