Skip to content

p2p/nat: remove forceful port mapping in upnp #30265

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

Merged
merged 6 commits into from
Apr 4, 2025

Conversation

qqqeck
Copy link
Contributor

@qqqeck qqqeck commented Aug 5, 2024

Since addAnyPortMapping is implemented, we don't need DeleteMapping to remove an existing port mapping and make a forceful port mapping (which somewhat goes against the concept of upnp).

Also there is some chance n.randomPort() in addAnyPortMapping returns an occupied port, so it's safer to try it couple of times. Three looks like enough according to libp2p.

Edit: Fixed so it would only delete port mapping only if the external port was originally used by geth.

@qqqeck qqqeck requested review from fjl and zsfelfoldi as code owners August 5, 2024 04:57
@lightclient
Copy link
Member

DeleteMapping was added in #15222 to fix issue #1024, but this is before addAnyPortMapping was added in #26359. I'm not really sure the point of removing it? But at the same time it seems like it isn't needed now.

Running addAnyPortMapping(..) three times before giving up looks okay to me.

Let's see what @fjl thinks.

@qqqeck
Copy link
Contributor Author

qqqeck commented Aug 5, 2024

I just thought it could cause some problems if it deletes port mappings used by other users(processes) using the same upnp.

Not sure about the full issue #1024 but the internal ip changing case is probably covered by addAnyPortMapping. I guess in this case geth does leave behind an abandoned port mapping, but upnp only maintains each port mapping for a certain time so it's probably okay.

@panicalways
Copy link
Contributor

Basically, the resources of NAT are not sufficient. Therefore, related protocols recommend minimizing the usage of NAT resources. I think it would be better to remove unused ports.

@qqqeck
Copy link
Contributor Author

qqqeck commented Aug 9, 2024

I agree it would be better to remove unused ports. Still, I think deleting maybe-in-use port mappings is a much bigger problem. I'll add the code to delete ports that were abandoned by geth.

@qqqeck qqqeck changed the title p2p/nat: remove forceful port mapping in upnp p2p: remove forceful port mapping in upnp Aug 9, 2024
@fjl fjl self-assigned this Dec 10, 2024
@cskiraly
Copy link
Collaborator

DeleteMapping was added in #15222 to fix issue #1024, but this is before addAnyPortMapping was added in #26359. I'm not really sure the point of removing it? But at the same time it seems like it isn't needed now.

Regarding the discussion on delete, it is not needed according to spec. Neither in UPnP IGDv1, nor in v2, nor in NAT-PMP. Implementations are notoriously buggy, so we might keep it as a workaround, but I would not use it as part of the base renewal mechanism, especially if we would risk side-effects.

Also, we do set a lifetime (10 minutes, which is well below the recommended 1-2 hours), so delete should happen even without an explicit call if we shutdown abruptly, while we could easily call it in normal shutdown (I didn't yet check if it is happening). But even if it was not deleted, the new request should count as an overwrite.

There are some corner cases. I will check what's handled and what's not:

  1. Lifetime might not be supported. There should be a specific error code for this. In that case we could redo the mapping with lifetime 0 (permanent), but I think this is very rare in practive.
    2, If a node restarted without deleting the mapping, and it restarts on a different internal IP, then it would not be able to get the same port. In this case addAnyPortMapping() would map to a different port. But mapping to a different port is perfectly fine, our protocol just needs a port, not that specific port anyway.

We should also anticipate the renewal. Currently we are doing it with the same time as the lifetime, which can result in some service outage.

Here are the relevant specs:

Spec

UPnP Internet Gateway Device v1 (IGD:1)

https://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v1-Service.pdf

UPnP Internet Gateway Device v2 (IGD:2).

https://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf

PMP spec

https://www.rfc-editor.org/rfc/rfc6886.txt

PCP (PMP next version) spec

https://www.rfc-editor.org/rfc/rfc6887.txt

Renawal

And here are the relevant parts about renewal (called overwrite in UPnP)

UPnP IGD v1

Overwriting Previous / Existing Port Mappings:
If the RemoteHost, ExternalPort, PortMappingProtocol and InternalClient are exactly the same as an existing mapping, the existing mapping values for InternalPort, PortMappingDescription, PortMappingEnabled and PortMappingLeaseDuration are overwritten.

UPnP IGD v2

2.5.16 AddPortMapping()
This action creates a new port mapping or overwrites an existing mapping with the same internal client. If the ExternalPort and PortMappingProtocol pair is already mapped to another internal client, an error is
returned.

Figure 2-3: Summary of AddPortMapping() results

Protocol ExternalPort RemoteHost InternalClient Result
= = = = Success (overwrite)

PMP

A renewal packet is formatted identically to an initial mapping request packet, except that for renewals the client sets the Suggested External Port field to the port the gateway actually assigned, rather than the port the client originally wanted.

@fjl fjl changed the title p2p: remove forceful port mapping in upnp p2p/nat: remove forceful port mapping in upnp Apr 1, 2025
qqqeck and others added 5 commits April 2, 2025 10:54
With UPnP IGD v2 it was random only if specified was unavailable.
With UPnP IGD v1 it was disregarding the external port.

Signed-off-by: Csaba Kiraly <[email protected]>
The default of addAnyPortMapping is what addPortMapping does, so
no need to call it separately.

Signed-off-by: Csaba Kiraly <[email protected]>
Signed-off-by: Csaba Kiraly <[email protected]>
@cskiraly cskiraly force-pushed the fix-upnp-port-mapping branch from c5a1ba7 to ca79cd0 Compare April 2, 2025 08:57
Was logging 0 instead of original port.

Signed-off-by: Csaba Kiraly <[email protected]>
@fjl fjl merged commit ff365af into ethereum:master Apr 4, 2025
2 checks passed
@fjl fjl added this to the 1.15.8 milestone Apr 4, 2025
fjl added a commit that referenced this pull request Apr 9, 2025
Make UPnP more robust

- Once a random port was mapped, we try to stick to it even if a UPnP
refresh fails. Previously we were immediately moving back to try the
default port, leading to frequent ENR changes.

- We were deleting port mappings before refresh as a possible
workaround. This created issues in some UPnP servers. The UPnP (and PMP)
specification is explicit about the refresh requirements, and delete is
clearly not needed (see
#30265 (comment)).
From now on we only delete when closing.

- We were trying to add port mappings only once, and then moved on to
random ports. Now we insist a bit more, so that a simple failed request
won't lead to ENR changes.

Fixes #31418

---------

Signed-off-by: Csaba Kiraly <[email protected]>
Co-authored-by: Felix Lange <[email protected]>
sivaratrisrinivas pushed a commit to sivaratrisrinivas/go-ethereum that referenced this pull request Apr 21, 2025
Here we are modifying the port mapping logic so that existing port
mappings will only be removed when they were previously created by geth.

The AddAnyPortMapping functionality has been adapted to work consistently
between the IGDv1 and IGDv2 backends.
sivaratrisrinivas pushed a commit to sivaratrisrinivas/go-ethereum that referenced this pull request Apr 21, 2025
Make UPnP more robust

- Once a random port was mapped, we try to stick to it even if a UPnP
refresh fails. Previously we were immediately moving back to try the
default port, leading to frequent ENR changes.

- We were deleting port mappings before refresh as a possible
workaround. This created issues in some UPnP servers. The UPnP (and PMP)
specification is explicit about the refresh requirements, and delete is
clearly not needed (see
ethereum#30265 (comment)).
From now on we only delete when closing.

- We were trying to add port mappings only once, and then moved on to
random ports. Now we insist a bit more, so that a simple failed request
won't lead to ENR changes.

Fixes ethereum#31418

---------

Signed-off-by: Csaba Kiraly <[email protected]>
Co-authored-by: Felix Lange <[email protected]>
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.

5 participants