Skip to content

Commit 4f5310f

Browse files
authored
Merge branch 'main' into fix/pubsub-race-conditions-418
2 parents acc3aba + 530a910 commit 4f5310f

File tree

13 files changed

+533
-64
lines changed

13 files changed

+533
-64
lines changed

docs/getting_started.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,36 @@ Now that you have configured a **Transport**, **Crypto** and **Stream Multiplexe
7979
.. literalinclude:: ../examples/doc-examples/example_running.py
8080
:language: python
8181

82+
Bind address and IPv6
83+
^^^^^^^^^^^^^^^^^^^^^
84+
85+
Default listen addresses are controlled by environment variables. Use ``LIBP2P_BIND`` for IPv4 (default ``127.0.0.1``) and ``LIBP2P_BIND_V6`` for IPv6 (default ``::1``). Invalid values fall back to these secure defaults.
86+
87+
**IPv4 (LIBP2P_BIND):**
88+
89+
.. code-block:: bash
90+
91+
# Listen on all IPv4 interfaces (e.g. for tests)
92+
export LIBP2P_BIND=0.0.0.0
93+
python your_script.py
94+
95+
**IPv6 (LIBP2P_BIND_V6):**
96+
97+
.. code-block:: bash
98+
99+
# Use default IPv6 loopback (::1)
100+
python your_script.py
101+
102+
# Listen on all IPv6 interfaces (e.g. for tests or dual-stack)
103+
export LIBP2P_BIND_V6=::
104+
python your_script.py
105+
106+
# Custom IPv6 address
107+
export LIBP2P_BIND_V6=fd00::1
108+
python your_script.py
109+
110+
Multiaddr formats for IPv6 include ``/ip6/::1/tcp/PORT`` and ``/ip6/::1/tcp/PORT/ws`` for WebSocket.
111+
82112
Resource Management
83113
^^^^^^^^^^^^^^^^^^^
84114

docs/release_notes.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ Internal Changes - for py-libp2p Contributors
391391
- Implement ``get_available_interfaces()``, ``get_optimal_binding_address()``, and ``expand_wildcard_address()``
392392
- Update echo example to use dynamic address discovery instead of hardcoded wildcard
393393
- Add safe fallbacks for environments lacking Thin Waist support
394-
- Temporarily disable IPv6 support due to libp2p handshake issues (TODO: re-enable when resolved) (`#811 <https://github.com/libp2p/py-libp2p/issues/811>`__)
394+
- Temporarily disable IPv6 support due to libp2p handshake issues (re-enabled later; use ``LIBP2P_BIND_V6`` to configure IPv6 bind address) (`#811 <https://github.com/libp2p/py-libp2p/issues/811>`__)
395395
- The TODO IK patterns in Noise has been deprecated in specs: https://github.com/libp2p/specs/tree/master/noise#handshake-pattern (`#816 <https://github.com/libp2p/py-libp2p/issues/816>`__)
396396
- Remove the already completed TODO tasks in Peerstore:
397397
TODO: Set up an async task for periodic peer-store cleanup for expired addresses and records.

ed25519

Lines changed: 0 additions & 1 deletion
This file was deleted.

libp2p/network/swarm.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
from libp2p.transport.upgrader import (
5555
TransportUpgrader,
5656
)
57+
from libp2p.utils.multiaddr_utils import (
58+
extract_ip_from_multiaddr,
59+
)
5760

5861
from ..exceptions import (
5962
MultiError,
@@ -347,11 +350,7 @@ async def _dial_addr_single_attempt(self, addr: Multiaddr, peer_id: ID) -> INetC
347350
pre_scope = None
348351
if self._resource_manager is not None:
349352
try:
350-
ep = None
351-
try:
352-
ep = addr.value_for_protocol("ip4")
353-
except Exception:
354-
ep = None
353+
ep = extract_ip_from_multiaddr(addr)
355354
pre_scope = self._resource_manager.open_connection(None, endpoint_ip=ep)
356355
if pre_scope is None:
357356
raise SwarmException("Connection denied by resource manager")

libp2p/tools/constants.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
def _validate_ipv4_address(address: str) -> str:
2727
"""
28-
Validate that the given address is a valid IPv4 address.
28+
Validate that a given address is a valid IPv4 address.
2929
3030
Args:
3131
address: The IP address string to validate
@@ -35,12 +35,30 @@ def _validate_ipv4_address(address: str) -> str:
3535
3636
"""
3737
try:
38-
# Validate that it's a valid IPv4 address
38+
# Validate that the given address is a valid IPv4 address
3939
ipaddress.IPv4Address(address)
4040
return address
4141
except (ipaddress.AddressValueError, ValueError):
42-
# If invalid, return the secure default
43-
return "127.0.0.1"
42+
return "127.0.0.1" # If invalid, return to the secure default
43+
44+
45+
def _validate_ipv6_address(address: str) -> str:
46+
"""
47+
Validate that a given address is a valid IPv6 address.
48+
49+
Args:
50+
address: The IP address string to validate
51+
52+
Returns:
53+
The validated IPv6 address, or "::1" if invalid
54+
55+
"""
56+
try:
57+
# Validate that the given address is a valid IPv6 address
58+
ipaddress.IPv6Address(address)
59+
return address
60+
except (ipaddress.AddressValueError, ValueError):
61+
return "::1" # If invalid, return to the secure default
4462

4563

4664
# Default bind address configuration with environment variable override
@@ -50,6 +68,13 @@ def _validate_ipv4_address(address: str) -> str:
5068
DEFAULT_BIND_ADDRESS = _validate_ipv4_address(os.getenv("LIBP2P_BIND", "127.0.0.1"))
5169
LISTEN_MADDR = multiaddr.Multiaddr(f"/ip4/{DEFAULT_BIND_ADDRESS}/tcp/0")
5270

71+
# IPv6 default bind address configuration with environment variable override
72+
# DEFAULT_BIND_ADDRESS_V6 defaults to "::1" (secure) but can be overridden
73+
# via LIBP2P_BIND_V6 environment variable (e.g., "::" for tests)
74+
# Invalid IPv6 addresses will fallback to "::1"
75+
DEFAULT_BIND_ADDRESS_V6 = _validate_ipv6_address(os.getenv("LIBP2P_BIND_V6", "::1"))
76+
LISTEN_MADDR_V6 = multiaddr.Multiaddr(f"/ip6/{DEFAULT_BIND_ADDRESS_V6}/tcp/0")
77+
5378

5479
FLOODSUB_PROTOCOL_ID = floodsub.PROTOCOL_ID
5580
GOSSIPSUB_PROTOCOL_ID = gossipsub.PROTOCOL_ID

libp2p/transport/tcp/tcp.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
from libp2p.transport.exceptions import (
3131
OpenConnectionError,
3232
)
33+
from libp2p.utils.multiaddr_utils import (
34+
extract_ip_from_multiaddr,
35+
multiaddr_from_socket,
36+
)
3337

3438
logger = logging.getLogger(__name__)
3539

@@ -88,15 +92,15 @@ async def handler(stream: trio.SocketStream) -> None:
8892
)
8993
return False
9094

91-
ip4_host_str = maddr.value_for_protocol("ip4")
92-
# For trio.serve_tcp, ip4_host_str (as host argument) can be None,
95+
host_str = extract_ip_from_multiaddr(maddr)
96+
# For trio.serve_tcp, host_str (as host argument) can be None,
9397
# which typically means listen on all available interfaces.
9498

9599
started_listeners = await nursery.start(
96100
serve_tcp,
97101
handler,
98102
tcp_port,
99-
ip4_host_str,
103+
host_str,
100104
)
101105

102106
if started_listeners is None:
@@ -138,7 +142,7 @@ async def dial(self, maddr: Multiaddr) -> IRawConnection:
138142
:return: `RawConnection` if successful
139143
:raise OpenConnectionError: raised when failed to open connection
140144
"""
141-
host_str = maddr.value_for_protocol("ip4")
145+
host_str = extract_ip_from_multiaddr(maddr)
142146
port_str = maddr.value_for_protocol("tcp")
143147

144148
if host_str is None:
@@ -194,5 +198,4 @@ def create_listener(self, handler_function: THandler) -> TCPListener:
194198

195199

196200
def _multiaddr_from_socket(socket: trio.socket.SocketType) -> Multiaddr:
197-
ip, port = socket.getsockname()
198-
return Multiaddr(f"/ip4/{ip}/tcp/{port}")
201+
return multiaddr_from_socket(socket)

libp2p/utils/address_validation.py

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import ipaddress
34
import socket
45

56
from multiaddr import Multiaddr
@@ -23,13 +24,55 @@ def _safe_get_network_addrs(ip_version: int) -> list[str]:
2324
return []
2425

2526

26-
def find_free_port() -> int:
27-
"""Find a free port on localhost."""
28-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
27+
def find_free_port(ip_version: int = 4) -> int:
28+
"""
29+
Find a free port on localhost.
30+
31+
:param ip_version: IP version (4 for IPv4, 6 for IPv6)
32+
:return: Available port number
33+
"""
34+
family = socket.AF_INET6 if ip_version == 6 else socket.AF_INET
35+
with socket.socket(family, socket.SOCK_STREAM) as s:
2936
s.bind(("", 0)) # Bind to a free port provided by the OS
3037
return s.getsockname()[1]
3138

3239

40+
def _validate_ipv4_address(address: str) -> str:
41+
"""
42+
Validate that a given address is a valid IPv4 address.
43+
44+
Args:
45+
address: The IP address string to validate
46+
47+
Returns:
48+
The validated IPv4 address, or "127.0.0.1" if invalid
49+
50+
"""
51+
try:
52+
ipaddress.IPv4Address(address)
53+
return address
54+
except (ipaddress.AddressValueError, ValueError):
55+
return "127.0.0.1"
56+
57+
58+
def _validate_ipv6_address(address: str) -> str:
59+
"""
60+
Validate that a given address is a valid IPv6 address.
61+
62+
Args:
63+
address: The IP address string to validate
64+
65+
Returns:
66+
The validated IPv6 address, or "::1" if invalid
67+
68+
"""
69+
try:
70+
ipaddress.IPv6Address(address)
71+
return address
72+
except (ipaddress.AddressValueError, ValueError):
73+
return "::1"
74+
75+
3376
def _safe_expand(addr: Multiaddr, port: int | None = None) -> list[Multiaddr]:
3477
"""
3578
Internal safe expansion wrapper. Returns a list of Multiaddr objects.
@@ -64,23 +107,16 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> list[Multiaddr
64107
if seen_v4 and "127.0.0.1" not in seen_v4:
65108
addrs.append(Multiaddr(f"/ip4/127.0.0.1/{protocol}/{port}"))
66109

67-
# TODO: IPv6 support temporarily disabled due to libp2p handshake issues
68-
# IPv6 connections fail during protocol negotiation (SecurityUpgradeFailure)
69-
# Re-enable IPv6 support once the following issues are resolved:
70-
# - libp2p security handshake over IPv6
71-
# - multiselect protocol over IPv6
72-
# - connection establishment over IPv6
73-
#
74-
# seen_v6: set[str] = set()
75-
# for ip in _safe_get_network_addrs(6):
76-
# if ip not in seen_v6: # Avoid duplicates
77-
# seen_v6.add(ip)
78-
# addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}"))
79-
#
80-
# # Always include IPv6 loopback for testing purposes when IPv6 is available
81-
# # This ensures IPv6 functionality can be tested even without global IPv6 addresses
82-
# if "::1" not in seen_v6:
83-
# addrs.append(Multiaddr(f"/ip6/::1/{protocol}/{port}"))
110+
seen_v6: set[str] = set()
111+
for ip in _safe_get_network_addrs(6):
112+
if ip not in seen_v6: # Avoid duplicates
113+
seen_v6.add(ip)
114+
addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}"))
115+
116+
# Always include IPv6 loopback for testing purposes when IPv6 is available
117+
# This ensures IPv6 functionality can be tested even without global IPv6 addresses
118+
if "::1" not in seen_v6:
119+
addrs.append(Multiaddr(f"/ip6/::1/{protocol}/{port}"))
84120

85121
# Fallback if nothing discovered
86122
if not addrs:
@@ -105,27 +141,34 @@ def expand_wildcard_address(
105141
return expanded
106142

107143

108-
def get_wildcard_address(port: int, protocol: str = "tcp") -> Multiaddr:
144+
def get_wildcard_address(
145+
port: int, protocol: str = "tcp", ip_version: int = 4
146+
) -> Multiaddr:
109147
"""
110-
Get wildcard address (0.0.0.0) when explicitly needed.
148+
Get wildcard address (0.0.0.0 or ::) when explicitly needed.
111149
112150
This function provides access to wildcard binding as a feature when
113151
explicitly required, preserving the ability to bind to all interfaces.
114152
115153
:param port: Port number.
116154
:param protocol: Transport protocol.
117-
:return: A Multiaddr with wildcard binding (0.0.0.0).
155+
:param ip_version: IP version (4 for 0.0.0.0, 6 for ::).
156+
:return: A Multiaddr with wildcard binding (0.0.0.0 or ::).
118157
"""
119-
return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")
158+
if ip_version == 6:
159+
return Multiaddr(f"/ip6/::/{protocol}/{port}")
160+
else:
161+
return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")
120162

121163

122164
def get_optimal_binding_address(port: int, protocol: str = "tcp") -> Multiaddr:
123165
"""
124166
Choose an optimal address for an example to bind to:
125-
- Prefer non-loopback IPv4
126-
- Then non-loopback IPv6
127-
- Fallback to loopback
128-
- Fallback to wildcard
167+
- Prefer non-loopback IPv6
168+
- Then non-loopback IPv4
169+
- Then loopback IPv6
170+
- Then loopback IPv4
171+
- Fallback to wildcard IPv4
129172
130173
:param port: Port number.
131174
:param protocol: Transport protocol.
@@ -137,14 +180,17 @@ def is_non_loopback(ma: Multiaddr) -> bool:
137180
s = str(ma)
138181
return not ("/ip4/127." in s or "/ip6/::1" in s)
139182

183+
for c in candidates:
184+
if "/ip6/" in str(c) and is_non_loopback(c):
185+
return c
140186
for c in candidates:
141187
if "/ip4/" in str(c) and is_non_loopback(c):
142188
return c
143189
for c in candidates:
144-
if "/ip6/" in str(c) and is_non_loopback(c):
190+
if "/ip6/::1" in str(c):
145191
return c
146192
for c in candidates:
147-
if "/ip4/127." in str(c) or "/ip6/::1" in str(c):
193+
if "/ip4/127." in str(c):
148194
return c
149195

150196
# As a final fallback, produce a loopback address

0 commit comments

Comments
 (0)