Skip to content

Don't gate PTO packets on address validation#49

Draft
rnro wants to merge 1 commit into
apple:mainfrom
rnro:pto_after_validation
Draft

Don't gate PTO packets on address validation#49
rnro wants to merge 1 commit into
apple:mainfrom
rnro:pto_after_validation

Conversation

@rnro

@rnro rnro commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

At the moment the code will not send an ack-eliciting ping once the peer's address is validated. This appears to be linked to RFC 9000 Section 8.1, which states

To prevent this deadlock, clients MUST send a packet on a Probe Timeout (PTO) [...] the client MUST send an Initial packet in a UDP datagram that contains at least 1200 bytes if it does not have Handshake keys, and otherwise send a Handshake packet.

However I think this is a slight mis-read, and that RFC 9002 Section 6.2.4

When a PTO timer expires, a sender MUST send at least one ack-eliciting packet [...] When there is no data to send, the sender SHOULD send a PING or other ack-eliciting frame in a single packet, rearming the PTO timer.

shows that whilst validation is an important inflection point for the two behaviors, PTO pings should still be sent after it. The pseudocode in A9 I think also indicates this.

A new test with the current code results in outstanding frames which will never have a PTO fire and the current code will end up in a tight loop.

This change removes the validation distinction in sendPTO, along with the now-unused validation local and the timer-clear branch it fed; the difference in behavior required by the RFC will be enacted by the timer.

At the moment the code will not send an ack-eliciting ping once the
peer's address is validated. This appears to be linked to RFC 9000
Section 8.1, which states
> To prevent this deadlock, clients MUST send a packet on a Probe
> Timeout (PTO) [...] the client MUST send an Initial packet in a UDP
> datagram that contains at least 1200 bytes if it does not have
> Handshake keys, and otherwise send a Handshake packet.
However I think this is a slight mis-read, and that RFC 9002 Section
6.2.4
> When a PTO timer expires, a sender MUST send at least one ack-eliciting
> packet [...] When there is no data to send, the sender SHOULD send a
> PING or other ack-eliciting frame in a single packet, rearming the PTO
> timer.
shows that whilst validation is an important inflection point for the two
behaviors, PTO pings should still be sent after it. The pseudocode in A9
I think also indicates this.

A new test with the current code results in outstanding frames which will
never have a PTO fire and the current code will end up in a tight loop.

This change removes the validation distinction in `sendPTO`, along with
the now-unused validation local and the timer-clear branch it fed; the
difference in behavior required by the RFC will be enacted by the timer.
@agnosticdev

Copy link
Copy Markdown
Collaborator

At the moment the code will not send an ack-eliciting ping once the peer's address is validated.

PTO pings should still be sent after it.

PTO can generate PINGs still if the ACK blocks get large enough to piggyback a PING frame to make them ACK-eliciting, see the code here: https://github.com/apple/swift-network-evolution/blob/main/Sources/SwiftNetwork/QUIC/Ack.swift#L304C13-L304C24

if ackElicitingPacketsInFlight == 0 {
guard ackElicitingPacketsInFlight > 0 else {
if _slowPath(ackElicitingPacketsInFlight < 0) {
connection.log.fault("ackElicitingPacketsInFlight negative: \(ackElicitingPacketsInFlight)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should probably make sure we are not underflowing ackElicitingPacketsInFlight, possibly adding an underflow check to -= 1 since this is an integer. That way we do not have to log out these negative values.

if !sentPTO {
connection.log.datapath("Sending a PING as PTO")
if peerCompletedValidation {
connection.log.fault("PTO fired after validation")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Historically this log has been a warning that something in Recovery may not be working as expected, or you may have run into a race condition in the logic. For example, I am looking at a case now where ackElicitingPacketsInFlight is 1, but then the handshake completes, clears the ack-eliciting packets, resetPTOCount set the count back 0, and then sendPTO falls into this path as a matter of timing. When this happens sendPTO tries to reset the timer and realizes that there are now no ackElicitingPacketsInFlight and then cancels the timer and continues on as normal.
So in this path we have now removed the resetTimer functionality, what is the rationale there?

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.

2 participants