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
42 changes: 42 additions & 0 deletions docs/HOLE_PUNCHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Hole Punching Implementation for py-libp2p

## What This Adds

This implementation adds hole punching capability to py-libp2p, allowing peers behind NATs to connect directly.

## Components

- **DCUtR Protocol**: Coordinates hole punching between peers
- **AutoNAT Service**: Detects if peer is behind NAT (basic implementation)
- **Examples**: Working code showing how to use hole punching

## Quick Start

1. Install py-libp2p with hole punching:

```bash
pip install -e .[dev]
```

2. Run basic example:

```bash
# Terminal 1
python examples/hole_punching/basic_example.py --mode listen

# Terminal 2 (use peer ID from terminal 1)
python examples/hole_punching/basic_example.py --mode dial --target PEER_ID
```

## Current Status

-Basic DCUtR protocol implementation

- Working example code
- Basic tests

## Testing

```bash
pytest tests/interop/ -v
```
77 changes: 77 additions & 0 deletions examples/hole_punching/basic_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import argparse

from multiaddr import Multiaddr
import trio

from libp2p import new_host
from libp2p.peer.peerinfo import info_from_p2p_addr
from libp2p.protocols.dcutr.dcutr import DCUTR_PROTOCOL_ID, DCUtRProtocol


async def run_listener():
"""Run as listener peer"""
print("Starting listener...")

host = new_host()
dcutr = DCUtRProtocol(host)

# Register DCUtR handler
host.set_stream_handler(DCUTR_PROTOCOL_ID, dcutr.handle_inbound_stream) # type: ignore

listen_addr = Multiaddr("/ip4/127.0.0.1/tcp/4002")

async with host.run(listen_addrs=[listen_addr]):
print(f"Listener ID: {host.get_id()}")
print(f"Addresses: {host.get_addrs()}")

# Keep running
await trio.sleep_forever()


async def run_dialer(target_id):
"""Run as dialer peer"""
print("Starting dialer...")

host = new_host()
dcutr = DCUtRProtocol(host)

host.set_stream_handler(DCUTR_PROTOCOL_ID, dcutr.handle_inbound_stream) # type: ignore

listen_addr = Multiaddr("/ip4/127.0.0.1/tcp/4003")

async with host.run(listen_addrs=[listen_addr]):
print(f"Dialer ID: {host.get_id()}")

try:
# Connect to target
target_addr = Multiaddr(f"/ip4/127.0.0.1/tcp/4002/p2p/{target_id}")
peer_info = info_from_p2p_addr(target_addr)
await host.connect(peer_info)
print("Connected!")

# Try hole punch
success = await dcutr.upgrade_connection(peer_info.peer_id)
print(f"Hole punch result: {success}")

except Exception as e:
print(f"Error: {e}")


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["listen", "dial"], required=True)
parser.add_argument("--target", help="Target peer ID for dial mode")

args = parser.parse_args()

if args.mode == "listen":
trio.run(run_listener)
else:
if not args.target:
print("Need --target for dial mode")
return
trio.run(run_dialer, args.target)


if __name__ == "__main__":
main()
29 changes: 29 additions & 0 deletions libp2p/protocols/autonat/pb/autonat.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
syntax = "proto2";

package autonat.pb;

message Message {
enum MessageType {
DIAL = 0;
DIAL_RESPONSE = 1;
}

enum ResponseStatus {
OK = 0;
E_DIAL_ERROR = 100;
}

message Dial {
required bytes peer_id = 1;
repeated bytes addrs = 2;
}

message DialResponse {
required ResponseStatus status = 1;
optional bytes addr = 2;
}

required MessageType type = 1;
optional Dial dial = 2;
optional DialResponse dial_response = 3;
}
33 changes: 33 additions & 0 deletions libp2p/protocols/autonat/pb/autonat_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions libp2p/protocols/dcutr/dcutr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import logging
from typing import Any

from libp2p.abc import IHost, INetStream
from libp2p.relay.circuit_v2.pb.dcutr_pb2 import HolePunch

DCUTR_PROTOCOL_ID = "/libp2p/dcutr/1.0.0"
logger = logging.getLogger(__name__)


class DCUtRProtocol:
def __init__(self, host: IHost) -> None:
self.host = host

async def handle_inbound_stream(self, stream: INetStream) -> None:
"""Handle incoming DCUtR stream"""
logger.info("Handling DCUtR stream")

try:
# Read CONNECT message
msg = await self._read_message(stream)
if msg.type == HolePunch.CONNECT: # type: ignore
await self._handle_connect(stream, msg)
except Exception as e:
logger.error(f"DCUtR error: {e}")
finally:
await stream.close()

async def upgrade_connection(self, peer_id: Any) -> bool:
"""Start hole punching with peer"""
logger.info(f"Starting hole punch to {peer_id}")

try:
# Open DCUtR stream
stream = await self.host.new_stream(peer_id, [DCUTR_PROTOCOL_ID]) # type: ignore

# Send CONNECT message
connect_msg = HolePunch() # type: ignore
connect_msg.type = HolePunch.CONNECT # type: ignore
# Add our addresses (simplified)
connect_msg.ObsAddrs.append(b"/ip4/127.0.0.1/tcp/0")

await self._write_message(stream, connect_msg)

# Read response
await self._read_message(stream)

# Send SYNC and attempt connections
sync_msg = HolePunch() # type: ignore
sync_msg.type = HolePunch.SYNC # type: ignore
await self._write_message(stream, sync_msg)

logger.info("Hole punch attempt completed")
return True

except Exception as e:
logger.error(f"Hole punch failed: {e}")
return False

async def _handle_connect(self, stream: INetStream, msg: Any) -> None:
"""Handle CONNECT message"""
# Send our CONNECT response
response = HolePunch() # type: ignore
response.type = HolePunch.CONNECT # type: ignore
response.ObsAddrs.append(b"/ip4/127.0.0.1/tcp/0")

await self._write_message(stream, response)

# Wait for SYNC
await self._read_message(stream)
logger.info("Received SYNC, starting hole punch")

async def _read_message(self, stream: INetStream) -> Any:
"""Read protobuf message from stream"""
# Simple message reading (length-prefixed)
length_bytes = await stream.read(1)
if not length_bytes:
raise ValueError("Stream closed")

length = length_bytes[0] # Convert first byte to integer
data = await stream.read(length)

msg = HolePunch() # type: ignore
msg.ParseFromString(data)
return msg

async def _write_message(self, stream: INetStream, msg: Any) -> None:
"""Write protobuf message to stream"""
data = msg.SerializeToString()
await stream.write(bytes([len(data)]) + data)
12 changes: 12 additions & 0 deletions libp2p/protocols/dcutr/pb/holepunch.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
syntax = "proto2";

package holepunch.pb;

message HolePunch {
enum Type {
CONNECT = 100;
SYNC = 300;
}
required Type type = 1;
repeated bytes ObsAddrs = 2;
}
27 changes: 27 additions & 0 deletions libp2p/protocols/dcutr/pb/holepunch_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ libp2p = ["py.typed"]


[tool.mypy]
exclude = [
"libp2p/protocols/autonat/pb/autonat_pb2.py"
]
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
Expand Down
Loading