marten-seemann / draft-seemann-quic-nat-traversal

Other
15 stars 4 forks source link

consider allowing probe packets to be smaller than 1200 bytes #20

Open marten-seemann opened 1 year ago

marten-seemann commented 1 year ago

In #19 @huitema suggested to allow probe packets to be smaller than 1200 bytes.

This would drastically reduce the bandwidth requirements for path probes (see also #8). This is especially true since it is expected that a lot of paths probed will not work out, and therefore trigger multiple retransmissions of path probes until a timeout is detected. However, a successful response wouldn't mean that the path is actually usable for QUIC, which has a minimum MTU requirement of 1200 bytes. This might be acceptable in Multipath QUIC, but the consequences are more severe in vanilla QUIC.

We could make path validation a 2-step process:

  1. Test connectivity using small QUIC packets.
  2. Once connectivity has been established, test that the path actually support 1200 bytes.

This takes an additional RTT, but for some applications this might be an acceptable tradeoff.

marten-seemann commented 1 year ago

The logic could be entirely driven by the client:

huitema commented 1 year ago

To be clear, I was only thinking of allowing the server to send short packets. If I understand correctly, the proposed logic would be:

1) Client sends "punch me now" to the server via the proxy collection 2) In parallel, client sends a probe packet (PATH CHALLENGE) on the desired path (new 4 tuple) 3) Server receives "punch me now" and sends a probe packet (PATH CHALLENGE) on the desired path (same new 4 tuple) 4) Hopefully the PATH CHALLENGE of the server has punched a hole in the local NAT, the source address of the server was predicted correctly, and the PATH CHALLENGE from the server reaches the client.

This is a bit heavy, because the server will be in fact creating the path. I would prefer something like:

1) Client sends "punch me now" to the server via the proxy collection 2) Server receives "punch me now" and sends a probe packet (some new frame) on the desired path (new 4 tuple). Server does not otherwise create any state. Probe opens a pinhole for the path on server side firewall, is not really expected to reach the client. (In a P2P environment, the server may be sitting behind a home firewall.) 3) Some time after "punch me now", client sends a probe packet (PATH CHALLENGE) on the desired path (new 4 tuple). If it arrives after the punch but before the pinhole is closed, server receives that, sends PATH Response, etc.

Maybe that's what you had in mind? In that case, we need to specify what the probe packet is. Shall it contain just a PING, or some new frame "PUNCHING YOU", with indication of which "punch me now" frame it is responding to? The probe packet could be kept small, of course.

marten-seemann commented 1 year ago

It was not, but it's an interesting idea. Currently, you're limited by the peer's active_connection_id_limit, which might be significantly smaller than the number of paths that you want to validate at the same time. It would be nice to get around this somehow. Maybe PUNCH_ME_NOW should contain a CID that doesn't count towards that limit, and is only used for the hole punching (the actual path would have to use a regular CID).

I like the idea of not creating any state, but I'm not sure if that's feasible. Due to its time-dependence, hole punching normally relies on multiple packets being sent. Furthermore, there might be packet loss on the way to your NAT (lossy WiFi, or normal packet loss if you're dealing with a CGNAT), so retransmissions are a necessity here.

huitema commented 1 year ago

I think it would be nice to only create state on the client that wants to set up a new path. Have a "puch me now" request trigger a stateless response from the server,with enough content to identify which "punch" request it is replying to. If more than one message is needed, then the client should repeat the process.

With IPv6, simple solutions might suffice. You need to deal with a stateful firewall at each end, but there is no NAT -- or there is a V6 NAT but the mapping is typically stable. In those environment, a simple "message from client" opens the pinhole in the stateful firewalls on the client side, and simple message from server opens the pinhole from the server side. So basically:

1) Client sends a sacrificial probe on the desired path. It opens a pinhole the client-side firewall, but most likely is dropped at the server firewall. 2) Client sends "punch me now" through the proxy. 3) Server replies with probe on desired path, does not create state -- one probe per punch, no more. Probes opens a pinhole in server firewall, and if things go as expected it squeezes through the pinhole created by the client's probe in the client's firewall. 4) If the client receives the server's probe, it can proceed with PATH CHALLENGE, etc.

The advantage there is "no CID consumed". The probes can be tiny, the PMTU will be checked by the path challenge. The cost is one RTT. The optimistic client could send a PATH CHALLENGE just after step 2, but there is a race condition between PATH CHALLENGE and PUNCH ME NOW, so that may fail. The cautious client will just spend one RTT.

That scenario will succeed with some IPv4 NAT, but will fail if the server side NAT maps the server probe to an address that the client did not predict (a.k.a. symmetric NAT). In that case, the server's probe will knock on the wrong pinhole. When I was working on Windows, we tried to fix that in a bunch of different ways, which all boil down to trying to predict the port number over which the server's response will arrive. You need a match between:

1) Client has sent a recent probe with source-ip=Ca, source-port=Cp, dest-ip=Sa, dest-port=Sp, 2) Server's probe appears as dest-ip=Ca', dest-port=Cp', source-ip=Sa', source-port=Sp'

The client in most scenarios does have a good idea of Ca and Sa, so we can assume Ca==Ca', Sa==Sa'. The client must predict Cp' and Sp'. The various prediction algorithms that were tried include "same as before", "in sequence", and "random", which lead to different strategies:

same as before: ask the server about the addresses that it has seen recently, collect the port numbers, send probes to each of those. in sequence: assume that if the server has seen port P in use recently, it is likely that the send probes will arrive from one of P+1...P+n. Send client probes to these ports to open as many pin-holes. random: can predict anything. May try the birthday paradox approach, in which each end sends N probes to the peer, with the hope than one pair will match.

Of course, each probe creates state in the NAT, so sending too many probes risk exhausting the space allocated to mapping tables in the NAT and thus kill existing paths through that NAT.

Bottom line: all these work much better if one of the two ends is open. If you can use PMP or UPNP, or if you can reserve a port number in your home NAT, then do it!