Skip to content

Commit bf8fdc7

Browse files
authored
Merge pull request #1268 from asabya/feat/announce_address
feat: add announce addrs in host, #1250
2 parents aa542b1 + 3c9549d commit bf8fdc7

9 files changed

Lines changed: 327 additions & 10 deletions

File tree

docs/examples.announce_addrs.rst

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Announce Addresses
2+
==================
3+
4+
This example demonstrates how to use announce addresses so that a node behind
5+
NAT or a reverse proxy (e.g., ngrok) advertises its publicly reachable address
6+
instead of its local listen address.
7+
8+
When running a libp2p node behind NAT or a reverse proxy, other nodes cannot
9+
reach it using the internal listen address. By specifying announce addresses,
10+
you can tell peers about your externally accessible addresses instead.
11+
12+
Usage
13+
-----
14+
15+
First, ensure you have installed the necessary dependencies from the root of
16+
the repository:
17+
18+
.. code-block:: console
19+
20+
$ python -m pip install -e .
21+
22+
**Node A (listener)** -- start the listener with announce addresses:
23+
24+
.. code-block:: console
25+
26+
$ python examples/announce_addrs/announce_addrs.py --listen-port 9001 \
27+
--announce /dns4/example.ngrok-free.app/tcp/9001 /ip4/1.2.3.4/tcp/4001
28+
29+
**Node B (dialer)** -- connect to the listener using its announced address and
30+
peer ID:
31+
32+
.. code-block:: console
33+
34+
$ python examples/announce_addrs/announce_addrs.py --listen-port 9002 \
35+
--dial /dns4/example.ngrok-free.app/tcp/9001/p2p/<PEER_ID_OF_A>
36+
37+
Notes on NAT and Reverse Proxies
38+
--------------------------------
39+
40+
This pattern is useful when:
41+
42+
- Your node is behind a NAT that performs port forwarding from an external IP
43+
to your local machine.
44+
- You are using a reverse proxy like ngrok that exposes your local port to the
45+
internet.
46+
- You need to advertise different addresses for external vs. internal
47+
connectivity.
48+
49+
By announcing the correct external addresses, peers will successfully dial your
50+
node regardless of their network position.
51+
52+
The full source code for this example is below:
53+
54+
.. literalinclude:: ../examples/announce_addrs/announce_addrs.py
55+
:language: python
56+
:linenos:

docs/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Examples
1919
examples.kademlia
2020
examples.mDNS
2121
examples.nat
22+
examples.announce_addrs
2223
examples.rendezvous
2324
examples.random_walk
2425
examples.multiple_connections
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""
2+
Announce Addresses Example for py-libp2p
3+
4+
Demonstrates how to use announce addresses so that a node behind NAT
5+
or a reverse proxy (e.g. ngrok) advertises its publicly reachable
6+
address instead of its local listen address.
7+
8+
Node A (listener):
9+
python announce_addrs.py --listen-port 9001 \
10+
--announce /dns4/example.ngrok-free.app/tcp/9001 /ip4/1.2.3.4/tcp/4001
11+
12+
Node B (dialer):
13+
python announce_addrs.py --listen-port 9002 \
14+
--dial /dns4/example.ngrok-free.app/tcp/9001/p2p/<PEER_ID_OF_A>
15+
"""
16+
17+
import argparse
18+
import logging
19+
import secrets
20+
21+
import multiaddr
22+
import trio
23+
24+
from libp2p import new_host
25+
from libp2p.crypto.secp256k1 import create_new_key_pair
26+
from libp2p.peer.peerinfo import info_from_p2p_addr
27+
28+
logging.basicConfig(
29+
level=logging.INFO,
30+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
31+
)
32+
logger = logging.getLogger("announce_addrs_example")
33+
34+
# Silence noisy libraries
35+
logging.getLogger("multiaddr").setLevel(logging.WARNING)
36+
37+
38+
async def run_listener(port: int, announce_addrs: list[str]) -> None:
39+
"""Start a node that listens locally and announces external addresses."""
40+
key_pair = create_new_key_pair(secrets.token_bytes(32))
41+
42+
listen_addrs = [multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")]
43+
44+
parsed_announce = [multiaddr.Multiaddr(a) for a in announce_addrs]
45+
46+
host = new_host(key_pair=key_pair, announce_addrs=parsed_announce)
47+
48+
async with host.run(listen_addrs=listen_addrs):
49+
peer_id = host.get_id().to_string()
50+
51+
logger.info("Node started")
52+
logger.info(f"Peer ID: {peer_id}")
53+
54+
logger.info("Transport (local) addresses:")
55+
for addr in host.get_transport_addrs():
56+
logger.info(f" {addr}")
57+
58+
logger.info("Announced (public) addresses:")
59+
for addr in host.get_addrs():
60+
logger.info(f" {addr}")
61+
62+
print(f"\nPeer ID: {peer_id}")
63+
print("\nTo connect from another node, run:")
64+
for addr in host.get_addrs():
65+
print(f" python announce_addrs.py --listen-port 9002 --dial {addr}")
66+
67+
print("\nPress Ctrl+C to exit.")
68+
await trio.sleep_forever()
69+
70+
71+
async def run_dialer(port: int, dial_addr: str) -> None:
72+
"""Start a node and connect to a remote peer."""
73+
key_pair = create_new_key_pair(secrets.token_bytes(32))
74+
75+
listen_addrs = [multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")]
76+
77+
host = new_host(key_pair=key_pair)
78+
79+
async with host.run(listen_addrs=listen_addrs):
80+
logger.info(f"Dialer started, peer ID: {host.get_id().to_string()}")
81+
82+
ma = multiaddr.Multiaddr(dial_addr)
83+
peer_info = info_from_p2p_addr(ma)
84+
85+
logger.info(f"Connecting to {peer_info.peer_id}...")
86+
await host.connect(peer_info)
87+
logger.info(f"Successfully connected to {peer_info.peer_id}")
88+
89+
print(f"\nConnected to peer: {peer_info.peer_id}")
90+
print("Press Ctrl+C to exit.")
91+
await trio.sleep_forever()
92+
93+
94+
def main() -> None:
95+
parser = argparse.ArgumentParser(
96+
description="Announce Addresses Example",
97+
formatter_class=argparse.RawDescriptionHelpFormatter,
98+
)
99+
parser.add_argument(
100+
"--listen-port",
101+
type=int,
102+
default=9001,
103+
help="Local TCP port to listen on (default: 9001)",
104+
)
105+
parser.add_argument(
106+
"--announce",
107+
nargs="+",
108+
help="Announce addresses (e.g. /dns4/example.ngrok-free.app/tcp/443)",
109+
)
110+
parser.add_argument(
111+
"--dial",
112+
type=str,
113+
help="Full multiaddr of remote peer to connect (must include /p2p/<peerID>)",
114+
)
115+
116+
args = parser.parse_args()
117+
118+
if args.dial:
119+
trio.run(run_dialer, args.listen_port, args.dial)
120+
elif args.announce:
121+
trio.run(run_listener, args.listen_port, args.announce)
122+
else:
123+
parser.error("Provide --announce to listen, or --dial to connect to a peer.")
124+
125+
126+
if __name__ == "__main__":
127+
main()

libp2p/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,8 @@ def new_host(
479479
bootstrap_allow_ipv6: bool = False,
480480
bootstrap_dns_timeout: float = 10.0,
481481
bootstrap_dns_max_retries: int = 3,
482-
connection_config: ConnectionConfig | None = None
482+
connection_config: ConnectionConfig | None = None,
483+
announce_addrs: Sequence[multiaddr.Multiaddr] | None = None,
483484
) -> IHost:
484485
"""
485486
Create a new libp2p host based on the given parameters.
@@ -505,6 +506,7 @@ def new_host(
505506
:param bootstrap_dns_timeout: DNS resolution timeout in seconds per attempt
506507
:param bootstrap_dns_max_retries: max DNS resolution retries with backoff
507508
:param connection_config: optional connection configuration for connection manager
509+
:param announce_addrs: if set, these replace listen addrs in get_addrs()
508510
:return: return a host instance
509511
"""
510512

@@ -557,6 +559,7 @@ def new_host(
557559
bootstrap_allow_ipv6=bootstrap_allow_ipv6,
558560
bootstrap_dns_timeout=bootstrap_dns_timeout,
559561
bootstrap_dns_max_retries=bootstrap_dns_max_retries,
562+
announce_addrs=announce_addrs,
560563
)
561564
return BasicHost(
562565
network=swarm,
@@ -568,6 +571,7 @@ def new_host(
568571
bootstrap_allow_ipv6=bootstrap_allow_ipv6,
569572
bootstrap_dns_timeout=bootstrap_dns_timeout,
570573
bootstrap_dns_max_retries=bootstrap_dns_max_retries,
574+
announce_addrs=announce_addrs,
571575
)
572576

573577
__version__ = __version("libp2p")

libp2p/abc.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2002,12 +2002,16 @@ def get_mux(self) -> "Multiselect":
20022002
@abstractmethod
20032003
def get_addrs(self) -> list[Multiaddr]:
20042004
"""
2005-
Retrieve all multiaddresses on which the host is listening.
2005+
Return the addresses this host advertises to other peers.
2006+
2007+
These may differ from the actual listen addresses when
2008+
``announce_addrs`` is configured. Each address includes a
2009+
``/p2p/{peer_id}`` suffix.
20062010
20072011
Returns
20082012
-------
20092013
list[Multiaddr]
2010-
A list of multiaddresses.
2014+
A list of advertised multiaddresses, each with a ``/p2p/{peer_id}`` suffix.
20112015
20122016
"""
20132017

libp2p/host/basic_host.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from cryptography import x509
1919
from cryptography.x509.oid import ExtensionOID
2020
import multiaddr
21+
from multiaddr.exceptions import ProtocolLookupError
2122
import trio
2223

2324
import libp2p
@@ -93,9 +94,6 @@
9394
background_trio_service,
9495
)
9596
from libp2p.transport.quic.connection import QUICConnection
96-
from libp2p.utils.multiaddr_utils import (
97-
join_multiaddrs,
98-
)
9997
import libp2p.utils.paths
10098
from libp2p.utils.varint import (
10199
read_length_prefixed_protobuf,
@@ -193,6 +191,7 @@ def __init__(
193191
bootstrap_allow_ipv6: bool = False,
194192
bootstrap_dns_timeout: float = 10.0,
195193
bootstrap_dns_max_retries: int = 3,
194+
announce_addrs: Sequence[multiaddr.Multiaddr] | None = None,
196195
) -> None:
197196
"""
198197
Initialize a BasicHost instance.
@@ -208,6 +207,9 @@ def __init__(
208207
:param bootstrap_allow_ipv6: If True, bootstrap uses IPv6+TCP when available.
209208
:param bootstrap_dns_timeout: DNS resolution timeout in seconds per attempt.
210209
:param bootstrap_dns_max_retries: Max DNS resolution retries (with backoff).
210+
:param announce_addrs: Optional addresses to advertise instead of
211+
listen addresses. ``None`` (default) uses listen addresses;
212+
an empty list advertises no addresses.
211213
"""
212214
self._network = network
213215
self._network.set_stream_handler(self._swarm_stream_handler)
@@ -253,6 +255,11 @@ def __init__(
253255
)
254256
self.psk = psk
255257

258+
# Address announcement configuration
259+
self._announce_addrs = (
260+
list(announce_addrs) if announce_addrs is not None else None
261+
)
262+
256263
# Cache a signed-record if the local-node in the PeerStore
257264
envelope = create_signed_peer_record(
258265
self.get_id(),
@@ -349,13 +356,34 @@ def get_transport_addrs(self) -> list[multiaddr.Multiaddr]:
349356

350357
def get_addrs(self) -> list[multiaddr.Multiaddr]:
351358
"""
352-
Return all the multiaddr addresses this host is listening to.
359+
Return the multiaddr addresses this host advertises to peers.
360+
361+
If ``announce_addrs`` was provided, those replace listen addresses
362+
entirely. Otherwise listen addresses are used.
353363
354364
Note: This method appends the /p2p/{peer_id} suffix to the addresses.
355365
Use get_transport_addrs() for raw transport addresses.
356366
"""
357367
p2p_part = multiaddr.Multiaddr(f"/p2p/{self.get_id()!s}")
358-
return [join_multiaddrs(addr, p2p_part) for addr in self.get_transport_addrs()]
368+
369+
if self._announce_addrs is not None:
370+
addrs = list(self._announce_addrs)
371+
else:
372+
addrs = self.get_transport_addrs()
373+
374+
result = []
375+
for addr in addrs:
376+
# Strip any existing /p2p/ component, then always append our own.
377+
# This avoids identity confusion when announce addrs contain a
378+
# mismatched peer ID (mirrors js-libp2p behaviour).
379+
try:
380+
p2p_value = addr.value_for_protocol("p2p")
381+
except ProtocolLookupError:
382+
p2p_value = None
383+
if p2p_value:
384+
addr = addr.decapsulate(multiaddr.Multiaddr(f"/p2p/{p2p_value}"))
385+
result.append(addr.encapsulate(p2p_part))
386+
return result
359387

360388
def get_connected_peers(self) -> list[ID]:
361389
"""
@@ -388,7 +416,7 @@ async def _run() -> AsyncIterator[None]:
388416
upnp_manager = self.upnp
389417
logger.debug("Starting UPnP discovery and port mapping")
390418
if await upnp_manager.discover():
391-
for addr in self.get_addrs():
419+
for addr in self.get_transport_addrs():
392420
if port := addr.value_for_protocol("tcp"):
393421
await upnp_manager.add_port_mapping(int(port), "TCP")
394422
if self.bootstrap is not None:
@@ -403,7 +431,7 @@ async def _run() -> AsyncIterator[None]:
403431
if self.upnp and self.upnp.get_external_ip():
404432
upnp_manager = self.upnp
405433
logger.debug("Removing UPnP port mappings")
406-
for addr in self.get_addrs():
434+
for addr in self.get_transport_addrs():
407435
if port := addr.value_for_protocol("tcp"):
408436
await upnp_manager.remove_port_mapping(int(port), "TCP")
409437
if self.bootstrap is not None:

libp2p/host/routed_host.py

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

3+
from collections.abc import (
4+
Sequence,
5+
)
6+
7+
import multiaddr
8+
39
from libp2p.abc import (
410
INetworkService,
511
IPeerRouting,
@@ -40,6 +46,7 @@ def __init__(
4046
bootstrap_allow_ipv6: bool = False,
4147
bootstrap_dns_timeout: float = 10.0,
4248
bootstrap_dns_max_retries: int = 3,
49+
announce_addrs: Sequence[multiaddr.Multiaddr] | None = None,
4350
):
4451
"""
4552
Initialize a RoutedHost instance.
@@ -55,6 +62,7 @@ def __init__(
5562
:param bootstrap_allow_ipv6: If True, bootstrap uses IPv6+TCP when available.
5663
:param bootstrap_dns_timeout: DNS resolution timeout in seconds per attempt.
5764
:param bootstrap_dns_max_retries: Max DNS resolution retries (with backoff).
65+
:param announce_addrs: If set, replace listen addrs in get_addrs()
5866
"""
5967
super().__init__(
6068
network,
@@ -66,6 +74,7 @@ def __init__(
6674
bootstrap_allow_ipv6=bootstrap_allow_ipv6,
6775
bootstrap_dns_timeout=bootstrap_dns_timeout,
6876
bootstrap_dns_max_retries=bootstrap_dns_max_retries,
77+
announce_addrs=announce_addrs,
6978
)
7079
self._router = router
7180

newsfragments/1250.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added ``announce_addrs`` support to ``BasicHost`` so nodes behind NAT or
2+
reverse proxies can advertise their publicly reachable addresses instead of
3+
local listen addresses.

0 commit comments

Comments
 (0)