From 838a01943efa83aac434da6eab6cbee4799a5c8c Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Fri, 28 Aug 2026 16:28:32 +0100 Subject: [PATCH 1/2] Server Initial packets should not carry tokens The RFC (17.2.2) says "Initial packets sent by the server MUST set the Token Length field to 0; clients that receive an Initial packet with a non-zero Token Length field MUST either discard the packet or generate a connection error of type PROTOCOL_VIOLATION." If I read the code correctly, the server might falsely include the issued retry token in the packet. The change only sets the token on the client side. --- Sources/SwiftNetwork/QUIC/QUICConnection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index 0418715..abb2cc9 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -3763,7 +3763,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, sentPacketRecord: &sentPacketRecord, connection: self, availableCongestionWindow: availableCongestionWindow, - token: initialToken, + token: isServer ? nil : initialToken, stats: &stats, version: currentVersion, isServer: isServer, From 8b5e79bf1aa3c8a3c9c5dd4f06a72f334d9c5beb Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Tue, 1 Sep 2026 10:09:42 +0100 Subject: [PATCH 2/2] Reset server token after validation --- Sources/SwiftNetwork/QUIC/QUICConnection.swift | 7 ++++++- .../SwiftNetworkQUICRetryTokenTests.swift | 15 +++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index abb2cc9..d0f8ada 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -1643,6 +1643,11 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, return } + if state == .retrySent { + // The token was validated and is no longer needed. + self.initialToken = nil + } + // Change state from idle to initial received with key state = handshake state.change(to: .initialReceived, logIDString: logPrefixer.logIDString) keyState = .handshake @@ -3763,7 +3768,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, sentPacketRecord: &sentPacketRecord, connection: self, availableCongestionWindow: availableCongestionWindow, - token: isServer ? nil : initialToken, + token: initialToken, stats: &stats, version: currentVersion, isServer: isServer, diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICRetryTokenTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICRetryTokenTests.swift index 7aa9424..537c3ae 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICRetryTokenTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICRetryTokenTests.swift @@ -65,9 +65,10 @@ final class SwiftNetworkQUICRetryTokenTests: NetTestCase { harness.state?.clientInstance.initialToken, "Client should have an initial retry token" ) - XCTAssertNotNil( + // The server's token was reset after verifying the client token. + XCTAssertNil( harness.state?.serverInstance.initialToken, - "Server should have an initial retry token" + "Server must discard the retry token once validated, so it is never sent in an INITIAL" ) expectation.fulfill() } @@ -101,9 +102,10 @@ final class SwiftNetworkQUICRetryTokenTests: NetTestCase { harness.state?.clientInstance.initialToken, "Client should have an initial retry token" ) - XCTAssertNotNil( + // The server's token was reset after verifying the client token. + XCTAssertNil( harness.state?.serverInstance.initialToken, - "Server should have an initial retry token" + "Server must discard the retry token once validated, so it is never sent in an INITIAL" ) expectation.fulfill() } @@ -128,9 +130,10 @@ final class SwiftNetworkQUICRetryTokenTests: NetTestCase { harness.state?.clientInstance.initialToken, "Client should have an initial retry token" ) - XCTAssertNotNil( + // The server's token was reset after verifying the client token. + XCTAssertNil( harness.state?.serverInstance.initialToken, - "Server should have an initial retry token" + "Server must discard the retry token once validated, so it is never sent in an INITIAL" ) expectation.fulfill() }