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
123 changes: 95 additions & 28 deletions libp2p/relay/circuit_v2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
dataclass,
field,
)
from enum import Flag, auto

from libp2p.peer.peerinfo import (
PeerInfo,
Expand All @@ -18,38 +19,104 @@
RelayLimits,
)

DEFAULT_MIN_RELAYS = 3
DEFAULT_MAX_RELAYS = 20
DEFAULT_DISCOVERY_INTERVAL = 300 # seconds
DEFAULT_RESERVATION_TTL = 3600 # seconds
DEFAULT_MAX_CIRCUIT_DURATION = 3600 # seconds
DEFAULT_MAX_CIRCUIT_BYTES = 1024 * 1024 * 1024 # 1GB

DEFAULT_MAX_CIRCUIT_CONNS = 8
DEFAULT_MAX_RESERVATIONS = 4

MAX_RESERVATIONS_PER_IP = 8
MAX_CIRCUITS_PER_IP = 16
RESERVATION_RATE_PER_IP = 4 # per minute
CIRCUIT_RATE_PER_IP = 8 # per minute
MAX_CIRCUITS_TOTAL = 64
MAX_RESERVATIONS_TOTAL = 32
MAX_BANDWIDTH_PER_CIRCUIT = 1024 * 1024 # 1MB/s
MAX_BANDWIDTH_TOTAL = 10 * 1024 * 1024 # 10MB/s

MIN_RELAY_SCORE = 0.5
MAX_RELAY_LATENCY = 1.0 # seconds
ENABLE_AUTO_RELAY = True
AUTO_RELAY_TIMEOUT = 30 # seconds
MAX_AUTO_RELAY_ATTEMPTS = 3
RESERVATION_REFRESH_THRESHOLD = 0.8 # Refresh at 80% of TTL
MAX_CONCURRENT_RESERVATIONS = 2

# Shared timeout constants (used across modules)
STREAM_READ_TIMEOUT = 15 # seconds
STREAM_WRITE_TIMEOUT = 15 # seconds
STREAM_CLOSE_TIMEOUT = 10 # seconds
DIAL_TIMEOUT = 10 # seconds

# NAT reachability timeout
REACHABILITY_TIMEOUT = 10 # seconds

# Relay roles enum -----------------------------------------------------------


class RelayRole(Flag):
"""
Bit-flag enum that captures the three possible relay capabilities.

A node can combine multiple roles using bit-wise OR, for example::

RelayRole.HOP | RelayRole.STOP
"""

HOP = auto() # Act as a relay for others ("hop")
STOP = auto() # Accept relayed connections ("stop")
CLIENT = auto() # Dial through existing relays ("client")


@dataclass
class RelayConfig:
"""Configuration for Circuit Relay v2."""

# Role configuration
enable_hop: bool = False # Whether to act as a relay (hop)
enable_stop: bool = True # Whether to accept relayed connections (stop)
enable_client: bool = True # Whether to use relays for dialing
# Role configuration (bit-flags)
roles: RelayRole = RelayRole.STOP | RelayRole.CLIENT

# Resource limits
limits: RelayLimits | None = None

# Discovery configuration
bootstrap_relays: list[PeerInfo] = field(default_factory=list)
min_relays: int = 3
max_relays: int = 20
discovery_interval: int = 300 # seconds
min_relays: int = DEFAULT_MIN_RELAYS
max_relays: int = DEFAULT_MAX_RELAYS
discovery_interval: int = DEFAULT_DISCOVERY_INTERVAL

# Connection configuration
reservation_ttl: int = 3600 # seconds
max_circuit_duration: int = 3600 # seconds
max_circuit_bytes: int = 1024 * 1024 * 1024 # 1GB
reservation_ttl: int = DEFAULT_RESERVATION_TTL
max_circuit_duration: int = DEFAULT_MAX_CIRCUIT_DURATION
max_circuit_bytes: int = DEFAULT_MAX_CIRCUIT_BYTES

# ---------------------------------------------------------------------
# Backwards-compat boolean helpers. Existing code that still accesses
# ``cfg.enable_hop, cfg.enable_stop, cfg.enable_client`` will continue to work.
# ---------------------------------------------------------------------

@property
def enable_hop(self) -> bool: # pragma: no cover – helper
return bool(self.roles & RelayRole.HOP)

@property
def enable_stop(self) -> bool: # pragma: no cover – helper
return bool(self.roles & RelayRole.STOP)

@property
def enable_client(self) -> bool: # pragma: no cover – helper
return bool(self.roles & RelayRole.CLIENT)

def __post_init__(self) -> None:
"""Initialize default values."""
if self.limits is None:
self.limits = RelayLimits(
duration=self.max_circuit_duration,
data=self.max_circuit_bytes,
max_circuit_conns=8,
max_reservations=4,
max_circuit_conns=DEFAULT_MAX_CIRCUIT_CONNS,
max_reservations=DEFAULT_MAX_RESERVATIONS,
)


Expand All @@ -58,35 +125,35 @@ class HopConfig:
"""Configuration specific to relay (hop) nodes."""

# Resource limits per IP
max_reservations_per_ip: int = 8
max_circuits_per_ip: int = 16
max_reservations_per_ip: int = MAX_RESERVATIONS_PER_IP
max_circuits_per_ip: int = MAX_CIRCUITS_PER_IP

# Rate limiting
reservation_rate_per_ip: int = 4 # per minute
circuit_rate_per_ip: int = 8 # per minute
reservation_rate_per_ip: int = RESERVATION_RATE_PER_IP
circuit_rate_per_ip: int = CIRCUIT_RATE_PER_IP

# Resource quotas
max_circuits_total: int = 64
max_reservations_total: int = 32
max_circuits_total: int = MAX_CIRCUITS_TOTAL
max_reservations_total: int = MAX_RESERVATIONS_TOTAL

# Bandwidth limits
max_bandwidth_per_circuit: int = 1024 * 1024 # 1MB/s
max_bandwidth_total: int = 10 * 1024 * 1024 # 10MB/s
max_bandwidth_per_circuit: int = MAX_BANDWIDTH_PER_CIRCUIT
max_bandwidth_total: int = MAX_BANDWIDTH_TOTAL


@dataclass
class ClientConfig:
"""Configuration specific to relay clients."""

# Relay selection
min_relay_score: float = 0.5
max_relay_latency: float = 1.0 # seconds
min_relay_score: float = MIN_RELAY_SCORE
max_relay_latency: float = MAX_RELAY_LATENCY

# Auto-relay settings
enable_auto_relay: bool = True
auto_relay_timeout: int = 30 # seconds
max_auto_relay_attempts: int = 3
enable_auto_relay: bool = ENABLE_AUTO_RELAY
auto_relay_timeout: int = AUTO_RELAY_TIMEOUT
max_auto_relay_attempts: int = MAX_AUTO_RELAY_ATTEMPTS

# Reservation management
reservation_refresh_threshold: float = 0.8 # Refresh at 80% of TTL
max_concurrent_reservations: int = 2
reservation_refresh_threshold: float = RESERVATION_REFRESH_THRESHOLD
max_concurrent_reservations: int = MAX_CONCURRENT_RESERVATIONS
11 changes: 6 additions & 5 deletions libp2p/relay/circuit_v2/dcutr.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
Service,
)

from .config import (
DIAL_TIMEOUT,
STREAM_READ_TIMEOUT,
STREAM_WRITE_TIMEOUT,
)

logger = logging.getLogger(__name__)

# Protocol ID for DCUtR
Expand All @@ -47,11 +53,6 @@
# Maximum message size for DCUtR (4KiB as per spec)
MAX_MESSAGE_SIZE = 4 * 1024

# Timeouts
STREAM_READ_TIMEOUT = 30 # seconds
STREAM_WRITE_TIMEOUT = 30 # seconds
DIAL_TIMEOUT = 10 # seconds

# Maximum number of hole punch attempts per peer
MAX_HOLE_PUNCH_ATTEMPTS = 5

Expand Down
20 changes: 12 additions & 8 deletions libp2p/relay/circuit_v2/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
Service,
)

from .config import (
DEFAULT_DISCOVERY_INTERVAL as CFG_DISCOVERY_INTERVAL,
)
from .pb.circuit_pb2 import (
HopMessage,
)
Expand All @@ -43,10 +46,11 @@

logger = logging.getLogger("libp2p.relay.circuit_v2.discovery")

# Constants
MAX_RELAYS_TO_TRACK = 10
DEFAULT_DISCOVERY_INTERVAL = 60 # seconds
# Constants (single-source-of-truth)
DEFAULT_DISCOVERY_INTERVAL = CFG_DISCOVERY_INTERVAL
MAX_RELAYS_TO_TRACK = 10 # Still discovery-specific
STREAM_TIMEOUT = 10 # seconds
PEER_PROTOCOL_TIMEOUT = 5 # seconds


# Extended interfaces for type checking
Expand Down Expand Up @@ -165,20 +169,20 @@ async def discover_relays(self) -> None:
self._discovered_relays[peer_id].last_seen = time.time()
continue

# Check if peer supports the relay protocol
with trio.move_on_after(5): # Don't wait too long for protocol info
# Don't wait too long for protocol info
with trio.move_on_after(PEER_PROTOCOL_TIMEOUT):
if await self._supports_relay_protocol(peer_id):
await self._add_relay(peer_id)

# Limit number of relays we track
if len(self._discovered_relays) > self.max_relays:
if len(self._discovered_relays) > MAX_RELAYS_TO_TRACK:
# Sort by last seen time and keep only the most recent ones
sorted_relays = sorted(
self._discovered_relays.items(),
key=lambda x: x[1].last_seen,
reverse=True,
)
to_remove = sorted_relays[self.max_relays :]
to_remove = sorted_relays[MAX_RELAYS_TO_TRACK:]
for peer_id, _ in to_remove:
del self._discovered_relays[peer_id]

Expand Down Expand Up @@ -463,7 +467,7 @@ async def _cleanup_expired(self) -> None:

for peer_id, relay_info in self._discovered_relays.items():
# Check if relay hasn't been seen in a while (3x discovery interval)
if now - relay_info.last_seen > self.discovery_interval * 3:
if now - relay_info.last_seen > DEFAULT_DISCOVERY_INTERVAL * 3:
to_remove.append(peer_id)
continue

Expand Down
5 changes: 2 additions & 3 deletions libp2p/relay/circuit_v2/nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
ID,
)

logger = logging.getLogger("libp2p.relay.circuit_v2.nat")
# REACHABILITY_TIMEOUT now imported from config

# Timeout for reachability checks
REACHABILITY_TIMEOUT = 10 # seconds
logger = logging.getLogger("libp2p.relay.circuit_v2.nat")

# Define private IP ranges
PRIVATE_IP_RANGES = [
Expand Down
Loading
Loading