Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b840eaa
Implement advanced network discovery example and address validation u…
Aug 8, 2025
fa17423
Refactor echo example to use optimal binding address
Aug 8, 2025
59a898c
Add tests for echo example and address validation utilities
Aug 8, 2025
09cd8b3
Merge branch 'main' into feat/804-add-thin-waist-address
yashksaini-coder Aug 10, 2025
a14c42e
Merge branch 'main' into feat/804-add-thin-waist-address
yashksaini-coder Aug 10, 2025
9a0f224
Merge branch 'libp2p:main' into feat/804-add-thin-waist-address
yashksaini-coder Aug 18, 2025
b363d1d
fix: update listening address handling to use all available interfaces
yashksaini-coder Aug 18, 2025
a2fcf33
refactor: migrate echo example test to use Trio for process handling
yashksaini-coder Aug 18, 2025
9378490
fix: ensure loopback addresses are included in available interfaces
yashksaini-coder Aug 18, 2025
fe71c47
Merge branch 'main' into feat/804-add-thin-waist-address
seetadev Aug 18, 2025
05b372b
Fix linting and type checking issues for Thin Waist feature
acul71 Aug 18, 2025
7f6469d
Merge remote-tracking branch 'acul71/feat/804-add-thin-waist-address'…
yashksaini-coder Aug 19, 2025
55dd883
Merge branch 'main' into feat/804-add-thin-waist-address
yashksaini-coder Aug 19, 2025
a1b1624
fix: correct listening address variable in echo example and streamlin…
yashksaini-coder Aug 19, 2025
3ff5728
Merge branch 'feat/804-add-thin-waist-address' of
yashksaini-coder Aug 19, 2025
69d5274
fix: update listening address parameter in echo example to accept a list
yashksaini-coder Aug 19, 2025
905f3a5
Merge branch 'main' into feat/804-add-thin-waist-address
yashksaini-coder Aug 20, 2025
8a2d1f7
Merge branch 'main' into feat/804-add-thin-waist-address
yashksaini-coder Aug 20, 2025
c2c91b8
refactor: Improve comment formatting in test_echo_thin_waist.py for c…
yashksaini-coder Aug 20, 2025
5b9bec8
fix: Enhance error handling in echo stream handler to manage stream c…
yashksaini-coder Aug 20, 2025
ed2716c
feat: Enhance echo example to dynamically find free ports and improve…
yashksaini-coder Aug 22, 2025
b6cbd78
Fix multi-address listening bug in swarm.listen()
acul71 Aug 23, 2025
3bd6d1f
doc: add newsfragment
acul71 Aug 24, 2025
b38d504
Merge pull request #1 from acul71/fix/multi-address-listening-bug
yashksaini-coder Aug 24, 2025
88a1f0a
cherry pick https://github.com/acul71/py-libp2p-fork/blob/7a1198c8c6e…
yashksaini-coder Aug 24, 2025
cf48d2e
chore(app): Add 811.internal.rst
yashksaini-coder Aug 24, 2025
75ffb79
fix: Ensure newline at end of file in address_validation.py and updat…
yashksaini-coder Aug 24, 2025
ed91ee0
refactor(app): 804 refactored find_free_port() in address_validation.py
yashksaini-coder Aug 24, 2025
63a8458
add import to __init__
yashksaini-coder Aug 24, 2025
fde8c8f
Merge branch 'main' into feat/804-add-thin-waist-address
yashksaini-coder Aug 24, 2025
6a0a7c2
chore(app): Add newsfragment for 811.feature.rst
yashksaini-coder Aug 24, 2025
6c6adf7
chore(app): 804 Suggested changes - Remove the comment
yashksaini-coder Aug 25, 2025
c9795e3
Merge branch 'main' into feat/804-add-thin-waist-address
seetadev Aug 25, 2025
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
63 changes: 63 additions & 0 deletions examples/advanced/network_discover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Advanced demonstration of Thin Waist address handling.

Run:
python -m examples.advanced.network_discovery
"""

from __future__ import annotations

from multiaddr import Multiaddr

try:
from libp2p.utils.address_validation import (
expand_wildcard_address,
get_available_interfaces,
get_optimal_binding_address,
)
except ImportError:
# Fallbacks if utilities are missing
def get_available_interfaces(port: int, protocol: str = "tcp"):
return [Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")]

def expand_wildcard_address(addr: Multiaddr, port: int | None = None):
if port is None:
return [addr]
addr_str = str(addr).rsplit("/", 1)[0]
return [Multiaddr(addr_str + f"/{port}")]

def get_optimal_binding_address(port: int, protocol: str = "tcp"):
return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")


def main() -> None:
port = 8080
interfaces = get_available_interfaces(port)
print(f"Discovered interfaces for port {port}:")
for a in interfaces:
print(f" - {a}")

wildcard_v4 = Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")
expanded_v4 = expand_wildcard_address(wildcard_v4)
print("\nExpanded IPv4 wildcard:")
for a in expanded_v4:
print(f" - {a}")

wildcard_v6 = Multiaddr(f"/ip6/::/tcp/{port}")
expanded_v6 = expand_wildcard_address(wildcard_v6)
print("\nExpanded IPv6 wildcard:")
for a in expanded_v6:
print(f" - {a}")

print("\nOptimal binding address heuristic result:")
print(f" -> {get_optimal_binding_address(port)}")

override_port = 9000
overridden = expand_wildcard_address(wildcard_v4, port=override_port)
print(f"\nPort override expansion to {override_port}:")
for a in overridden:
print(f" - {a}")


if __name__ == "__main__":
main()
55 changes: 42 additions & 13 deletions examples/echo/echo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import argparse
import random
import secrets
import socket

import multiaddr
import trio
Expand All @@ -12,40 +15,61 @@
from libp2p.custom_types import (
TProtocol,
)
from libp2p.network.stream.exceptions import (
StreamEOF,
)
from libp2p.network.stream.net_stream import (
INetStream,
)
from libp2p.peer.peerinfo import (
info_from_p2p_addr,
)
from libp2p.utils.address_validation import (
get_available_interfaces,
)

PROTOCOL_ID = TProtocol("/echo/1.0.0")
MAX_READ_LEN = 2**32 - 1


def find_free_port():
"""Find a free port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0)) # Bind to a free port provided by the OS
return s.getsockname()[1]


async def _echo_stream_handler(stream: INetStream) -> None:
# Wait until EOF
msg = await stream.read(MAX_READ_LEN)
await stream.write(msg)
await stream.close()
try:
peer_id = stream.muxed_conn.peer_id
print(f"Received connection from {peer_id}")
# Wait until EOF
msg = await stream.read(MAX_READ_LEN)
print(f"Echoing message: {msg.decode('utf-8')}")
await stream.write(msg)
except StreamEOF:
print("Stream closed by remote peer.")
except Exception as e:
print(f"Error in echo handler: {e}")
finally:
await stream.close()


async def run(port: int, destination: str, seed: int | None = None) -> None:
listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")
# CHANGED: previously hardcoded 0.0.0.0
if port <= 0:
port = find_free_port()
listen_addr = get_available_interfaces(port)

if seed:
import random

random.seed(seed)
secret_number = random.getrandbits(32 * 8)
secret = secret_number.to_bytes(length=32, byteorder="big")
else:
import secrets

secret = secrets.token_bytes(32)

host = new_host(key_pair=create_new_key_pair(secret))
async with host.run(listen_addrs=[listen_addr]), trio.open_nursery() as nursery:
async with host.run(listen_addrs=listen_addr), trio.open_nursery() as nursery:
# Start the peer-store cleanup task
nursery.start_soon(host.get_peerstore().start_cleanup_task, 60)

Expand All @@ -54,10 +78,15 @@ async def run(port: int, destination: str, seed: int | None = None) -> None:
if not destination: # its the server
host.set_stream_handler(PROTOCOL_ID, _echo_stream_handler)

# Print all listen addresses with peer ID (JS parity)
print("Listener ready, listening on:\n")
peer_id = host.get_id().to_string()
for addr in listen_addr:
print(f"{addr}/p2p/{peer_id}")

print(
"Run this from the same folder in another console:\n\n"
f"echo-demo "
f"-d {host.get_addrs()[0]}\n"
"\nRun this from the same folder in another console:\n\n"
f"echo-demo -d {host.get_addrs()[0]}\n"
)
print("Waiting for incoming connections...")
await trio.sleep_forever()
Expand Down
11 changes: 7 additions & 4 deletions libp2p/network/swarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,11 @@ async def listen(self, *multiaddrs: Multiaddr) -> bool:
# We need to wait until `self.listener_nursery` is created.
await self.event_listener_nursery_created.wait()

success_count = 0
for maddr in multiaddrs:
if str(maddr) in self.listeners:
return True
success_count += 1
continue

async def conn_handler(
read_write_closer: ReadWriteCloser, maddr: Multiaddr = maddr
Expand Down Expand Up @@ -302,13 +304,14 @@ async def conn_handler(
# Call notifiers since event occurred
await self.notify_listen(maddr)

return True
success_count += 1
logger.debug("successfully started listening on: %s", maddr)
except OSError:
# Failed. Continue looping.
logger.debug("fail to listen on: %s", maddr)

# No maddr succeeded
return False
# Return true if at least one address succeeded
return success_count > 0

async def close(self) -> None:
"""
Expand Down
9 changes: 9 additions & 0 deletions libp2p/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
get_agent_version,
)

from libp2p.utils.address_validation import (
get_available_interfaces,
get_optimal_binding_address,
expand_wildcard_address,
)

__all__ = [
"decode_uvarint_from_stream",
"encode_delim",
Expand All @@ -26,4 +32,7 @@
"decode_varint_from_bytes",
"decode_varint_with_size",
"read_length_prefixed_protobuf",
"get_available_interfaces",
"get_optimal_binding_address",
"expand_wildcard_address",
]
150 changes: 150 additions & 0 deletions libp2p/utils/address_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from __future__ import annotations

from multiaddr import Multiaddr

try:
from multiaddr.utils import ( # type: ignore
get_network_addrs,
get_thin_waist_addresses,
)

_HAS_THIN_WAIST = True
except ImportError: # pragma: no cover - only executed in older environments
_HAS_THIN_WAIST = False
get_thin_waist_addresses = None # type: ignore
get_network_addrs = None # type: ignore


def _safe_get_network_addrs(ip_version: int) -> list[str]:
"""
Internal safe wrapper. Returns a list of IP addresses for the requested IP version.
Falls back to minimal defaults when Thin Waist helpers are missing.

:param ip_version: 4 or 6
"""
if _HAS_THIN_WAIST and get_network_addrs:
try:
return get_network_addrs(ip_version) or []
except Exception: # pragma: no cover - defensive
return []
# Fallback behavior (very conservative)
if ip_version == 4:
return ["127.0.0.1"]
if ip_version == 6:
return ["::1"]
return []


def _safe_expand(addr: Multiaddr, port: int | None = None) -> list[Multiaddr]:
"""
Internal safe expansion wrapper. Returns a list of Multiaddr objects.
If Thin Waist isn't available, returns [addr] (identity).
"""
if _HAS_THIN_WAIST and get_thin_waist_addresses:
try:
if port is not None:
return get_thin_waist_addresses(addr, port=port) or []
return get_thin_waist_addresses(addr) or []
except Exception: # pragma: no cover - defensive
return [addr]
return [addr]


def get_available_interfaces(port: int, protocol: str = "tcp") -> list[Multiaddr]:
"""
Discover available network interfaces (IPv4 + IPv6 if supported) for binding.

:param port: Port number to bind to.
:param protocol: Transport protocol (e.g., "tcp" or "udp").
:return: List of Multiaddr objects representing candidate interface addresses.
"""
addrs: list[Multiaddr] = []

# IPv4 enumeration
seen_v4: set[str] = set()

for ip in _safe_get_network_addrs(4):
seen_v4.add(ip)
addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}"))

# Ensure IPv4 loopback is always included when IPv4 interfaces are discovered
if seen_v4 and "127.0.0.1" not in seen_v4:
addrs.append(Multiaddr(f"/ip4/127.0.0.1/{protocol}/{port}"))

# TODO: IPv6 support temporarily disabled due to libp2p handshake issues
# IPv6 connections fail during protocol negotiation (SecurityUpgradeFailure)
# Re-enable IPv6 support once the following issues are resolved:
# - libp2p security handshake over IPv6
# - multiselect protocol over IPv6
# - connection establishment over IPv6
#
# seen_v6: set[str] = set()
# for ip in _safe_get_network_addrs(6):
# seen_v6.add(ip)
# addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}"))
#
# # Always include IPv6 loopback for testing purposes when IPv6 is available
# # This ensures IPv6 functionality can be tested even without global IPv6 addresses
# if "::1" not in seen_v6:
# addrs.append(Multiaddr(f"/ip6/::1/{protocol}/{port}"))

# Fallback if nothing discovered
if not addrs:
addrs.append(Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}"))

return addrs


def expand_wildcard_address(
addr: Multiaddr, port: int | None = None
) -> list[Multiaddr]:
"""
Expand a wildcard (e.g. /ip4/0.0.0.0/tcp/0) into all concrete interfaces.

:param addr: Multiaddr to expand.
:param port: Optional override for port selection.
:return: List of concrete Multiaddr instances.
"""
expanded = _safe_expand(addr, port=port)
if not expanded: # Safety fallback
return [addr]
return expanded


def get_optimal_binding_address(port: int, protocol: str = "tcp") -> Multiaddr:
"""
Choose an optimal address for an example to bind to:
- Prefer non-loopback IPv4
- Then non-loopback IPv6
- Fallback to loopback
- Fallback to wildcard

:param port: Port number.
:param protocol: Transport protocol.
:return: A single Multiaddr chosen heuristically.
"""
candidates = get_available_interfaces(port, protocol)

def is_non_loopback(ma: Multiaddr) -> bool:
s = str(ma)
return not ("/ip4/127." in s or "/ip6/::1" in s)

for c in candidates:
if "/ip4/" in str(c) and is_non_loopback(c):
return c
for c in candidates:
if "/ip6/" in str(c) and is_non_loopback(c):
return c
for c in candidates:
if "/ip4/127." in str(c) or "/ip6/::1" in str(c):
return c

# As a final fallback, produce a wildcard
return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")


__all__ = [
"get_available_interfaces",
"get_optimal_binding_address",
"expand_wildcard_address",
]
7 changes: 7 additions & 0 deletions newsfragments/811.internal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Add Thin Waist address validation utilities and integrate into echo example

- Add ``libp2p/utils/address_validation.py`` with dynamic interface discovery
- Implement ``get_available_interfaces()``, ``get_optimal_binding_address()``, and ``expand_wildcard_address()``
- Update echo example to use dynamic address discovery instead of hardcoded wildcard
- Add safe fallbacks for environments lacking Thin Waist support
- Temporarily disable IPv6 support due to libp2p handshake issues (TODO: re-enable when resolved)
5 changes: 5 additions & 0 deletions newsfragments/863.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix multi-address listening bug in swarm.listen()

- Fix early return in swarm.listen() that prevented listening on all addresses
- Add comprehensive tests for multi-address listening functionality
- Ensure all available interfaces are properly bound and connectable
Loading