From 456e104aa6f8cb1d01e1fe07b935ef74d9b5ecf7 Mon Sep 17 00:00:00 2001 From: agnosticdev Date: Thu, 2 Jul 2026 11:38:16 -0700 Subject: [PATCH 1/3] SwiftQUIC: Fix variance in Spin Bit testing --- Sources/SwiftNetwork/QUIC/QUICConnection.swift | 3 +++ Sources/SwiftNetwork/QUIC/QUICPath.swift | 5 +++++ Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index 8ff817c..8113c14 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -2322,6 +2322,9 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, // Reflect the spin bit. let spinValue = packet.spinValue path.spinValue = spinValue + if spinValue { + path.spinValueForTestingSeen = true + } } else { // Spin it! let spinValue = packet.spinValue diff --git a/Sources/SwiftNetwork/QUIC/QUICPath.swift b/Sources/SwiftNetwork/QUIC/QUICPath.swift index 77c6464..3f17225 100644 --- a/Sources/SwiftNetwork/QUIC/QUICPath.swift +++ b/Sources/SwiftNetwork/QUIC/QUICPath.swift @@ -188,6 +188,7 @@ public final class QUICPath: MultiplexingDatagramPath, Equatable static let isFlowControlled = Flags(rawValue: 1 << 9) static let l4sEnabled = Flags(rawValue: 1 << 10) static let reportedIdleEvent = Flags(rawValue: 1 << 11) + static let spinValueForTestingSeen = Flags(rawValue: 1 << 12) } private var flags = Flags() @@ -241,6 +242,10 @@ public final class QUICPath: MultiplexingDatagramPath, Equatable get { flags.contains(.reportedIdleEvent) } set { if newValue { flags.insert(.reportedIdleEvent) } else { flags.remove(.reportedIdleEvent) } } } + var spinValueForTestingSeen: Bool { + get { flags.contains(.spinValueForTestingSeen) } + set { if newValue { flags.insert(.spinValueForTestingSeen) } else { flags.remove(.spinValueForTestingSeen) } } + } var log: LogPrefixer { parentProtocol.logPrefixer diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift index 4243975..1564baa 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift @@ -91,7 +91,10 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { defer { expectation.fulfill() } let clientSpinBit = harness.state?.clientInstance.currentPath?.spinValue ?? false - let serverSpinBit = harness.state?.serverInstance.currentPath?.spinValue ?? false + // Note that spinValueForTestingSeen is checked here because it could be that by the time + // this test checks the server spin bit value it has received the ACK from the client and the + // spin bit value has already changed. This reliably make sure that the spin bit on the server was present. + let serverSpinBit = harness.state?.serverInstance.currentPath?.spinValueForTestingSeen ?? false XCTAssertFalse(clientSpinBit, "Client should have the spin bit set to false") XCTAssertTrue(serverSpinBit, "Server should have the spin bit set to true") } From 2f32370f8e43309d4c834eccbf75e8253555716b Mon Sep 17 00:00:00 2001 From: agnosticdev Date: Thu, 2 Jul 2026 15:53:07 -0700 Subject: [PATCH 2/3] agnosticdev/SpinBitVariance: Observe spin bit from the bridge protocol --- .../Protocols/BridgeProtocol.swift | 9 +++++++ .../SwiftNetwork/QUIC/QUICConnection.swift | 3 --- Sources/SwiftNetwork/QUIC/QUICPath.swift | 5 ---- Tests/SwiftNetworkTests/QUICTestHarness.swift | 25 ++++++++++++++++--- .../SwiftNetworkQUICSpinBitTests.swift | 22 ++++++++++------ 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift index 24658f9..17f67f9 100644 --- a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift @@ -142,6 +142,7 @@ public struct BridgeDatagramProtocol: NetworkProtocol { var linkDelay: NetworkDuration = .zero var datagramDrops: DatagramDrops? = nil + var observeFirstByte: ((UInt8) -> Void)? = nil private var timerSet = false func deliverInboundDataAvailableEvent() { @@ -275,6 +276,14 @@ public struct BridgeDatagramProtocol: NetworkProtocol { datagrams = remainingDatagrams } log.datapath("forwarding \(datagrams.count) datagrams to port: \(remotePort)") + if let observeFirstByte { + datagrams.iterateMutableFrames { frame in + if let bytes = frame.bytes, bytes.byteCount > 0 { + observeFirstByte(bytes[0]) + } + return true + } + } remoteInstance.incomingFrames.add(frames: datagrams) remoteInstance.deliverInboundDataAvailableEvent() } diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index 8113c14..8ff817c 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -2322,9 +2322,6 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, // Reflect the spin bit. let spinValue = packet.spinValue path.spinValue = spinValue - if spinValue { - path.spinValueForTestingSeen = true - } } else { // Spin it! let spinValue = packet.spinValue diff --git a/Sources/SwiftNetwork/QUIC/QUICPath.swift b/Sources/SwiftNetwork/QUIC/QUICPath.swift index 3f17225..77c6464 100644 --- a/Sources/SwiftNetwork/QUIC/QUICPath.swift +++ b/Sources/SwiftNetwork/QUIC/QUICPath.swift @@ -188,7 +188,6 @@ public final class QUICPath: MultiplexingDatagramPath, Equatable static let isFlowControlled = Flags(rawValue: 1 << 9) static let l4sEnabled = Flags(rawValue: 1 << 10) static let reportedIdleEvent = Flags(rawValue: 1 << 11) - static let spinValueForTestingSeen = Flags(rawValue: 1 << 12) } private var flags = Flags() @@ -242,10 +241,6 @@ public final class QUICPath: MultiplexingDatagramPath, Equatable get { flags.contains(.reportedIdleEvent) } set { if newValue { flags.insert(.reportedIdleEvent) } else { flags.remove(.reportedIdleEvent) } } } - var spinValueForTestingSeen: Bool { - get { flags.contains(.spinValueForTestingSeen) } - set { if newValue { flags.insert(.spinValueForTestingSeen) } else { flags.remove(.spinValueForTestingSeen) } } - } var log: LogPrefixer { parentProtocol.logPrefixer diff --git a/Tests/SwiftNetworkTests/QUICTestHarness.swift b/Tests/SwiftNetworkTests/QUICTestHarness.swift index 42aaba7..cc8ab7e 100644 --- a/Tests/SwiftNetworkTests/QUICTestHarness.swift +++ b/Tests/SwiftNetworkTests/QUICTestHarness.swift @@ -76,6 +76,9 @@ final class QUICTestHarness { let clientInstance: QUICProtocol.Instance let serverInstance: QUICProtocol.Instance + + let clientBridge: BridgeDatagramProtocol.Instance + let serverBridge: BridgeDatagramProtocol.Instance } var state: QUICHarnessState? = nil @@ -165,7 +168,8 @@ final class QUICTestHarness { clientOptions.setProtocolInstance(clientReference) clientParameters.defaultStack.transport = .quic(clientOptions) - let clientBridge = BridgeDatagramProtocol.instance(context: self.context) + let clientBridgeInstance = BridgeDatagramProtocol.Instance(context: self.context) + let clientBridge = clientBridgeInstance.reference let clientBridgeOptions = BridgeDatagramProtocol.options() clientBridgeOptions.setProtocolInstance(clientBridge) clientBridgeOptions.linkDelay = clientLinkDelay @@ -192,7 +196,8 @@ final class QUICTestHarness { serverOptions.setProtocolInstance(serverReference) serverParameters.defaultStack.transport = .quic(serverOptions) - let serverBridge = BridgeDatagramProtocol.instance(context: self.context) + let serverBridgeInstance = BridgeDatagramProtocol.Instance(context: self.context) + let serverBridge = serverBridgeInstance.reference let serverBridgeOptions = BridgeDatagramProtocol.options() serverBridgeOptions.setProtocolInstance(serverBridge) serverBridgeOptions.linkDelay = serverLinkDelay @@ -292,7 +297,9 @@ final class QUICTestHarness { clientReference: clientReference, serverReference: serverReference, clientInstance: clientInstance, - serverInstance: serverInstance + serverInstance: serverInstance, + clientBridge: clientBridgeInstance, + serverBridge: serverBridgeInstance ) clientHarness.waitForError { error in @@ -543,6 +550,18 @@ final class QUICTestHarness { wait(for: [stopCompleteExpectation], timeout: 5.0) } + // Installs a callback on the client and server bridge that fires passing the raw first byte to the + // caller for a short header packet. Used to detect a spin bit passing between the client and server. + func observeFirstByteForOutboundShortHeaderPacket(handler: @escaping (_ firstByte: UInt8) -> Void) { + guard let state else { return } + let observe: ((UInt8) -> Void) = { firstByte in + guard (firstByte & 0xC0) == 0x40 else { return } + handler(firstByte) + } + state.clientBridge.observeFirstByte = observe + state.serverBridge.observeFirstByte = observe + } + func echoDataOnStream( dataGenerator: TestDataGenerator, streamIndex: Int, diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift index 1564baa..c090171 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift @@ -60,6 +60,7 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { // it from the server. var hasSpinBit = true + var observedSpinBitValues: Set = [] QUICTestHarness().runQUICTest( dataBlock: Array("Hello World!".utf8), afterHandshake: { harness in @@ -78,6 +79,11 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { hasSpinBit = false return } + // If we have made it this far we should at least be able to detect at least one spin bit value going back and forth + // Detect at least 1 outbound datagram sending a spin bit value + harness.observeFirstByteForOutboundShortHeaderPacket { firstByte in + observedSpinBitValues.insert((firstByte & 0x20) != 0) + } } self.wait(for: [expectation], timeout: 5.0) }, @@ -89,14 +95,14 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { let expectation = XCTestExpectation(description: "Wait to validate spin bit") harness.context.async { defer { expectation.fulfill() } - - let clientSpinBit = harness.state?.clientInstance.currentPath?.spinValue ?? false - // Note that spinValueForTestingSeen is checked here because it could be that by the time - // this test checks the server spin bit value it has received the ACK from the client and the - // spin bit value has already changed. This reliably make sure that the spin bit on the server was present. - let serverSpinBit = harness.state?.serverInstance.currentPath?.spinValueForTestingSeen ?? false - XCTAssertFalse(clientSpinBit, "Client should have the spin bit set to false") - XCTAssertTrue(serverSpinBit, "Server should have the spin bit set to true") + XCTAssertFalse( + observedSpinBitValues.isEmpty, + "BridgeDatagramProtocol should have observed short-header packets with spin bit values" + ) + XCTAssertTrue( + observedSpinBitValues.contains(true), + "BridgeDatagramProtocol should have observed at least one packet with spin bit set" + ) } } ) From 2c404e925bcf969f6a8ef2c2a012bc4fe771427e Mon Sep 17 00:00:00 2001 From: agnosticdev Date: Thu, 2 Jul 2026 21:15:36 -0700 Subject: [PATCH 3/3] agnosticdev/SpinBitVariance: Added first byte observer to protocol options --- .../Protocols/BridgeProtocol.swift | 19 +++++++++-- Tests/SwiftNetworkTests/QUICTestHarness.swift | 34 ++++++------------- .../SwiftNetworkQUICSpinBitTests.swift | 12 +++---- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift index 17f67f9..cf73d12 100644 --- a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift @@ -66,6 +66,8 @@ public struct DatagramDrops: Equatable { } } +public typealias BridgeObserveFirstByteHandler = ((UInt8) -> Void)? + @_spi(Essentials) @available(Network 0.1.0, *) public struct BridgeDatagramProtocol: NetworkProtocol { @@ -75,6 +77,7 @@ public struct BridgeDatagramProtocol: NetworkProtocol { public struct BridgeOptions: PerProtocolOptions { public var linkDelay: NetworkDuration = .zero + public var observeFirstByteHandler: BridgeObserveFirstByteHandler = nil var datagramDrops: DatagramDrops? init() {} @@ -96,6 +99,10 @@ public struct BridgeDatagramProtocol: NetworkProtocol { self == other } + static public func == (lhs: BridgeOptions, rhs: BridgeOptions) -> Bool { + lhs.linkDelay == rhs.linkDelay && lhs.datagramDrops == rhs.datagramDrops + } + var isDefault: Bool { self == BridgeOptions() } @@ -142,7 +149,7 @@ public struct BridgeDatagramProtocol: NetworkProtocol { var linkDelay: NetworkDuration = .zero var datagramDrops: DatagramDrops? = nil - var observeFirstByte: ((UInt8) -> Void)? = nil + var observeFirstByteHandler: BridgeObserveFirstByteHandler = nil private var timerSet = false func deliverInboundDataAvailableEvent() { @@ -184,6 +191,7 @@ public struct BridgeDatagramProtocol: NetworkProtocol { { self.linkDelay = bridgeOptions.linkDelay self.datagramDrops = bridgeOptions.datagramDrops + self.observeFirstByteHandler = bridgeOptions.observeFirstByteHandler } #endif @@ -276,10 +284,10 @@ public struct BridgeDatagramProtocol: NetworkProtocol { datagrams = remainingDatagrams } log.datapath("forwarding \(datagrams.count) datagrams to port: \(remotePort)") - if let observeFirstByte { + if let observeFirstByteHandler { datagrams.iterateMutableFrames { frame in if let bytes = frame.bytes, bytes.byteCount > 0 { - observeFirstByte(bytes[0]) + observeFirstByteHandler(bytes[0]) } return true } @@ -332,6 +340,11 @@ extension ProtocolOptions { set { perProtocolOptions!.linkDelay = newValue } } + public var observeFirstByteHandler: BridgeObserveFirstByteHandler { + get { perProtocolOptions!.observeFirstByteHandler } + set { perProtocolOptions!.observeFirstByteHandler = newValue } + } + public var datagramDrops: DatagramDrops? { get { perProtocolOptions!.datagramDrops } set { perProtocolOptions!.datagramDrops = newValue } diff --git a/Tests/SwiftNetworkTests/QUICTestHarness.swift b/Tests/SwiftNetworkTests/QUICTestHarness.swift index cc8ab7e..e895b40 100644 --- a/Tests/SwiftNetworkTests/QUICTestHarness.swift +++ b/Tests/SwiftNetworkTests/QUICTestHarness.swift @@ -76,9 +76,6 @@ final class QUICTestHarness { let clientInstance: QUICProtocol.Instance let serverInstance: QUICProtocol.Instance - - let clientBridge: BridgeDatagramProtocol.Instance - let serverBridge: BridgeDatagramProtocol.Instance } var state: QUICHarnessState? = nil @@ -141,7 +138,8 @@ final class QUICTestHarness { serverDrops: DatagramDrops? = nil, timeout: TimeInterval = 5.0, clientOptions: ProtocolOptions = QUICProtocol.options(), - serverOptions: ProtocolOptions = QUICProtocol.options() + serverOptions: ProtocolOptions = QUICProtocol.options(), + bridgeObserveFirstByteHandler: BridgeObserveFirstByteHandler = nil ) throws(NetworkError) { var clientConnected = false var serverConnected = false @@ -168,9 +166,9 @@ final class QUICTestHarness { clientOptions.setProtocolInstance(clientReference) clientParameters.defaultStack.transport = .quic(clientOptions) - let clientBridgeInstance = BridgeDatagramProtocol.Instance(context: self.context) - let clientBridge = clientBridgeInstance.reference + let clientBridge = BridgeDatagramProtocol.instance(context: self.context) let clientBridgeOptions = BridgeDatagramProtocol.options() + clientBridgeOptions.observeFirstByteHandler = bridgeObserveFirstByteHandler clientBridgeOptions.setProtocolInstance(clientBridge) clientBridgeOptions.linkDelay = clientLinkDelay clientBridgeOptions.datagramDrops = clientDrops @@ -196,9 +194,9 @@ final class QUICTestHarness { serverOptions.setProtocolInstance(serverReference) serverParameters.defaultStack.transport = .quic(serverOptions) - let serverBridgeInstance = BridgeDatagramProtocol.Instance(context: self.context) - let serverBridge = serverBridgeInstance.reference + let serverBridge = BridgeDatagramProtocol.instance(context: self.context) let serverBridgeOptions = BridgeDatagramProtocol.options() + serverBridgeOptions.observeFirstByteHandler = bridgeObserveFirstByteHandler serverBridgeOptions.setProtocolInstance(serverBridge) serverBridgeOptions.linkDelay = serverLinkDelay serverBridgeOptions.datagramDrops = serverDrops @@ -297,9 +295,7 @@ final class QUICTestHarness { clientReference: clientReference, serverReference: serverReference, clientInstance: clientInstance, - serverInstance: serverInstance, - clientBridge: clientBridgeInstance, - serverBridge: serverBridgeInstance + serverInstance: serverInstance ) clientHarness.waitForError { error in @@ -550,18 +546,6 @@ final class QUICTestHarness { wait(for: [stopCompleteExpectation], timeout: 5.0) } - // Installs a callback on the client and server bridge that fires passing the raw first byte to the - // caller for a short header packet. Used to detect a spin bit passing between the client and server. - func observeFirstByteForOutboundShortHeaderPacket(handler: @escaping (_ firstByte: UInt8) -> Void) { - guard let state else { return } - let observe: ((UInt8) -> Void) = { firstByte in - guard (firstByte & 0xC0) == 0x40 else { return } - handler(firstByte) - } - state.clientBridge.observeFirstByte = observe - state.serverBridge.observeFirstByte = observe - } - func echoDataOnStream( dataGenerator: TestDataGenerator, streamIndex: Int, @@ -933,6 +917,7 @@ final class QUICTestHarness { extraServerCIDs: [(QUICConnectionID, QUICStatelessResetToken)] = .init(), afterHandshake: ((QUICTestHarness) -> Void)? = nil, // Block to run after handshake is complete afterData: ((QUICTestHarness) -> Void)? = nil, // Block to run after handshake is complete + bridgeObserveFirstByteHandler: BridgeObserveFirstByteHandler = nil ) { // Start with the handshake Logger.test.debug("Test phase: Handshake") @@ -952,7 +937,8 @@ final class QUICTestHarness { serverDrops: serverDrops, timeout: timeout, clientOptions: clientOptions, - serverOptions: serverOptions + serverOptions: serverOptions, + bridgeObserveFirstByteHandler: bridgeObserveFirstByteHandler ) } catch { if expectHandshakeError == nil { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift index c090171..af91342 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift @@ -61,6 +61,10 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { var hasSpinBit = true var observedSpinBitValues: Set = [] + let observeFirstByteHandler: BridgeObserveFirstByteHandler = { firstByte in + guard (firstByte & 0xC0) == 0x40 else { return } + observedSpinBitValues.insert((firstByte & 0x20) != 0) + } QUICTestHarness().runQUICTest( dataBlock: Array("Hello World!".utf8), afterHandshake: { harness in @@ -79,11 +83,6 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { hasSpinBit = false return } - // If we have made it this far we should at least be able to detect at least one spin bit value going back and forth - // Detect at least 1 outbound datagram sending a spin bit value - harness.observeFirstByteForOutboundShortHeaderPacket { firstByte in - observedSpinBitValues.insert((firstByte & 0x20) != 0) - } } self.wait(for: [expectation], timeout: 5.0) }, @@ -104,7 +103,8 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { "BridgeDatagramProtocol should have observed at least one packet with spin bit set" ) } - } + }, + bridgeObserveFirstByteHandler: observeFirstByteHandler ) }