diff --git a/examples/discovery/mDNS/mDNS.py b/examples/discovery/mDNS/mDNS.py new file mode 100644 index 00000000..8563a1fb --- /dev/null +++ b/examples/discovery/mDNS/mDNS.py @@ -0,0 +1,36 @@ +import secrets +import multiaddr +import trio + +from libp2p import new_host +from libp2p.crypto.secp256k1 import create_new_key_pair + +async def main(): + # Generate a key pair for the host + secret = secrets.token_bytes(32) + key_pair = create_new_key_pair(secret) + + # Listen on a random TCP port + listen_addr = multiaddr.Multiaddr("/ip4/0.0.0.0/tcp/0") + + # Enable mDNS discovery + host = new_host(key_pair=key_pair, enable_mDNS=True) + + async with host.run(listen_addrs=[listen_addr]): + print("Host started!") + print("Peer ID:", host.get_id()) + print("Listening on:", [str(addr) for addr in host.get_addrs()]) + + # Print discovered peers via mDNS + print("Waiting for mDNS peer discovery events (Ctrl+C to exit)...") + try: + while True: + # Print all known peers every 5 seconds + peers = host.get_peerstore().peer_ids() + print("Known peers:", [str(p) for p in peers if p != host.get_id()]) + await trio.sleep(5) + except KeyboardInterrupt: + print("Exiting...") + +if __name__ == "__main__": + trio.run(main) \ No newline at end of file diff --git a/libp2p/__init__.py b/libp2p/__init__.py index 64f47243..bc814da6 100644 --- a/libp2p/__init__.py +++ b/libp2p/__init__.py @@ -71,6 +71,9 @@ from libp2p.transport.upgrader import ( from libp2p.utils.logging import ( setup_logging, ) +from libp2p.discovery.mdns.mdns import ( + MDNSDiscovery +) # Initialize logging configuration setup_logging() @@ -236,13 +239,14 @@ def new_swarm( def new_host( - key_pair: KeyPair | None = None, - muxer_opt: TMuxerOptions | None = None, - sec_opt: TSecurityOptions | None = None, - peerstore_opt: IPeerStore | None = None, - disc_opt: IPeerRouting | None = None, - muxer_preference: Literal["YAMUX", "MPLEX"] | None = None, - listen_addrs: Sequence[multiaddr.Multiaddr] | None = None, + key_pair: Optional[KeyPair] = None, + muxer_opt: Optional[TMuxerOptions] = None, + sec_opt: Optional[TSecurityOptions] = None, + peerstore_opt: Optional[IPeerStore] = None, + disc_opt: Optional[IPeerRouting] = None, + muxer_preference: Optional[Literal["YAMUX", "MPLEX"]] = None, + listen_addrs: Sequence[multiaddr.Multiaddr] = None, + enable_mDNS: bool = False, ) -> IHost: """ Create a new libp2p host based on the given parameters. @@ -266,8 +270,15 @@ def new_host( ) if disc_opt is not None: - return RoutedHost(swarm, disc_opt) - return BasicHost(swarm) + host = RoutedHost(swarm, disc_opt) + else: + host = BasicHost(swarm) + if enable_mDNS: + mdns = MDNSDiscovery(swarm) + mdns.start() + host._mdns = mdns + + return host __version__ = __version("libp2p") diff --git a/libp2p/discovery/__init__.py b/libp2p/discovery/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libp2p/discovery/mdns/__init__.py b/libp2p/discovery/mdns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libp2p/discovery/mdns/broadcaster.py b/libp2p/discovery/mdns/broadcaster.py new file mode 100644 index 00000000..e69de29b diff --git a/libp2p/discovery/mdns/listener.py b/libp2p/discovery/mdns/listener.py new file mode 100644 index 00000000..e69de29b diff --git a/libp2p/discovery/mdns/mdns.py b/libp2p/discovery/mdns/mdns.py new file mode 100644 index 00000000..8670e00d --- /dev/null +++ b/libp2p/discovery/mdns/mdns.py @@ -0,0 +1,85 @@ +""" +mDNS-based peer discovery for py-libp2p. +Conforms to https://github.com/libp2p/specs/blob/master/discovery/mdns.md +Uses zeroconf for mDNS broadcast/listen. Async operations use trio. +""" +import trio +from typing import Callable, Optional +from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser, ServiceStateChange +from .utils import ( + stringGen +) +from libp2p.abc import ( + INetworkService +) + +SERVICE_TYPE = "_p2p._udp.local." +MCAST_PORT = 5353 +MCAST_ADDR = "224.0.0.251" + +class MDNSDiscovery: + def __init__(self, swarm: INetworkService): + self.peer_id = swarm.get_peer_id() + self.port = 8000 # Default port, can be overridden + # self.broadcast = init.get('broadcast', True) is not False + # self.on_peer = on_peer # Callback: async def on_peer(peer_info) + # self.service_name = service_name or f"{peer_id}.{SERVICE_TYPE}" + # self.zeroconf = Zeroconf() + # self._service_info = ServiceInfo( + # SERVICE_TYPE, + # self.service_name, + # addresses=[], # Will be set on register + # port=self.port, + # properties={b'id': peer_id.encode()}, + # ) + # self._browser = None + # self._running = False + + def main(self) -> None: + """ + Main entry point for the mDNS discovery service. + This method is intended to be run in an event loop. + """ + trio.run(self.start) + + async def start(self): + await trio.sleep(10) + # self._running = True + # await trio.to_thread.run_sync(self.zeroconf.register_service, self._service_info) + # self._browser = ServiceBrowser(self.zeroconf, SERVICE_TYPE, handlers=[self._on_service_state_change]) + print(f"Starting mDNS discovery for peer {self.peer_id} on port {self.port}") + + async def stop(self): + # self._running = False + # await trio.to_thread.run_sync(self.zeroconf.unregister_service, self._service_info) + # await trio.to_thread.run_sync(self.zeroconf.close) + print(f"Stopping mDNS discovery for peer {self.peer_id}") + + def _on_service_state_change(self, zeroconf, service_type, name, state_change): + if state_change is not ServiceStateChange.Added: + return + info = zeroconf.get_service_info(service_type, name) + if not info or name == self.service_name: + return + peer_id = info.properties.get(b'id') + if not peer_id: + return + peer_id = peer_id.decode() + addresses = [addr for addr in info.parsed_addresses()] + port = info.port + peer_info = {'peer_id': peer_id, 'addresses': addresses, 'port': port} + if self.on_peer: + # Schedule callback in the background + trio.lowlevel.spawn_system_task(self._call_on_peer, peer_info) + + async def _call_on_peer(self, peer_info): + if self.on_peer: + await self.on_peer(peer_info) + +# Example usage: +# async def on_peer(peer_info): +# print(f"Discovered peer: {peer_info['peer_id']} at {peer_info['addresses']}:{peer_info['port']}") +# mdns = MDNSDiscovery(peer_id, port, on_peer) +# await mdns.start() +# ... +# await mdns.stop()