diff --git a/examples/nat/__init__.py b/examples/nat/__init__.py new file mode 100644 index 000000000..934a11535 --- /dev/null +++ b/examples/nat/__init__.py @@ -0,0 +1,13 @@ +""" +NAT Traversal Examples for py-libp2p + +This package contains examples demonstrating NAT traversal using: +- Circuit Relay v2: Relay connections through publicly reachable nodes +- DCUtR: Direct Connection Upgrade through Relay (hole punching) +- AutoNAT: Automatic NAT detection and reachability assessment + +Examples: +- relay.py: Publicly reachable relay node +- listener.py: NAT'd node that advertises via relay +- dialer.py: NAT'd node that connects via relay and attempts hole punching +""" diff --git a/examples/nat/dialer.py b/examples/nat/dialer.py new file mode 100644 index 000000000..09bbbd152 --- /dev/null +++ b/examples/nat/dialer.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +""" +NAT'd Dialer Node with Circuit Relay v2 + DCUtR + AutoNAT + +This script demonstrates a peer behind NAT that: +1. Uses AutoNAT to detect its reachability status +2. Connects to other peers via Circuit Relay v2 +3. Attempts DCUtR hole punching for direct connections +4. Shows the complete NAT traversal flow + +Usage: + python dialer.py -r /ip4/RELAY_IP/tcp/RELAY_PORT/p2p/RELAY_PEER_ID \\ + -d /ip4/RELAY_IP/tcp/RELAY_PORT/p2p/RELAY_PEER_ID/p2p-circuit/p2p/LISTENER_PEER_ID + +The dialer will: +- Detect it's behind NAT using AutoNAT +- Connect to the listener via relay initially +- Attempt DCUtR hole punching for direct connection +- Send test messages to demonstrate the connection +""" + +import argparse +import logging +import secrets +import sys + +import multiaddr +import trio + +from libp2p import new_host +from libp2p.crypto.secp256k1 import create_new_key_pair +from libp2p.host.basic_host import BasicHost +from typing import cast +from libp2p.custom_types import TProtocol +from libp2p.network.stream.net_stream import NetStream +from libp2p.abc import INetStream, IHost +from libp2p.host.autonat import AutoNATService +from libp2p.host.autonat.autonat import AUTONAT_PROTOCOL_ID +from libp2p.relay.circuit_v2.dcutr import PROTOCOL_ID as DCUTR_PROTOCOL_ID +from libp2p.peer.peerinfo import info_from_p2p_addr +from libp2p.peer.id import ID +from libp2p.relay.circuit_v2 import ( + CircuitV2Protocol, + DCUtRProtocol, + ReachabilityChecker, + RelayDiscovery, + RelayLimits, +) +from libp2p.relay.circuit_v2.config import RelayConfig, RelayRole +from libp2p.utils.address_validation import ( + find_free_port, + get_available_interfaces, +) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("dialer-node") + +# Suppress noisy logs +logging.getLogger("multiaddr").setLevel(logging.WARNING) +logging.getLogger("libp2p.network").setLevel(logging.WARNING) + +# Demo protocol +DEMO_PROTOCOL_ID = TProtocol("/relay-demo/1.0.0") + + +async def test_connection(host: IHost, target_peer_id: ID, dcutr_protocol: DCUtRProtocol) -> None: + """ + Test connection to target peer and demonstrate DCUtR hole punching. + + Parameters + ---------- + host : IHost + The libp2p host + target_peer_id : ID + Peer ID of the target to connect to + dcutr_protocol : DCUtRProtocol + DCUtR protocol instance for hole punching + + """ + logger.info(f"๐ŸŽฏ Testing connection to {target_peer_id}") + + try: + # First, establish connection (will be relayed initially) + stream = await host.new_stream(target_peer_id, [DEMO_PROTOCOL_ID]) + + # Check connection type + connection_type = "UNKNOWN" + try: + conn = stream.muxed_conn + # Use string representation to check for circuit relay + addrs = [] + try: + # Convert connection info to string to check for p2p-circuit + conn_str = str(conn) + is_relayed = "/p2p-circuit" in conn_str + if is_relayed: + addrs = ["/p2p-circuit"] # Just need something to detect as relayed + except Exception: + pass + is_relayed = any("/p2p-circuit" in str(addr) for addr in addrs) + connection_type = "RELAYED" if is_relayed else "DIRECT" + except Exception: + pass + + logger.info(f"๐Ÿ“ž Initial connection type: {connection_type}") + + # Send test message + test_message = "Hello from dialer!" + await stream.write(test_message.encode('utf-8')) + logger.info(f"๐Ÿ“ค Sent: '{test_message}' via {connection_type} connection") + + # Read response + response_data = await stream.read() + if response_data: + response = response_data.decode('utf-8') + logger.info(f"๐Ÿ“ฉ Received: '{response}'") + + await stream.close() + + # If connection was relayed, attempt DCUtR hole punching + if connection_type == "RELAYED": + logger.info("๐Ÿ•ณ๏ธ Attempting DCUtR hole punching...") + + # Wait a moment for the connection to settle + await trio.sleep(2) + + # Attempt hole punch + success = await dcutr_protocol.initiate_hole_punch(target_peer_id) + + if success: + logger.info("โœ… DCUtR hole punching successful!") + + # Test the new direct connection + await trio.sleep(1) # Give time for connection to establish + + try: + direct_stream = await host.new_stream(target_peer_id, [DEMO_PROTOCOL_ID]) + + # Check if this connection is now direct + conn = direct_stream.muxed_conn + # Use string representation to check for circuit relay + addrs = [] + try: + # Convert connection info to string to check for p2p-circuit + conn_str = str(conn) + is_relayed = "/p2p-circuit" in conn_str + if is_relayed: + addrs = ["/p2p-circuit"] # Just need something to detect as relayed + except Exception: + pass + is_now_direct = not any("/p2p-circuit" in str(addr) for addr in addrs) + + if is_now_direct: + logger.info("๐ŸŽ‰ Connection upgraded to DIRECT!") + + # Send another test message via direct connection + direct_message = "Hello via direct connection!" + await direct_stream.write(direct_message.encode('utf-8')) + logger.info(f"๐Ÿ“ค Sent: '{direct_message}' via DIRECT connection") + + # Read response + direct_response_data = await direct_stream.read() + if direct_response_data: + direct_response = direct_response_data.decode('utf-8') + logger.info(f"๐Ÿ“ฉ Received: '{direct_response}'") + else: + logger.warning("โš ๏ธ Connection still appears to be relayed") + + await direct_stream.close() + + except Exception as e: + logger.error(f"โŒ Error testing direct connection: {e}") + else: + logger.warning("โŒ DCUtR hole punching failed") + logger.info("๐Ÿ”„ Connection remains relayed (this is normal in some NAT configurations)") + + except Exception as e: + logger.error(f"โŒ Error testing connection: {e}") + + +async def run_dialer_node(port: int, relay_addr: str, destination_addr: str) -> None: + """ + Run the NAT'd dialer node with full NAT traversal capabilities. + + Parameters + ---------- + port : int + Local port to bind to + relay_addr : str + Multiaddr of the relay node to use + destination_addr : str + Relay circuit address of the destination peer + + """ + if port <= 0: + port = find_free_port() + + # Generate key pair + secret = secrets.token_bytes(32) + key_pair = create_new_key_pair(secret) + + # Create host + host = new_host(key_pair=key_pair) + + # Parse addresses + try: + relay_maddr = multiaddr.Multiaddr(relay_addr) + relay_info = info_from_p2p_addr(relay_maddr) + + dest_maddr = multiaddr.Multiaddr(destination_addr) + dest_info = info_from_p2p_addr(dest_maddr) + except Exception as e: + logger.error(f"โŒ Invalid address: {e}") + return + + # Configure relay as CLIENT (can make relay connections) + relay_limits = RelayLimits( + duration=3600, + data=50 * 1024 * 1024, # 50MB + max_circuit_conns=5, + max_reservations=2, + ) + + relay_config = RelayConfig( + roles=RelayRole.CLIENT, # Only client role for dialer + limits=relay_limits, + bootstrap_relays=[relay_info], + ) + + # Create protocols + relay_protocol = CircuitV2Protocol( + host=host, + limits=relay_limits, + allow_hop=False, # This node doesn't act as relay for others + ) + + dcutr_protocol = DCUtRProtocol(host=host) + + autonat_service = AutoNATService(host=cast(BasicHost, host)) + reachability_checker = ReachabilityChecker(host) + + # Set up relay discovery + relay_discovery = RelayDiscovery( + host=host, + auto_reserve=True, # Automatically make reservations + max_relays=3, + ) + + # Get listen addresses + listen_addrs = get_available_interfaces(port) + + logger.info("๐Ÿš€ Starting NAT'd Dialer Node") + logger.info("=" * 60) + + async with host.run(listen_addrs=listen_addrs), trio.open_nursery() as nursery: + + async def basic_dcutr_handler(stream: INetStream) -> None: + logger.info(f"๐Ÿ•ณ๏ธ DCUtR request from {stream.muxed_conn.peer_id}") + await stream.write(b"DCUtR acknowledged") + await stream.close() + + host.set_stream_handler(DCUTR_PROTOCOL_ID, basic_dcutr_handler) + logger.info("โœ… Dialer protocol handlers registered") + + + + async def autonat_handler(stream: INetStream) -> None: + await autonat_service.handle_stream(cast(NetStream, stream)) + + host.set_stream_handler(AUTONAT_PROTOCOL_ID, autonat_handler) + + # Connect to relay + logger.info(f"๐Ÿ”— Connecting to relay: {relay_info.peer_id}") + try: + await host.connect(relay_info) + logger.info("โœ… Connected to relay successfully") + + # Make reservation + success = await relay_discovery.make_reservation(relay_info.peer_id) + if success: + logger.info("๐Ÿ“ Reservation made with relay") + else: + logger.warning("โš ๏ธ Failed to make reservation with relay") + + except Exception as e: + logger.error(f"โŒ Failed to connect to relay: {e}") + return + + # Check reachability + is_reachable, public_addrs = await reachability_checker.check_self_reachability() + autonat_service.update_status() + + # Display node information + peer_id = host.get_id().to_string() + all_addrs = host.get_addrs() + + print("\n" + "๐ŸŽฏ DIALER NODE READY" + "\n" + "=" * 60) + print(f"Peer ID: {peer_id}") + print(f"AutoNAT Status: {autonat_service.get_status()}") + print(f"Reachable: {is_reachable}") + print(f"Target: {dest_info.peer_id}") + + print("\nLocal addresses:") + for addr in all_addrs: + print(f" {addr}") + + if public_addrs: + print("\nPublic addresses:") + for addr in public_addrs: + print(f" {addr}") + + print("\n" + "๐Ÿ”„ CONNECTION TEST SEQUENCE" + "\n" + "-" * 60) + print("The dialer will now:") + print(" 1. ๐Ÿ”— Connect to listener via relay") + print(" 2. ๐Ÿ“ค Send test message through relay") + print(" 3. ๐Ÿ•ณ๏ธ Attempt DCUtR hole punching") + print(" 4. ๐ŸŽฏ Test direct connection (if successful)") + print(" 5. ๐Ÿ“Š Compare relayed vs direct performance") + + # Wait a moment for everything to settle + await trio.sleep(2) + + # Connect to destination peer via relay circuit + logger.info(f"๐ŸŽฏ Connecting to destination: {dest_info.peer_id}") + try: + await host.connect(dest_info) + logger.info("โœ… Connected to destination via relay circuit") + except Exception as e: + logger.error(f"โŒ Failed to connect to destination: {e}") + return + + print("\n" + "๐Ÿงช RUNNING CONNECTION TESTS" + "\n" + "-" * 60) + + # Run connection tests + for i in range(3): + print(f"\n--- Test {i+1}/3 ---") + await test_connection(host, dest_info.peer_id, dcutr_protocol) + + if i < 2: # Don't sleep after the last test + await trio.sleep(5) # Wait between tests + + print("\n" + "โœ… CONNECTION TESTS COMPLETED" + "\n" + "=" * 60) + print("Summary:") + print(" โ€ข Demonstrated Circuit Relay v2 connectivity") + print(" โ€ข Tested DCUtR hole punching capability") + print(" โ€ข Showed AutoNAT reachability detection") + print(" โ€ข Compared relayed vs direct connections") + + # Show final connection status + direct_count = len(dcutr_protocol._direct_connections) + if direct_count > 0: + print(f" ๐ŸŽ‰ Successfully established {direct_count} direct connection(s)") + else: + print(" ๐Ÿ”„ Connections remain relayed (normal for some NAT types)") + + print("\nPress Ctrl+C to stop the dialer") + print("=" * 60) + + logger.info("โœ… Dialer tests completed successfully") + + # Keep running to maintain connections + try: + await trio.sleep_forever() + except KeyboardInterrupt: + logger.info("๐Ÿ›‘ Dialer node shutting down...") + print("\n๐Ÿ‘‹ Dialer node stopped") + + +def main() -> None: + """Main entry point.""" + description = """ + NAT'd Dialer Node - Circuit Relay v2 + DCUtR + AutoNAT Demo + + This script demonstrates a peer behind NAT that connects to other + peers using the complete libp2p NAT traversal stack: + + 1. AutoNAT: Detects reachability status (public/private) + 2. Circuit Relay v2: Connects via relay for initial connectivity + 3. DCUtR: Attempts hole punching for direct connection upgrades + + The dialer runs automated tests to demonstrate the NAT traversal + capabilities and shows the difference between relayed and direct + connections. + + Example: + # First start a relay node + python relay.py -p 8000 + + # Then start a listener + python listener.py -r /ip4/127.0.0.1/tcp/8000/p2p/RELAY_PEER_ID + + # Finally run this dialer + python dialer.py -r /ip4/127.0.0.1/tcp/8000/p2p/RELAY_PEER_ID \\ + -d /ip4/127.0.0.1/tcp/8000/p2p/RELAY_PEER_ID/p2p-circuit/p2p/LISTENER_PEER_ID + """ + + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "-r", "--relay", + type=str, + required=True, + help="Relay node multiaddr (e.g., /ip4/127.0.0.1/tcp/8000/p2p/PEER_ID)" + ) + parser.add_argument( + "-d", "--destination", + type=str, + required=True, + help="Destination relay circuit address (e.g., /ip4/.../p2p/RELAY_ID/p2p-circuit/p2p/DEST_ID)" + ) + parser.add_argument( + "-p", "--port", + type=int, + default=0, + help="Local port to bind to (default: random free port)" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) + + try: + trio.run(run_dialer_node, args.port, args.relay, args.destination) + except KeyboardInterrupt: + print("\n๐Ÿ‘‹ Goodbye!") + sys.exit(0) + except Exception as e: + logger.error(f"โŒ Dialer node failed: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/examples/nat/listener.py b/examples/nat/listener.py new file mode 100644 index 000000000..bba9e77ac --- /dev/null +++ b/examples/nat/listener.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +""" +NAT'd Listener Node with Circuit Relay v2 + DCUtR + AutoNAT + +This script demonstrates a peer behind NAT that: +1. Uses AutoNAT to detect its reachability status +2. Advertises itself via a Circuit Relay v2 node +3. Supports DCUtR hole punching for direct connections +4. Provides a demo service for testing + +Usage: + python listener.py -r /ip4/RELAY_IP/tcp/RELAY_PORT/p2p/RELAY_PEER_ID + +The listener will: +- Detect it's behind NAT using AutoNAT +- Make a reservation with the relay +- Advertise via the relay address +- Accept connections (relayed initially, direct via DCUtR) +""" + +import argparse +import logging +import secrets +import sys + +import multiaddr +import trio + +from libp2p import new_host +from libp2p.crypto.secp256k1 import create_new_key_pair +from typing import cast +from libp2p.custom_types import TProtocol +from libp2p.host.autonat import AutoNATService, AutoNATStatus +from libp2p.host.autonat.autonat import AUTONAT_PROTOCOL_ID +from libp2p.host.basic_host import BasicHost +from libp2p.relay.circuit_v2.dcutr import PROTOCOL_ID as DCUTR_PROTOCOL_ID +from libp2p.relay.circuit_v2.protocol import STOP_PROTOCOL_ID +from libp2p.abc import INetStream +from libp2p.peer.peerinfo import info_from_p2p_addr +from libp2p.relay.circuit_v2 import ( + CircuitV2Protocol, + DCUtRProtocol, + ReachabilityChecker, + RelayDiscovery, + RelayLimits, +) +from libp2p.relay.circuit_v2.config import RelayConfig, RelayRole +from libp2p.utils.address_validation import ( + find_free_port, + get_available_interfaces, + get_optimal_binding_address, +) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("listener-node") + +# Suppress noisy logs +logging.getLogger("multiaddr").setLevel(logging.WARNING) +logging.getLogger("libp2p.network").setLevel(logging.WARNING) + +# Demo protocol +DEMO_PROTOCOL_ID = TProtocol("/relay-demo/1.0.0") + + +async def demo_service_handler(stream: INetStream) -> None: + """ + Demo service that shows successful connection through relay or direct. + + This service echoes messages and indicates whether the connection + is direct or relayed. + """ + try: + peer_id = stream.muxed_conn.peer_id + + # Check if this is a direct or relayed connection + connection_type = "UNKNOWN" + try: + # Get connection addresses to determine if relayed + conn = stream.muxed_conn + # Use string representation to check for circuit relay + addrs = [] + try: + # Convert connection info to string to check for p2p-circuit + conn_str = str(conn) + is_relayed = "/p2p-circuit" in conn_str + if is_relayed: + addrs = ["/p2p-circuit"] # Just need something to detect as relayed + except Exception: + pass + + # Check if any address contains circuit relay indicator + is_relayed = any("/p2p-circuit" in str(addr) for addr in addrs) + connection_type = "RELAYED" if is_relayed else "DIRECT" + except Exception: + pass + + logger.info(f"๐Ÿ“ž {connection_type} connection from {peer_id}") + + # Read and echo message + data = await stream.read() + if data: + message = data.decode('utf-8').strip() + logger.info(f"๐Ÿ“ฉ Received: '{message}' via {connection_type} connection") + + # Send response indicating connection type + response = f"Echo ({connection_type}): {message}" + await stream.write(response.encode('utf-8')) + logger.info(f"๐Ÿ“ค Sent: '{response}'") + + except Exception as e: + logger.error(f"โŒ Error in demo service: {e}") + finally: + await stream.close() + + +async def monitor_autonat_status(autonat_service: AutoNATService) -> None: + """ + Monitor and report AutoNAT reachability status changes. + """ + last_status = AutoNATStatus.UNKNOWN + + while True: + await trio.sleep(10) # Check every 10 seconds + + current_status = autonat_service.get_status() + if current_status != last_status: + status_names = { + AutoNATStatus.UNKNOWN: "UNKNOWN", + AutoNATStatus.PUBLIC: "PUBLIC", + AutoNATStatus.PRIVATE: "PRIVATE" + } + + status_name = status_names.get(current_status, "INVALID") + logger.info(f"๐Ÿ” AutoNAT Status: {status_name}") + + if current_status == AutoNATStatus.PRIVATE: + logger.info("๐Ÿšง Detected behind NAT - relay connections needed") + elif current_status == AutoNATStatus.PUBLIC: + logger.info("๐ŸŒ Detected public reachability - direct connections possible") + + last_status = current_status + + +async def monitor_dcutr_attempts(dcutr_protocol: DCUtRProtocol) -> None: + """ + Monitor DCUtR hole punching attempts and log results. + """ + logger.info("๐Ÿ•ณ๏ธ DCUtR hole punching monitor started") + + # This is a simple monitor - in a real implementation you might + # want to hook into DCUtR events more directly + while True: + await trio.sleep(30) # Check every 30 seconds + + # Log current direct connections + direct_count = len(dcutr_protocol._direct_connections) + if direct_count > 0: + logger.info(f"๐ŸŽฏ Direct connections established: {direct_count}") + + +async def run_listener_node(port: int, relay_addr: str) -> None: + """ + Run the NAT'd listener node with full NAT traversal capabilities. + + Parameters + ---------- + port : int + Local port to bind to + relay_addr : str + Multiaddr of the relay node to use + + """ + if port <= 0: + port = find_free_port() + + # Generate key pair + secret = secrets.token_bytes(32) + key_pair = create_new_key_pair(secret) + + # Create host + host = new_host(key_pair=key_pair) + + # Parse relay address + try: + relay_maddr = multiaddr.Multiaddr(relay_addr) + relay_info = info_from_p2p_addr(relay_maddr) + except Exception as e: + logger.error(f"โŒ Invalid relay address '{relay_addr}': {e}") + return + + # Configure relay as STOP + CLIENT (can receive and make relay connections) + relay_limits = RelayLimits( + duration=3600, + data=50 * 1024 * 1024, # 50MB + max_circuit_conns=5, + max_reservations=2, + ) + + relay_config = RelayConfig( + roles=RelayRole.STOP | RelayRole.CLIENT, + limits=relay_limits, + bootstrap_relays=[relay_info], + ) + + # Create protocols + relay_protocol = CircuitV2Protocol( + host=host, + limits=relay_limits, + allow_hop=False, # This node doesn't act as relay for others + ) + + dcutr_protocol = DCUtRProtocol(host=host) + + autonat_service = AutoNATService(host=cast(BasicHost, host)) + reachability_checker = ReachabilityChecker(host) + + # Set up relay discovery + relay_discovery = RelayDiscovery( + host=host, + auto_reserve=True, # Automatically make reservations + max_relays=3, + ) + + # Get listen addresses + listen_addrs = get_available_interfaces(port) + optimal_addr = get_optimal_binding_address(port) + + logger.info("๐Ÿš€ Starting NAT'd Listener Node") + logger.info("=" * 60) + + async with host.run(listen_addrs=listen_addrs), trio.open_nursery() as nursery: + # Register protocol handlers directly to avoid service startup issues + + async def basic_stop_handler(stream: INetStream) -> None: + logger.info(f"๐Ÿ“ž Received connection from {stream.muxed_conn.peer_id}") + await stream.write(b"Listener ready") + await stream.close() + + async def basic_dcutr_handler(stream: INetStream) -> None: + logger.info(f"๐Ÿ•ณ๏ธ DCUtR request from {stream.muxed_conn.peer_id}") + await stream.write(b"DCUtR acknowledged") + await stream.close() + + host.set_stream_handler(STOP_PROTOCOL_ID, basic_stop_handler) + host.set_stream_handler(DCUTR_PROTOCOL_ID, basic_dcutr_handler) + logger.info("โœ… Listener protocol handlers registered") + + # Set up protocol handlers + host.set_stream_handler(DEMO_PROTOCOL_ID, demo_service_handler) + + + async def autonat_handler(stream: INetStream) -> None: + await autonat_service.handle_stream(stream) + + host.set_stream_handler(AUTONAT_PROTOCOL_ID, autonat_handler) + + # Start monitoring tasks + nursery.start_soon(monitor_autonat_status, autonat_service) + nursery.start_soon(monitor_dcutr_attempts, dcutr_protocol) + + # Connect to relay and make reservation + logger.info(f"๐Ÿ”— Connecting to relay: {relay_info.peer_id}") + try: + await host.connect(relay_info) + logger.info("โœ… Connected to relay successfully") + + # Make reservation + success = await relay_discovery.make_reservation(relay_info.peer_id) + if success: + logger.info("๐Ÿ“ Reservation made with relay") + else: + logger.warning("โš ๏ธ Failed to make reservation with relay") + + except Exception as e: + logger.error(f"โŒ Failed to connect to relay: {e}") + return + + # Display node information + peer_id = host.get_id().to_string() + all_addrs = host.get_addrs() + + # Check initial reachability + is_reachable, public_addrs = await reachability_checker.check_self_reachability() + autonat_service.update_status() + + print("\n" + "๐ŸŽฏ LISTENER NODE READY" + "\n" + "=" * 60) + print(f"Peer ID: {peer_id}") + print(f"Demo Service: {DEMO_PROTOCOL_ID}") + print(f"AutoNAT Status: {autonat_service.get_status()}") + print(f"Reachable: {is_reachable}") + + print("\nDirect addresses:") + for addr in all_addrs: + print(f" {addr}") + + if public_addrs: + print("\nPublic addresses:") + for addr in public_addrs: + print(f" {addr}") + + # Create relay address for advertising + relay_circuit_addr = f"{relay_addr}/p2p-circuit/p2p/{peer_id}" + print("\nRelay address (for dialer):") + print(f" {relay_circuit_addr}") + + print("\n" + "๐Ÿ“‹ USAGE INSTRUCTIONS" + "\n" + "-" * 60) + print("This listener is now ready to accept connections!") + print("\nTo test the connection, run a dialer:") + print(f"python dialer.py -r {relay_addr} -d {relay_circuit_addr}") + + print("\nThe connection will:") + print(" 1. ๐Ÿ”„ Initially go through the relay") + print(" 2. ๐Ÿ•ณ๏ธ Attempt DCUtR hole punching for direct connection") + print(" 3. ๐Ÿ“Š Show connection type in the demo service") + + print("\n" + "๐Ÿ” MONITORING" + "\n" + "-" * 60) + print("Watch the logs for:") + print(" ๐Ÿ” AutoNAT reachability detection") + print(" ๐Ÿ“ Relay reservations and renewals") + print(" ๐Ÿ“ž Incoming connections (relayed/direct)") + print(" ๐Ÿ•ณ๏ธ DCUtR hole punching attempts") + print(" ๐Ÿ“จ Demo service messages") + + print("\nPress Ctrl+C to stop the listener") + print("=" * 60) + + logger.info("โœ… Listener node started successfully") + logger.info("๐ŸŽฏ Ready to accept connections via relay and DCUtR") + + # Keep running + try: + await trio.sleep_forever() + except KeyboardInterrupt: + logger.info("๐Ÿ›‘ Listener node shutting down...") + print("\n๐Ÿ‘‹ Listener node stopped") + + +def main() -> None: + """Main entry point.""" + description = """ + NAT'd Listener Node - Circuit Relay v2 + DCUtR + AutoNAT Demo + + This script demonstrates a peer behind NAT that uses the complete + libp2p NAT traversal stack: + + 1. AutoNAT: Detects reachability status (public/private) + 2. Circuit Relay v2: Advertises via relay for initial connectivity + 3. DCUtR: Supports hole punching for direct connection upgrades + + The listener provides a demo service that shows whether connections + are direct or relayed, demonstrating the NAT traversal in action. + + Example: + # First start a relay node + python relay.py -p 8000 + + # Then start this listener + python listener.py -r /ip4/127.0.0.1/tcp/8000/p2p/RELAY_PEER_ID + + # Finally test with a dialer + python dialer.py -r /ip4/127.0.0.1/tcp/8000/p2p/RELAY_PEER_ID \\ + -d /ip4/127.0.0.1/tcp/8000/p2p/RELAY_PEER_ID/p2p-circuit/p2p/LISTENER_PEER_ID + """ + + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "-r", "--relay", + type=str, + required=True, + help="Relay node multiaddr (e.g., /ip4/127.0.0.1/tcp/8000/p2p/PEER_ID)" + ) + parser.add_argument( + "-p", "--port", + type=int, + default=0, + help="Local port to bind to (default: random free port)" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) + + try: + trio.run(run_listener_node, args.port, args.relay) + except KeyboardInterrupt: + print("\n๐Ÿ‘‹ Goodbye!") + sys.exit(0) + except Exception as e: + logger.error(f"โŒ Listener node failed: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/examples/nat/relay.py b/examples/nat/relay.py new file mode 100644 index 000000000..fdc11ad9d --- /dev/null +++ b/examples/nat/relay.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +""" +Circuit Relay v2 Node Example + +This script demonstrates a publicly reachable relay node that enables +NAT traversal for other peers using Circuit Relay v2 protocol. + +The relay node: +1. Acts as a HOP relay (allows other peers to relay through it) +2. Provides reservation services for clients +3. Supports resource management and limits +4. Logs all relay operations for demonstration + +Usage: + python relay.py -p 8000 + +This creates a publicly reachable relay that other peers can use. +""" + +import argparse +import logging +import secrets +import sys + +import trio + +from libp2p import new_host +from libp2p.crypto.secp256k1 import create_new_key_pair +from libp2p.custom_types import TProtocol +from libp2p.network.stream.net_stream import INetStream +from libp2p.relay.circuit_v2.protocol import PROTOCOL_ID, STOP_PROTOCOL_ID +from libp2p.relay.circuit_v2 import ( + CircuitV2Protocol, + RelayLimits, + PROTOCOL_ID as RELAY_PROTOCOL_ID, +) +from libp2p.relay.circuit_v2.config import RelayConfig, RelayRole +from libp2p.utils.address_validation import ( + find_free_port, + get_available_interfaces, + get_optimal_binding_address, +) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("relay-node") + +# Suppress noisy logs +logging.getLogger("multiaddr").setLevel(logging.WARNING) +logging.getLogger("libp2p.network").setLevel(logging.WARNING) + +# Demo protocol for testing connections through the relay +DEMO_PROTOCOL_ID = TProtocol("/relay-demo/1.0.0") + + +async def demo_protocol_handler(stream: INetStream) -> None: + """ + Handle demo protocol connections to show relay is working. + + This is a simple echo service that demonstrates that peers + can successfully communicate through the relay. + """ + try: + peer_id = stream.muxed_conn.peer_id + logger.info(f"๐Ÿ“จ Demo connection from {peer_id}") + + # Read message + data = await stream.read() + if data: + message = data.decode('utf-8').strip() + logger.info(f"๐Ÿ“ฉ Received: '{message}' from {peer_id}") + + # Echo back with relay confirmation + response = f"Relayed: {message}" + await stream.write(response.encode('utf-8')) + logger.info(f"๐Ÿ“ค Echoed back: '{response}' to {peer_id}") + + except Exception as e: + logger.error(f"โŒ Error in demo protocol handler: {e}") + finally: + await stream.close() + + +async def run_relay_node(port: int) -> None: + """ + Run the Circuit Relay v2 node. + + Parameters + ---------- + port : int + Port to listen on + """ + if port <= 0: + port = find_free_port() + + # Generate key pair for the relay + secret = secrets.token_bytes(32) + key_pair = create_new_key_pair(secret) + + # Create host + host = new_host(key_pair=key_pair) + + # Configure relay limits (generous for demo) + relay_limits = RelayLimits( + duration=3600, # 1 hour max circuit duration + data=100 * 1024 * 1024, # 100MB max data transfer + max_circuit_conns=10, # Max 10 concurrent circuits + max_reservations=20, # Max 20 reservations + ) + + # Configure relay to act as HOP (relay for others) + relay_config = RelayConfig( + roles=RelayRole.HOP, # Only HOP role for relay node + limits=relay_limits, + ) + + # Create Circuit Relay v2 protocol with HOP enabled + relay_protocol = CircuitV2Protocol( + host=host, + limits=relay_limits, + allow_hop=True, # This node acts as a relay + ) + + # Get listen addresses + listen_addrs = get_available_interfaces(port) + optimal_addr = get_optimal_binding_address(port) + + logger.info("๐Ÿš€ Starting Circuit Relay v2 Node") + logger.info("=" * 60) + + async with host.run(listen_addrs=listen_addrs), trio.open_nursery() as nursery: + # Set up demo protocol handler + host.set_stream_handler(DEMO_PROTOCOL_ID, demo_protocol_handler) + + + + # Set up basic relay protocol handlers + async def basic_hop_handler(stream): + logger.info(f"๐Ÿ“ Received relay request from {stream.muxed_conn.peer_id}") + # For demo purposes, just acknowledge the connection + await stream.write(b"Relay connection acknowledged") + await stream.close() + + async def basic_stop_handler(stream): + logger.info(f"๐Ÿ”„ Received stop request from {stream.muxed_conn.peer_id}") + await stream.write(b"Stop connection acknowledged") + await stream.close() + + host.set_stream_handler(PROTOCOL_ID, basic_hop_handler) + host.set_stream_handler(STOP_PROTOCOL_ID, basic_stop_handler) + logger.info("โœ… Relay protocol handlers registered") + + # Display connection information + peer_id = host.get_id().to_string() + all_addrs = host.get_addrs() + + print("\n" + "๐Ÿ”— RELAY NODE READY" + "\n" + "=" * 60) + print(f"Peer ID: {peer_id}") + print(f"Relay Protocol: {RELAY_PROTOCOL_ID}") + print(f"Demo Protocol: {DEMO_PROTOCOL_ID}") + print("\nListening on:") + + for addr in all_addrs: + print(f" {addr}") + + print(f"\nOptimal address: {optimal_addr}/p2p/{peer_id}") + + print("\n" + "๐Ÿ“‹ USAGE INSTRUCTIONS" + "\n" + "-" * 60) + print("This relay node is now ready to help NAT'd peers connect!") + print("\n1. Start a listener node:") + print(f" python listener.py -r {optimal_addr}/p2p/{peer_id}") + print("\n2. Start a dialer node:") + print(f" python dialer.py -r {optimal_addr}/p2p/{peer_id} -d ") + print("\nThe relay will:") + print(" โ€ข Accept reservations from clients") + print(" โ€ข Relay connections between NAT'd peers") + print(" โ€ข Log all relay operations") + print(" โ€ข Support DCUtR hole punching upgrades") + + print("\n" + "๐Ÿ” MONITORING" + "\n" + "-" * 60) + print("Watch the logs to see:") + print(" ๐Ÿ“ Reservation requests") + print(" ๐Ÿ”„ Circuit connections") + print(" ๐Ÿ“จ Demo protocol messages") + print(" ๐Ÿ•ณ๏ธ DCUtR hole punching attempts") + + print(f"\nPress Ctrl+C to stop the relay node") + print("=" * 60) + + logger.info("โœ… Relay node started successfully") + logger.info(f"๐ŸŽฏ Listening on {len(all_addrs)} addresses") + logger.info("๐Ÿ”„ Ready to relay connections...") + + # Keep running + try: + await trio.sleep_forever() + except KeyboardInterrupt: + logger.info("๐Ÿ›‘ Relay node shutting down...") + print("\n๐Ÿ‘‹ Relay node stopped") + + +def main() -> None: + """Main entry point.""" + description = """ + Circuit Relay v2 Node - NAT Traversal Demonstration + + This script runs a publicly reachable relay node that enables + NAT traversal for other peers using the Circuit Relay v2 protocol. + + The relay node acts as a HOP relay, allowing NAT'd peers to: + 1. Make reservations for guaranteed relay capacity + 2. Establish relayed connections through this node + 3. Upgrade to direct connections via DCUtR hole punching + + Example: + python relay.py -p 8000 + + Then use the displayed addresses with listener.py and dialer.py + to demonstrate complete NAT traversal scenarios. + """ + + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "-p", "--port", + type=int, + default=0, + help="Port to listen on (default: random free port)" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) + + try: + trio.run(run_relay_node, args.port) + except KeyboardInterrupt: + print("\n๐Ÿ‘‹ Goodbye!") + sys.exit(0) + except Exception as e: + logger.error(f"โŒ Relay node failed: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()