diff --git a/Sources/SwiftNetwork/Protocols/Checksum.swift b/Sources/SwiftNetwork/Protocols/Checksum.swift index 0ef95ac..4e6c67f 100644 --- a/Sources/SwiftNetwork/Protocols/Checksum.swift +++ b/Sources/SwiftNetwork/Protocols/Checksum.swift @@ -57,6 +57,23 @@ enum ChecksumError: Error { case invalidBuffer } +struct ChecksumFlags: OptionSet { + let rawValue: UInt8 + static let partial = ChecksumFlags(rawValue: 0x01) + static let zeroInvert = ChecksumFlags(rawValue: 0x02) + static let ip = ChecksumFlags(rawValue: 0x04) + static let tcpIPv4 = ChecksumFlags(rawValue: 0x08) + static let udpIPv4 = ChecksumFlags(rawValue: 0x10) + static let tcpIPv6 = ChecksumFlags(rawValue: 0x20) + static let udpIPv6 = ChecksumFlags(rawValue: 0x40) +} + +struct InterfaceChecksumFlags: OptionSet { + let rawValue: UInt32 + static let udpIPv4 = InterfaceChecksumFlags(rawValue: 0x0000_0004) + static let udpIPv6 = InterfaceChecksumFlags(rawValue: 0x0000_0040) +} + @available(Network 0.1.0, *) extension IPv6Address { func checksum() -> UInt32 { @@ -141,6 +158,11 @@ extension Frame { return ((~value) & 0xffff) } + mutating func setInternetChecksum(flags: ChecksumFlags, startOffset: UInt16, checksumOffset: UInt16) -> Bool { + checksumOffloadFlags |= flags.rawValue + return false + } + mutating func finalizeIPChecksum(checksumOffset: Int, zeroInvert: Bool) throws(ChecksumError) { let unclaimedLength = self.unclaimedLength if unclaimedLength == 0 { diff --git a/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift b/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift index 0875dc3..d3c2984 100644 --- a/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift +++ b/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift @@ -237,6 +237,10 @@ public class UpperHarness: UpperHarnessProtocol return invokeGetMetadata() as? ProtocolMetadata

} + final public func getMetrics(requestedNetworkMetric: RequestedNetworkMetrics) -> NetworkMetrics? { + lower.invokeGetMetrics(reference, requestedNetworkMetric: requestedNetworkMetric) + } + public func setApplicationError(_ applicationError: UInt64, applicationErrorReason: String) { if let metadata: ProtocolMetadata = self.getMetadata() { metadata.perProtocolMetadata?.quicConnectionMetadata?.applicationError = applicationError diff --git a/Sources/SwiftNetwork/Protocols/UDPProtocol.swift b/Sources/SwiftNetwork/Protocols/UDPProtocol.swift index 0f6c05e..7e8d7ab 100644 --- a/Sources/SwiftNetwork/Protocols/UDPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/UDPProtocol.swift @@ -48,6 +48,7 @@ public struct UDPProtocol: NetworkProtocol { static public let noMetadata = UDPOptions(rawValue: 1 << 1) static public let ignoreInboundChecksum = UDPOptions(rawValue: 1 << 2) static public let useQUICStats = UDPOptions(rawValue: 1 << 3) + static public let fullChecksumOffload = UDPOptions(rawValue: 1 << 4) #if NETWORK_PRIVATE var privateStorage = UDPProtocolOptionsPrivateStorage() @@ -97,7 +98,9 @@ public struct UDPProtocol: NetworkProtocol { var eventManager = ProtocolEventManager() // Only called by newProtocolInstance() - fileprivate static func registerNewUDP(on context: NetworkContext) -> ProtocolInstanceReference { + fileprivate static func registerNewUDP( + on context: NetworkContext, + ) -> ProtocolInstanceReference { let udp = UDPInstance(context: context) let registeredIndex = context.registerUDPInstance(udp) context.udpInstances[registeredIndex].udpInstanceIndex = registeredIndex @@ -121,6 +124,9 @@ public struct UDPProtocol: NetworkProtocol { var serviceClass = Parameters.ServiceClass.bestEffort var maximumDatagramSize: Int = 0 + var isIPv4: Bool { + flags.contains(.isIPv4) + } struct Flags: OptionSet { init(rawValue: Self.RawValue) { @@ -153,6 +159,13 @@ public struct UDPProtocol: NetworkProtocol { if self.maximumDatagramSize > UDPProtocol.headerLength { self.maximumDatagramSize -= UDPProtocol.headerLength } + let udpChecksumOffload: UInt32 = path.hardwareChecksumFlags + if isIPv4 && ((udpChecksumOffload & InterfaceChecksumFlags.udpIPv4.rawValue) != 0) + || !isIPv4 && ((udpChecksumOffload & InterfaceChecksumFlags.udpIPv6.rawValue) != 0) + { + self.flags.insert(.fullChecksumOffload) + self.flags.remove(.partialChecksumOffload) + } } mutating func setup( @@ -202,6 +215,21 @@ public struct UDPProtocol: NetworkProtocol { if udpOptions.ignoreInboundChecksum { self.flags.insert(.ignoreInboundChecksum) } + if udpOptions.fullChecksumOffload { + self.flags.insert(.fullChecksumOffload) + } + #if !NETWORK_EMBEDDED + if let transport = parameters.defaultStack.transport { + if transport.options == udpOptions, udpOptions.useQUICStats { + self.flags.insert(.upperTransportIsQUIC) + } else if let quicOptions = transport.options, + quicOptions.matches(identifier: QUICStreamProtocol.identifier) + || quicOptions.matches(identifier: QUICConnectionProtocol.identifier) + { + self.flags.insert(.upperTransportIsQUIC) + } + } + #endif } } @@ -213,7 +241,7 @@ public struct UDPProtocol: NetworkProtocol { let inbound = (inboundChecksum != nil) let existingChecksum: UInt16 = inboundChecksum ?? 0 var checksumValue: UInt16 = 0 - if self.flags.contains(.isIPv4) { + if isIPv4 { checksumValue = Checksum.ipv4PseudoHeader( source: inbound ? ipv4Remote : ipv4Local, dest: inbound ? ipv4Local : ipv4Remote, @@ -295,7 +323,7 @@ public struct UDPProtocol: NetworkProtocol { return .removeFrameAndContinue } - guard self.flags.contains(.isIPv4) || checksum != 0 else { + guard isIPv4 || checksum != 0 else { log.error("Received an IPv6 packet with zero checksum") frame.finalize(success: false) return .removeFrameAndContinue @@ -391,7 +419,7 @@ public struct UDPProtocol: NetworkProtocol { frame.serviceClass = self.serviceClass } - if !self.flags.contains(.isIPv4) || !self.flags.contains(.noChecksum) { + if !isIPv4 || !self.flags.contains(.noChecksum) { // Always insert pseudo header checksum let checksumValue = pseudoHeaderChecksum(inboundChecksum: nil, length: length) @@ -407,23 +435,41 @@ public struct UDPProtocol: NetworkProtocol { return .removeFrameAndContinue } - let finalizedChecksum = false if self.flags.contains(.fullChecksumOffload) { - // TODO: Checksum offload - } else if self.flags.contains(.partialChecksumOffload) { - // TODO: Checksum offload + let csumFlags: ChecksumFlags = + isIPv4 + ? [.udpIPv4, .zeroInvert] + : [.udpIPv6, .zeroInvert] + frame.checksumOffloadFlags |= csumFlags.rawValue } - if !finalizedChecksum { - do throws(ChecksumError) { - try frame.finalizeIPChecksum(checksumOffset: checksumOffset, zeroInvert: true) - } catch { - log.error("Failed to finalize UDP checksum") - frame.finalize(success: false) - return .removeFrameAndContinue + if !self.flags.contains(.fullChecksumOffload) { + var checksumOffloadDisabled = !self.flags.contains(.partialChecksumOffload) + if !checksumOffloadDisabled { + let csumStart = UInt16(isIPv4 ? IPProtocol.ipv4HeaderLength : IPProtocol.ipv6HeaderLength) + let csumWrite = csumStart + UInt16(checksumOffset) + let csumFlags: ChecksumFlags = [.partial, .zeroInvert] + if !frame.setInternetChecksum( + flags: csumFlags, + startOffset: csumStart, + checksumOffset: csumWrite + ) { + checksumOffloadDisabled = true + } + } + + if checksumOffloadDisabled { + do throws(ChecksumError) { + try frame.finalizeIPChecksum(checksumOffset: checksumOffset, zeroInvert: true) + } catch { + log.error("Failed to finalize UDP checksum") + frame.finalize(success: false) + return .removeFrameAndContinue + } } } } + transmitByteCount += length - UDPProtocol.headerLength return .continueIterating } @@ -434,6 +480,11 @@ public struct UDPProtocol: NetworkProtocol { #if !NETWORK_EMBEDDED var metadata: AbstractProtocolMetadata? { nil } #endif + + func updateDataTransferSnapshot(_ snapshot: inout DataTransferSnapshot) { + snapshot.receivedTransportByteCount = UInt64(receiveByteCount) + snapshot.sentTransportByteCount = UInt64(transmitByteCount) + } } public init() {} @@ -506,4 +557,15 @@ extension ProtocolOptions { } } } + + public var fullChecksumOffload: Bool { + get { perProtocolOptions!.contains(.fullChecksumOffload) } + set { + if newValue { + perProtocolOptions!.insert(.fullChecksumOffload) + } else { + perProtocolOptions!.remove(.fullChecksumOffload) + } + } + } } diff --git a/Tests/SwiftNetworkTests/SwiftNetworkMultiplexingTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkMultiplexingTests.swift index 5515a16..7a215a0 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkMultiplexingTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkMultiplexingTests.swift @@ -25,6 +25,10 @@ import XCTest @available(Network 0.1.0, *) final class SwiftNetworkMultiplexingTests: NetTestCase { + // 10.0.0.20 + static let localIPv4Address: [UInt8] = [0x0a, 0x00, 0x00, 0x14] + // 10.0.0.117 + static let remoteIPv4Address: [UInt8] = [0x0a, 0x00, 0x00, 0x75] static let outputMessage: [UInt8] = [0x0a, 0x0b, 0x0c, 0x0d] static let inputMessage: [UInt8] = [0x0d, 0x0c, 0x0b, 0x0a, 0x01] @@ -33,8 +37,8 @@ final class SwiftNetworkMultiplexingTests: NetTestCase { let context = parameters.context let path = PathProperties(parameters: parameters) - let localEndpoint = Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 1234) - let remoteEndpoint = Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 2345) + let localEndpoint = Endpoint(address: IPv4Address(SwiftNetworkMultiplexingTests.localIPv4Address)!, port: 1234) + let remoteEndpoint = Endpoint(address: IPv4Address(SwiftNetworkMultiplexingTests.localIPv4Address)!, port: 2345) var instance: TestMultiplexingProtocol? = nil var upperHarness1: DatagramUpperHarness? @@ -217,8 +221,8 @@ final class SwiftNetworkMultiplexingTests: NetTestCase { let context = parameters.context let path = PathProperties(parameters: parameters) - let localEndpoint = Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 1234) - let remoteEndpoint = Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 2345) + let localEndpoint = Endpoint(address: IPv4Address(SwiftNetworkMultiplexingTests.localIPv4Address)!, port: 1234) + let remoteEndpoint = Endpoint(address: IPv4Address(SwiftNetworkMultiplexingTests.localIPv4Address)!, port: 2345) // Use a high number of streams to ensure that we don't have poor scaling let upperHarnessCount = 1000 diff --git a/Tests/SwiftNetworkTests/SwiftNetworkUDPTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkUDPTests.swift index 51664ec..2353e2e 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkUDPTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkUDPTests.swift @@ -19,7 +19,7 @@ import XCTest #if canImport(SwiftNetwork) @_spi(Essentials) @_spi(ProtocolProvider) @testable import SwiftNetwork #elseif canImport(Network) -@_spi(Essentials) @_spi(ProtocolProvider) import Network +@_spi(Essentials) @_spi(ProtocolProvider) @testable import Network #endif @available(Network 0.1.0, *) @@ -79,7 +79,8 @@ final class SwiftNetworkUDPTests: NetTestCase { outputPacket: [UInt8], inputPacket: [UInt8], secondaryInputPacket: [UInt8]? = nil, - expectBadInput: Bool = false + expectBadInput: Bool = false, + validateMetrics: Bool = false ) { let parameters = Parameters() @@ -169,6 +170,17 @@ final class SwiftNetworkUDPTests: NetTestCase { XCTAssertEqual(expectedInputData, readApplicationData, "Failed to read expected UDP input data") } + if validateMetrics { + let metrics = upperHarness.getMetrics(requestedNetworkMetric: .dataTransferSnapshot) + XCTAssertNotNil(metrics) + switch metrics { + case .dataTransferSnapshot(let snapshot): + XCTAssertTrue(snapshot.receivedTransportByteCount > 0) + XCTAssertTrue(snapshot.sentTransportByteCount > 0) + default: + XCTFail("Failed to get UDP harness metrics") + } + } upperHarness.stop() upperHarness.teardown() @@ -186,6 +198,16 @@ final class SwiftNetworkUDPTests: NetTestCase { ) } + func testUDPIPv4WithMetrics() { + internalTestUDP( + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 1234), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkUDPTests.remoteIPv4Address)!, port: 2345), + outputPacket: SwiftNetworkUDPTests.outputIPv4Packet, + inputPacket: SwiftNetworkUDPTests.inputIPv4Packet, + validateMetrics: true + ) + } + func testUDPIPv6() { internalTestUDP( localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkUDPTests.localIPv6Address)!, port: 1234), @@ -195,6 +217,116 @@ final class SwiftNetworkUDPTests: NetTestCase { ) } + func internalTestUDPChecksumFlags( + localEndpoint: Endpoint, + remoteEndpoint: Endpoint, + fullChecksumOffload: Bool, + preferNoChecksum: Bool = false, + validateOutbound: @escaping (DatagramLowerHarness) -> Void + ) { + let parameters = Parameters() + let expectation = XCTestExpectation() + let context = parameters.context + context.async { + defer { expectation.fulfill() } + let path = PathProperties(parameters: parameters) + + let reference = UDPProtocol.instance(context: context) + let udpOptions = UDPProtocol.options() + udpOptions.fullChecksumOffload = fullChecksumOffload + udpOptions.preferNoChecksum = preferNoChecksum + udpOptions.noMetadata = true + udpOptions.setLogID(prefix: "C", parent: "1", protocolLogIDNumber: 1) + udpOptions.setProtocolInstance(reference) + parameters.defaultStack.transport = .udp(udpOptions) + + let udpLinkage = OutboundDatagramLinkage(reference: reference) + guard + let upperHarness = DatagramUpperHarness( + identifier: "Client", + local: localEndpoint, + remote: remoteEndpoint, + parameters: parameters, + path: path, + context: context, + lowerProtocol: udpLinkage + ) + else { + XCTFail("Failed to attach UDP to upper harness") + return + } + + let lowerHarness = DatagramLowerHarness(identifier: "Client", context: context) + do { + try reference.attachLowerDatagramProtocol( + lowerHarness.reference, + remote: remoteEndpoint, + local: localEndpoint, + parameters: parameters, + path: path + ) + } catch { + XCTFail("Failed to attach UDP to lower harness") + return + } + + upperHarness.start { connected in + XCTAssertTrue(connected, "UDP failed to become connected") + } + + var message = SwiftNetworkUDPTests.outputMessage + XCTAssertTrue(message.withUTF8 { upperHarness.write($0.map { $0 }) }, "Failed to write") + + validateOutbound(lowerHarness) + + upperHarness.stop() + upperHarness.teardown() + } + wait(for: [expectation], timeout: 10.0) + } + + func testUDPIPv4FullChecksumOffload() { + internalTestUDPChecksumFlags( + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 1234), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkUDPTests.remoteIPv4Address)!, port: 2345), + fullChecksumOffload: true + ) { lowerHarness in + let flags = lowerHarness.pendingOutboundPackets.peekFirstFrame { $0.checksumOffloadFlags } + // udpv4 = 0x10, zeroInvert = 0x02 → 0x12 + XCTAssertEqual(flags, 0x10 | 0x02, "Expected fullChecksumOffload flags for IPv4 UDP") + } + } + + func testUDPIPv6FullChecksumOffload() { + internalTestUDPChecksumFlags( + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkUDPTests.localIPv6Address)!, port: 1234), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkUDPTests.remoteIPv6Address)!, port: 2345), + fullChecksumOffload: true + ) { lowerHarness in + let flags = lowerHarness.pendingOutboundPackets.peekFirstFrame { $0.checksumOffloadFlags } + // udpv6 = 0x40, zeroInvert = 0x02 → 0x42 + XCTAssertEqual(flags, 0x40 | 0x02, "Expected fullChecksumOffload flags for IPv6 UDP") + } + } + + func testUDPIPv4NoChecksum() { + internalTestUDPChecksumFlags( + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 1234), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkUDPTests.remoteIPv4Address)!, port: 2345), + fullChecksumOffload: false, + preferNoChecksum: true + ) { lowerHarness in + guard let packet = lowerHarness.extractLastOutboundPacket() else { + XCTFail("No outbound packet") + return + } + // Everything should be zero here + XCTAssertTrue(packet.count >= 8, "Packet too short") + XCTAssertEqual(packet[6], 0x00, "Checksum high byte should be zero") + XCTAssertEqual(packet[7], 0x00, "Checksum low byte should be zero") + } + } + func testBadChecksumUDPIPv4() { internalTestUDP( localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkUDPTests.localIPv4Address)!, port: 1234),