Code change:
This keeps things simple.
I have no idea how that happened. Forced it.
Are we ok with a server sending both a VN packet and a Retry in response to a single UDP datagram?
NAME Does that happen? IIUC VN is sent only when there's a version mismatch. Retry is sent only when there's no version mismatch.
NAME A client could coalesce two packets of different versions. As I've said on the issue, I still feel uneasy with the fact that a client could coalesce around 70 packets into a single datagram, and I'd prefer more rigid rules to solve this problem.
NAME I'd argue that a client can't do that, because the draft states "Senders MUST NOT coalesce QUIC packets for different connections into a single UDP datagram", OTOH I agree that there's nothing that requires the server to drop following QUIC packets with different versions within a combined datagram. As I stated on URL, I (still) think the approach in this PR would require having minimal state. Would you mind elaborating (possibly on the issue) why you think differently?
Sure, the issue here comes from multi-threaded processing, and reusing packet buffers. I have one thread (or rather, one go-routine) that reads UDP datagrams from the network, splits the coalesced packets and parses the unencrypted part of the header, and then passes the QUIC packets to their respective connections (which are running in a separate go-routine each). In order to reduce allocations, I reuse the buffers I read my packets into. When handling coalesced packets, this means I have to wait until all coalesced parts have been processed before reusing the buffer. Therefore, I can't just parse the first coalesced packet, pass it on for handling in another threat, and then continue parsing. This would create a race condition, since the handling of the packet might be executed before the parsing is finished, and the packet buffer would be returned already. So I have to split the coalesced packets first, and then handle them. This wouldn't be a problem at all if a coalesced packet just had a few parts, as it's supposed to be. But since we're maximizing flexibility for no reason, an attacker might send me datagram consisting of around 70 packets, which I would all have to completely parse.
NAME Thank you for elaborating. I think that we should be dispatching each UDP datagram (instead of each QUIC packet) to the respective connections. That's how load balancers are expected to work, and what you are implementing is essentially a load balancer. The benefit of dispatching at the datagram level is that you'd have less things to do on the receiving thread. I understand your point on flexibility. However, in my view, the flexibility is not only how you coalesce, but the redundancy in all the long header fields. We have deliberately chosen the design because it is clear and easy to analyze (especially in regard to encryption). My preference goes to retaining the clarity by avoiding introducing restrictions.
I think this PR addresses the amplification problem directly, by tying the number of response datagrams to the number of incoming datagrams, so I am in favor of this PR as a spec solution. NAME : quic-go can still choose to drop datagrams with a large (> 3?) coalesced packets, which might help you (slightly). I agree with NAME also that since all coalesced packets are supposed to go to the same connection, the implementation would be better aligned if it were to pass the datagram along to the connection instead of the individual packets... but that's an impl choice, and it's certainly not the only way to do this.
I'm merging this. Please reopen the issue if you think this ought to be discussed further, especially in Tokyo.

Related discussion:
text: Consequently, a Version Negotiation packet consumes an entire UDP datagram. <ins> A server MUST NOT send more than one Version Negotiation packet in response to a single UDP datagram. </ins> See version-negotiation for a description of the version negotiation process.