Skip to content
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

Consider only reusing TCP port when hole punching #389

Open
mxinden opened this issue Jan 28, 2022 · 9 comments
Open

Consider only reusing TCP port when hole punching #389

mxinden opened this issue Jan 28, 2022 · 9 comments

Comments

@mxinden
Copy link
Member

mxinden commented Jan 28, 2022

Background

One can use the same port both listening for incoming connections and dialing outgoing connections. This is controlled via the SO_REUSEPORT socket option.

Port reuse is necessary for hole punching via TCP. It allows one to discover its outwards facing listening port via an outgoing TCP connection. The outgoing connection reuses the local listening port and is thus likely assigned the same outwards facing port as the outward facing listening port on the NAT.

When both endpoints of a connection send the TCP SYN simultaneously, it results not in two TCP connections but in one. On that TCP connections both endpoints assume to be the dialer (see sim-open spec for details.

Tie breaking on a simultaneous open TCP connection where both endpoints assume to be the dialer can today be done either via:

On hole punched, i.e. coordinated, TCP simultaneous open connections tie breaking via DCUtR can be used. (Tie breaking could as well be done via Multistream Select simultaneous open here, though that is less efficient as it requires more roundtrips and the tie breaking via DCUtR is available for free.)

On non-coordinated, i.e. accidental, TCP simultaneous open connections only tie breaking via Multistream Select simultaneous open can be used. This is problematic as we would like to move away from Multistream Select, replacing it with Protocol Select (see #349). Protocol Select does not support tie breaking, see "TCP Simultaneous Open" section in Protocol Select specification.

Thus far we planned to simply not-support non-coordinated, i.e. accidental, TCP simultaneous open connections. In such cases, as both endpoint assume to be the dialer and as there is no tie breaking mechanism in place, the connection upgrade would simply fail. In #349 we argue that the low probability of such non-coordinated TCP simultaneous open connection is too low for it to be special cased.

Proposal

We could do port reuse on coordinated TCP hole-punched connections only and use a new port (default TCP behaviour) for all other outgoing TCP connections. This would prevent the case of uncoordinated TCP simultaneous open and thus dissolve the need for tie breaking on uncoordinated accidentally simultaneously dialed TCP connections.

There are two problems with this proposal:

  1. How does one discover ones outwards-facing listening port? Previously done via non-holepunching outgoing TCP connections reusing the local listening port.
  2. How does one keep the port-mapping of ones listening port on the NAT alive. Previously done via non-holepunching outgoing TCP connections reusing the local listening port.

Would it be worth special casing specific outgoing connections (e.g. used for AutoNAT and identify) and have them reuse the listening TCP port, despite not being a hole-punching connection?

Would it be safe to not do port-reuse when one is public, i.e. directly reachable?


Credit for the proposal goes to @hamamo.

@mxinden
Copy link
Member Author

mxinden commented Jan 28, 2022

//CC @rkuhn as this is related to libp2p/rust-libp2p#2066.

@vyzo
Copy link
Contributor

vyzo commented Jan 28, 2022

Why do we want this? It will break NAT type detection.

@rkuhn
Copy link

rkuhn commented Jan 28, 2022

In ipfs-embed (on the unreleased sim-open branch) I worked around TCP simultaneous open in two ways:

  • prefer to not bind for outgoing connections, i.e. using random source ports (as proposed above)
  • if binding to the listen port is configured, recognise likely upgrade failures for non-coordinated simultaneous open by the nature of the raised error and redial with a randomised delay in that case

Discovery of an incoming peer’s likely listen address proceeds via the Identify protocol: when receiving information on the peer’s local listening ports, compute all combinations of those port numbers with the observed IP addresses, discard duplicates and known bad ones, and validate the remaining ones by attempting to dial them. This can of course not guess other forwarded ports on intermediate firewalls; my focus lay on non-NAT swarms within the same network or adequately configured networks (my software is made for the factory shop floor).

In general, I think it would be great if we could just use networks and foundational as they were designed — I’m still dreaming of IPv6 end-to-end connectivity (as I’m using from home). So as long as there’s a way to make libp2p not bind outgoing connections to a specific address or port, I’m happy.


For reference why I care about TCP simultaneous open: Actyx includes higher-level broadcast mechanisms used for peer discovery that rather frequently triggers connections in sufficient propinquity for this to be an issue in practice.

@mxinden
Copy link
Member Author

mxinden commented Jan 31, 2022

Why do we want this?

To prevent accidental, i.e. non-coordinated, TCP simultaneous open. See:

We could do port reuse on coordinated TCP hole-punched connections only and use a new port (default TCP behaviour) for all other outgoing TCP connections. This would prevent the case of uncoordinated TCP simultaneous open and thus dissolve the need for tie breaking on uncoordinated accidentally simultaneously dialed TCP connections.


It will break NAT type detection.

Yes, this is as well described above:

There are two problems with this proposal:

  1. How does one discover ones outwards-facing listening port? Previously done via non-holepunching outgoing TCP connections reusing the local listening port.
  2. How does one keep the port-mapping of ones listening port on the NAT alive. Previously done via non-holepunching outgoing TCP connections reusing the local listening port.

There are ways around this, e.g. using SO_REUSEPORT on special connections doing the identify and AutoNAT protocol (see "Proposal" section). In addition, nodes that do know they are public, i.e. not behind a NAT, could disable port-reuse.


I don't think the consideration above is ideal, at least not quite yet. Also note that in case we can figure out the caveats and do decide it is worth doing, I will not have the capacity to push this effort anytime soon.

@vyzo
Copy link
Contributor

vyzo commented Jan 31, 2022 via email

@hamamo
Copy link

hamamo commented Jan 31, 2022 via email

@Winterhuman
Copy link

Winterhuman commented Apr 8, 2022

This issue is relevant to this discussion as well, reusing TCP ports was found to cause issues with some routers: ipfs/kubo#3320 (comment)

"...do port reuse on coordinated TCP hole-punched connections only" seems like the best solution to avoid the router issue.

@diegomrsantos
Copy link

How about something like that?

  1. Start with port reuse disabled.
  2. Start Autonat. If we are confident enough that we are behind a NAT, enable port reuse.
  3. Learn about our public port after having enough outgoing connections with port reuse enabled.
  4. Start Hole punching.

Seems we won't have non-coordinated TCP simultaneous open connections in this case as peers can't connect to us without hole punching.

@marten-seemann
Copy link
Contributor

Not only does this break NAT type detection, but also normal address detection (via our STUN-like Identify mechanism). This is very important for AutoNAT v2.

We should not drop a crucial feature to get a slight performance advantage in a rare corner case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Triage
Development

No branches or pull requests

7 participants