Skip to content

Add NAT traversal via UPnP port mapping #771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

GautamBytes
Copy link

What was wrong?

py-libp2p lacked support for automatic port mapping via UPnP, making nodes behind home routers non-dialable from the public internet. This limited peer connectivity in common NAT scenarios.

Issue: NAT traversal for home network users

How was it fixed?

  1. Implemented a thread-safe UpnpManager class that:
    • Discovers UPnP gateways with caching
    • Maps TCP ports with permanent leases
    • Detects double-NAT scenarios
    • Automatically cleans mappings on shutdown
  2. Added optional dependency miniupnpc via pip install .[upnp]
  3. Created a comprehensive example demonstrating:
    • Successful mapping
    • Gateway failure cases
    • Clean shutdown behavior

Summary:

  • Standalone manager avoids host/swarm refactoring
  • Event-driven error handling (GatewayNotFound, NonRoutableGateway)
  • IPv4/IPv6 compatible address handling

To-Do

  • Clean up commit history (single cohesive commit)
  • Add documentation (example + README)

@GautamBytes
Copy link
Author

@seetadev please review it whenever feasible and suggest improvements!

First, ensure you have installed the necessary dependencies from the root of the repository:

```sh
pip install -e .[upnp]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be:

pip install -e ".[upnp]"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad , will update it soon. Any other changes apart from this??

Comment on lines 31 to 33
except Exception as e:
if str(e) == "Success":
num_devices = 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific edge case in miniupnpc where it raises an exception with the message "Success" but should actually be treated as a valid discovery result?

This looks a bit fragile, so I was wondering if it's based on something you've seen happen in the wild. Could you clarify the motivation?

logger.exception("UPnP discovery failed")
return False

async def add_port_mapping(self, port: int, protocol: str = "TCP") -> bool:
Copy link
Contributor

@lla-dane lla-dane Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if port <= 0 or port > 65535:
    logger.error(f"Invalid port number: {port}")
    return False
if port < 1024:
    logger.warning(f"Mapping a well-known port: {port} (may fail)")

Setting up these kinds of warning for sketchy port numbers in the params, would save computation time in cases of misbehaviour. Also add similar checks to remove_port_mapping.

Comment on lines 18 to 25
upnp = UpnpManager()
if not await upnp.discover():
logger.error(
"❌ Could not find a UPnP-enabled gateway. "
"The host will start, but may not be dialable from the public internet."
)
else:
logger.info(f"✅ UPnP gateway found! External IP: {upnp.get_external_ip()}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to integrate the UpnpManager into the Host runtime. You can see how the mdns service is integrated, Unpn will follow the same thing.

class BasicHost(IHost):
	...

    def __init__(
        self,
        network: INetworkService,
        enable_mDNS: bool = False,
        default_protocols: Optional["OrderedDict[TProtocol, StreamHandlerFn]"] = None,
        negotitate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
    ) -> None:
	...
        if enable_mDNS:
            self.mDNS = MDNSDiscovery(network)

Could go with an enable_upnp bool to set up the manager object and then inside this part in the example:

    async with host.run(listen_addrs=[listen_maddr]):

call host.upnp to start the relevant operations.
Embedding this in the host logic would be much cleaner and more consistent to updates.
Let me know if you need any pointers on how to set this up.

@lla-dane
Copy link
Contributor

@seetadev @GautamBytes: Added some inline comments and suggestions.
Couldn’t test the upnp_demo example since I’m currently on a college network without access to a personal router.
Would be great if someone with a UPnP-enabled router could give it a spin and verify the behavior.

@GautamBytes GautamBytes requested a review from lla-dane July 20, 2025 15:35
@@ -277,6 +278,6 @@ def new_host(

if disc_opt is not None:
return RoutedHost(swarm, disc_opt, enable_mDNS)
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , negotitate_timeout=negotiate_timeout)
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , negotitate_timeout=negotiate_timeout, enable_upnp=enable_upnp)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

negotitate_timeout Should be negotiate_timeout

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was already there so didn't notice , will correct it!

logger = logging.getLogger("libp2p.discovery.upnp")


class UpnpManager:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpnpManager would benefit with unit tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure , will be adding that asap!

@GautamBytes GautamBytes requested a review from luu-alex July 22, 2025 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants