diff --git a/Sources/SwiftNetwork/Protocols/Frame.swift b/Sources/SwiftNetwork/Protocols/Frame.swift index 033b7a3..f4b8500 100644 --- a/Sources/SwiftNetwork/Protocols/Frame.swift +++ b/Sources/SwiftNetwork/Protocols/Frame.swift @@ -576,7 +576,7 @@ public struct Frame: ~Copyable { } return } - guard newValue < 2 ^ 6 else { + guard newValue < 64 else { Logger.proto.fault("Cannot set DSCP value of \(newValue)") return } diff --git a/Sources/SwiftNetwork/Protocols/IPProtocol.swift b/Sources/SwiftNetwork/Protocols/IPProtocol.swift index c7c4464..db8bc33 100644 --- a/Sources/SwiftNetwork/Protocols/IPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/IPProtocol.swift @@ -371,11 +371,6 @@ public struct IPProtocol: NetworkProtocol { var dscpValue: UInt8? } - struct IPReassemblyState: ~Copyable { - var reassemblyID: UInt16 - var inputReassemblyFrames = FrameArray() - } - struct IPInstanceFlags: OptionSet { init(rawValue: Self.RawValue) { self.rawValue = rawValue @@ -448,7 +443,12 @@ public struct IPProtocol: NetworkProtocol { var flags = IPInstanceFlags() var counters = IPCounters() var pathProperties = IPPathProperties() - var reassemblyState: IPReassemblyState? + var reassemblyState: IPv4ReassemblyState? + + struct IPv4ReassemblyState: ~Copyable { + var reassemblyID: UInt16 + var inputReassemblyFrames = FrameArray() + } static var headerLength: Int { MemoryLayout.size * 5 @@ -664,7 +664,7 @@ public struct IPProtocol: NetworkProtocol { // Only update the stored reassembly ID when processing a real fragment and not on force flush if !forceFlush { if reassemblyState == nil { - reassemblyState = IPReassemblyState(reassemblyID: ipID) + reassemblyState = IPv4ReassemblyState(reassemblyID: ipID) } else { reassemblyState?.reassemblyID = ipID } @@ -672,7 +672,6 @@ public struct IPProtocol: NetworkProtocol { } mutating func processInboundFrames(_ log: borrowing NetworkLoggerState, _ inboundFrames: inout FrameArray) { - // Hoist loop-invariant values. let localAddress: UInt32 = self.localAddress.addressValue let remoteAddress: UInt32 = self.remoteAddress.addressValue let mask = (0xF000_0000 as UInt32).bigEndian @@ -1013,7 +1012,28 @@ public struct IPProtocol: NetworkProtocol { var flags = IPInstanceFlags() var counters = IPCounters() var pathProperties = IPPathProperties() - var reassemblyState: IPReassemblyState? + var reassemblyState: IPv6ReassemblyState? + + static let fragmentExtensionHeader: UInt8 = 44 + static let hopoptsExtensionHeader: UInt8 = 0 + static let routingExtensionHeader: UInt8 = 43 + static let dstoptsExtensionHeader: UInt8 = 60 + static let fragmentExtensionHeaderLength: Int = 8 + static let ip6fOffMask: UInt16 = 0xFFF8 + static let ip6fMoreFragmentMask: UInt16 = 0x0001 + + struct IPv6ReassemblyState: ~Copyable { + var reassemblyID: UInt32 + var inputReassemblyFrames = FrameArray() + } + + struct IPv6FragmentValues { + var fragmentOffset: UInt16 + var moreFragments: Bool + var payloadOffset: Int + var innerLength: Int + var nextProtocol: UInt8 + } static var minimalMTU: Int { 1280 @@ -1030,17 +1050,269 @@ public struct IPProtocol: NetworkProtocol { return value + IPv6Instance.headerLength } + static func parseFragmentValues( + _ frame: inout Frame, + ipProtocolNumber: UInt8 + ) -> IPv6FragmentValues? { + var payloadLength: UInt16 = 0 + var firstProto: UInt8 = 0 + var fragmentOffset: UInt16 = 0 + var moreFragments = false + var headerOffset = IPv6Instance.headerLength + var nextProtocol: UInt8 = 0 + var foundFragment = false + + let result = Deserializer.deserialize(&frame, claim: false) { read throws(DeserializationError) in + try read.skip(4) + try read.uint16NetworkByteOrder(&payloadLength) + try read.uint8(&firstProto) + try read.skip(1) + try read.skip(32) // src and dst addresses + var currentProto = firstProto + extensionHeaderLoop: while currentProto != ipProtocolNumber { + switch currentProto { + case IPv6Instance.fragmentExtensionHeader: + var nextProto: UInt8 = 0 + var offsetAndFlags: UInt16 = 0 + try read.uint8(&nextProto) + try read.skip(1) + try read.uint16NetworkByteOrder(&offsetAndFlags) + try read.skip(4) + fragmentOffset = offsetAndFlags & IPv6Instance.ip6fOffMask + moreFragments = (offsetAndFlags & IPv6Instance.ip6fMoreFragmentMask) != 0 + nextProtocol = nextProto + headerOffset += IPv6Instance.fragmentExtensionHeaderLength + currentProto = nextProto + foundFragment = true + break extensionHeaderLoop + case IPv6Instance.hopoptsExtensionHeader, + IPv6Instance.routingExtensionHeader, + IPv6Instance.dstoptsExtensionHeader: + var extensionNext: UInt8 = 0 + var extensionLength: UInt8 = 0 + try read.uint8(&extensionNext) + try read.uint8(&extensionLength) + let extensionTotal = (Int(extensionLength) + 1) * 8 + try read.skip(extensionTotal - 2) + headerOffset += extensionTotal + currentProto = extensionNext + default: + break extensionHeaderLoop + } + } + } + + guard result.isValid && foundFragment else { return nil } + let innerLength = Int(payloadLength) - (headerOffset - IPv6Instance.headerLength) + guard innerLength >= 0 else { return nil } + return IPv6FragmentValues( + fragmentOffset: fragmentOffset, + moreFragments: moreFragments, + payloadOffset: headerOffset, + innerLength: innerLength, + nextProtocol: nextProtocol + ) + } + + mutating func appendReassembledPackets( + _ log: borrowing NetworkLoggerState, + reassembled: inout FrameArray + ) { + guard let empty = reassemblyState?.inputReassemblyFrames.isEmpty, !empty else { + return + } + guard let reassemblyID = reassemblyState?.reassemblyID else { + return + } + // Overlapping IPv6 fragments are not allowed due [RFC 5722] + // Verify all stored fragments are contiguous and in offset order + var complete = false + var expectedOffset: UInt16 = 0 + var firstTrafficClass: UInt8 = 0 + var firstHopLimit: UInt8 = 0 + var firstNextProtocol: UInt8 = 0 + var isFirstFragment = true + reassemblyState?.inputReassemblyFrames.iterateMutableFrames { fragment in + guard + let values = IPv6Instance.parseFragmentValues( + &fragment, + ipProtocolNumber: self.ipProtocolNumber + ) + else { + log.info("Reassembly frame is no longer valid for ID \(reassemblyID)") + return false + } + if isFirstFragment { + // Read traffic class and hop limit directly from the IPv6 base header + let result = Deserializer.deserialize(&fragment, claim: false) { + read throws(DeserializationError) in + var flow: UInt32 = 0 + try read.uint32NetworkByteOrder(&flow) + firstTrafficClass = UInt8((flow >> 20) & 0xFF) + try read.skip(3) // payload length + next header + try read.uint8(&firstHopLimit) + } + guard result.isValid else { + return false + } + firstNextProtocol = values.nextProtocol + isFirstFragment = false + } + guard values.fragmentOffset == expectedOffset else { + log.debug("IPv6 fragment out of order for ID \(reassemblyID)") + return false + } + let (next, overflow) = expectedOffset.addingReportingOverflow(UInt16(values.innerLength)) + guard !overflow else { + log.error("Fragment offset overflow for IPv6 ID \(reassemblyID)") + return false + } + expectedOffset = next + if !values.moreFragments { + complete = true + return false + } + return true + } + guard complete else { + log.debug("Fragments for IPv6 ID \(reassemblyID) incomplete") + return + } + // Create a new frame for reassembly + let newFrameLength = IPv6Instance.headerLength + Int(expectedOffset) + var newFrame = Frame(count: newFrameLength) + + // Copy the IPv6 header from the first fragment + let headerCopied = reassemblyState?.inputReassemblyFrames.peekFirstFrame { first in + first.copyInto(&newFrame, length: IPv6Instance.headerLength) + } + guard headerCopied == IPv6Instance.headerLength else { + log.error("Failed to copy IPv6 header from first fragment (ID \(reassemblyID))") + newFrame.finalize(success: false) + return + } + // Update payload length and next header based on the first fragments values + let headerUpdateResult = Serializer.serialize(&newFrame, claim: false) { + write throws(SerializationError) in + try write.skip(4) + try write.uint16NetworkByteOrder(expectedOffset) + try write.uint8(firstNextProtocol) + } + guard headerUpdateResult.isValid else { + log.error("Failed to update IPv6 header in reassembled frame (ID \(reassemblyID))") + newFrame.finalize(success: false) + return + } + // Claim the IPv6 header so subsequent payload writes target the payload region + guard newFrame.claim(fromStart: IPv6Instance.headerLength) else { + log.error("Failed to claim IPv6 header in reassembled frame (ID \(reassemblyID))") + newFrame.finalize(success: false) + return + } + // Copy each fragments inner payload contiguously into the new frame + var writeOffset = 0 + var copyFailed = false + reassemblyState?.inputReassemblyFrames.iterateMutableFrames { fragment in + guard + let values = IPv6Instance.parseFragmentValues( + &fragment, + ipProtocolNumber: self.ipProtocolNumber + ) + else { + log.error("Failed to re-parse fragment during copy for IPv6 ID \(reassemblyID)") + copyFailed = true + return false + } + let copied = fragment.copyInto( + &newFrame, + atOffset: writeOffset, + fromOffset: values.payloadOffset, + length: values.innerLength + ) + guard copied == values.innerLength else { + log.error( + "Fragment payload copy mismatch for IPv6 ID \(reassemblyID): \(copied) != \(values.innerLength)" + ) + copyFailed = true + return false + } + writeOffset += values.innerLength + return true + } + + guard !copyFailed else { + newFrame.finalize(success: false) + return + } + + log.debug("IPv6 reassembly complete for ID \(reassemblyID), total length \(newFrameLength)") + + newFrame.dscpValue = firstTrafficClass >> 2 + if self.flags.receiveHopLimit { + newFrame.hopLimit = firstHopLimit + } + if self.flags.calculateReceiveTime { + newFrame.timestamp = Frame.FrameTimestamp.receiveTime(.now) + } + newFrame.metadataComplete = true + reassembled.add(frame: newFrame) + + // Finalize the original fragment frames. + while var fragment = reassemblyState?.inputReassemblyFrames.popFirst() { + fragment.finalize(success: true) + } + } + + mutating func processReassembly( + _ log: borrowing NetworkLoggerState, + fragmentID: UInt32, + reassembled: inout FrameArray, + forceFlush: Bool + ) { + let hasAccumulatedFragments = reassemblyState?.inputReassemblyFrames.isEmpty == false + let isNewID = reassemblyState?.reassemblyID != fragmentID + + if hasAccumulatedFragments && (isNewID || forceFlush) { + appendReassembledPackets(log, reassembled: &reassembled) + // Only discard buffered fragments when the IP ID change + if isNewID && !forceFlush { + var dropped = 0 + while var fragment = reassemblyState?.inputReassemblyFrames.popFirst() { + fragment.finalize(success: false) + dropped += 1 + } + if dropped > 0 { + log.error( + "Dropping \(dropped) incomplete IPv6 fragments for ID \(reassemblyState?.reassemblyID ?? 0)" + ) + } + } + } + if !forceFlush { + if reassemblyState == nil { + reassemblyState = IPv6ReassemblyState(reassemblyID: fragmentID) + } else { + reassemblyState?.reassemblyID = fragmentID + } + } + } + mutating func processInboundFrames(_ log: borrowing NetworkLoggerState, _ inboundFrames: inout FrameArray) { + + let localAddress = self.localAddress.addressValue + let remoteAddress = self.remoteAddress.addressValue + // IP fragments are not common so preserve a fast-path that just loops inboundFrames in-place + var hadFragments = false + // If fragments are present, hadFragments will be set and metadataComplete will not be set on the frame. inboundFrames.iterateMutableFrames { frame in let originalFrameLength = frame.unclaimedLength var flow: UInt32 = 0 var payloadLength: UInt16 = 0 var hopLimit: UInt8 = 0 var nextProtocol: UInt8 = 0 - let remoteAddress = self.remoteAddress.addressValue - let localAddress = self.localAddress.addressValue - let result = Deserializer.deserialize(&frame, claim: true) { read throws(DeserializationError) in + // Do not completely claim the header so any future parsing + let result = Deserializer.deserialize(&frame, claim: false) { read throws(DeserializationError) in try read.uint32NetworkByteOrder(&flow) try read.uint16NetworkByteOrder(&payloadLength) try read.uint8(&nextProtocol) @@ -1082,8 +1354,67 @@ public struct IPProtocol: NetworkProtocol { frame.finalize(success: false) return .removeFrameAndContinue } + + var currentProto = nextProtocol + var headerOffset = IPv6Instance.headerLength + var isFragment = false + var parseError = false + if currentProto != self.ipProtocolNumber { + if frame.isSingleIPAggregate { + log.fault( + "Received IPv6 extension-headers on a super-packet with length \(originalFrameLength)" + ) + frame.finalize(success: false) + return .removeFrameAndContinue + } + let extensionResult = Deserializer.deserialize(&frame, claim: false) { + read throws(DeserializationError) in + try read.skip(IPv6Instance.headerLength) + extensionHeaderLoop: while currentProto != self.ipProtocolNumber { + switch currentProto { + case IPv6Instance.fragmentExtensionHeader: + var nextProto: UInt8 = 0 + try read.uint8(&nextProto) + try read.skip(1) + try read.skip(2) + try read.skip(4) + headerOffset += IPv6Instance.fragmentExtensionHeaderLength + currentProto = nextProto + isFragment = true + break extensionHeaderLoop + case IPv6Instance.hopoptsExtensionHeader, + IPv6Instance.routingExtensionHeader, + IPv6Instance.dstoptsExtensionHeader: + var extensionNext: UInt8 = 0 + var extensionLength: UInt8 = 0 + try read.uint8(&extensionNext) + try read.uint8(&extensionLength) + let extensionTotal = (Int(extensionLength) + 1) * 8 + try read.skip(extensionTotal - 2) + headerOffset += extensionTotal + currentProto = extensionNext + default: + break extensionHeaderLoop + } + } + } + if !extensionResult.isValid { + log.info("Failed to parse IPv6 extension headers: \(extensionResult)") + parseError = true + } + } + guard !parseError && currentProto == self.ipProtocolNumber else { + frame.finalize(success: false) + return .removeFrameAndContinue + } + // Fragment detected, leave the fraim unclaimed and defer to the reassembly path + if isFragment { + hadFragments = true + return .continueIterating + } + let trafficClassShift = flow >> 4 - let trafficClass = trafficClassShift & 0xFF + let trafficClass = UInt8(trafficClassShift & 0xFF) let ipECN = IPProtocol.ECN(UInt8(trafficClass)) frame.ecnFlag = ipECN switch ipECN { @@ -1103,21 +1434,131 @@ public struct IPProtocol: NetworkProtocol { if self.flags.receiveHopLimit { frame.hopLimit = hopLimit } + frame.dscpValue = trafficClass >> 2 frame.metadataComplete = true - if nextProtocol != self.ipProtocolNumber { - // TODO: Handle Fragmentation, header extensions - frame.finalize(success: false) - return .removeFrameAndContinue - } - _ = frame.claim( - fromStart: 0, + fromStart: headerOffset, fromEnd: originalFrameLength - (Int(payloadLength) + IPv6Instance.headerLength) ) self.counters.rxPackets += 1 return .continueIterating } + + // Fast path: no fragments and no prior reassembly state, return here + guard hadFragments || reassemblyState != nil else { return } + + // Fragment reassembly path, this is not common so reparse and build up the reassembly queue + var processedFrames = FrameArray(capacity: inboundFrames.count) + var reassembledFragments = FrameArray() + + while var frame = inboundFrames.popFirst() { + // metadataComplete signals that the frame does not need to be processed + guard !frame.metadataComplete else { + processedFrames.add(frame: frame) + continue + } + + var fragmentID: UInt32 = 0 + var fragmentOffset: UInt16 = 0 + var moreFragments = false + var foundFragment = false + let parseResult = Deserializer.deserialize(&frame, claim: false) { + read throws(DeserializationError) in + try read.skip(4) + try read.skip(2) + var firstProto: UInt8 = 0 + try read.uint8(&firstProto) + try read.skip(1) + try read.skip(32) // source and destination address + var currentProto = firstProto + extensionHeaderLoop: while currentProto != self.ipProtocolNumber { + switch currentProto { + case IPv6Instance.fragmentExtensionHeader: + var nextProto: UInt8 = 0 + var offsetFlags: UInt16 = 0 + var identifier: UInt32 = 0 + try read.uint8(&nextProto) + try read.skip(1) + try read.uint16NetworkByteOrder(&offsetFlags) + try read.uint32(&identifier) + fragmentOffset = offsetFlags & IPv6Instance.ip6fOffMask + moreFragments = (offsetFlags & IPv6Instance.ip6fMoreFragmentMask) != 0 + fragmentID = identifier + currentProto = nextProto + foundFragment = true + break extensionHeaderLoop + case IPv6Instance.hopoptsExtensionHeader, + IPv6Instance.routingExtensionHeader, + IPv6Instance.dstoptsExtensionHeader: + var extensionNext: UInt8 = 0 + var extensionLength: UInt8 = 0 + try read.uint8(&extensionNext) + try read.uint8(&extensionLength) + try read.skip((Int(extensionLength) + 1) * 8 - 2) + currentProto = extensionNext + default: + break extensionHeaderLoop + } + } + } + guard parseResult.isValid && foundFragment else { + frame.finalize(success: false) + continue + } + processReassembly( + log, + fragmentID: fragmentID, + reassembled: &reassembledFragments, + forceFlush: false + ) + + let currentFragmentCount = reassemblyState?.inputReassemblyFrames.count ?? 0 + guard currentFragmentCount < IPMaxFragmentCount else { + log.error("Too many fragments for IPv6 ID \(fragmentID)") + frame.finalize(success: false) + continue + } + + // Insert fragment in offset order, if not, sort them + if fragmentOffset == 0 { + reassemblyState?.inputReassemblyFrames.prepend(frame: frame) + } else if !moreFragments { + reassemblyState?.inputReassemblyFrames.add(frame: frame) + } else { + // Sort the fragments in offset order + var sorted = FrameArray() + var predecessorFound = false + while var existing = reassemblyState?.inputReassemblyFrames.popFirst() { + if !predecessorFound, + let existingValue = IPv6Instance.parseFragmentValues( + &existing, + ipProtocolNumber: self.ipProtocolNumber + ) + { + let predecessorEnd = + UInt32(existingValue.fragmentOffset) + UInt32(existingValue.innerLength) + if UInt32(fragmentOffset) == predecessorEnd { + sorted.add(frame: existing) + predecessorFound = true + break + } + } + sorted.add(frame: existing) + } + sorted.add(frame: frame) + if predecessorFound { + while let remaining = reassemblyState?.inputReassemblyFrames.popFirst() { + sorted.add(frame: remaining) + } + } + reassemblyState?.inputReassemblyFrames.add(frames: sorted) + } + self.counters.rxPackets += 1 + } + processReassembly(log, fragmentID: 0, reassembled: &reassembledFragments, forceFlush: true) + processedFrames.add(frames: reassembledFragments) + inboundFrames.add(frames: processedFrames) } func prepareOutboundFrames(_ outboundFrames: inout FrameArray) { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkIPTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkIPTests.swift index 3c1a4cb..780ed1b 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkIPTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkIPTests.swift @@ -238,6 +238,223 @@ final class SwiftNetworkIPTests: NetTestCase { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, ] + // fe80::1cd6:90f7:2466:d31b -> fe80::1444:4d27:b896:d383, fragment 1 of 2: first 8 bytes of inputMessage, MF=1 (More fragments) + // reassemblyID = 0xABCD1234 + static let twoIPv6FragmentInputPacket1: [UInt8] = [ + // IPv6 base header (40 bytes) + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x01, 0xAB, 0xCD, 0x12, 0x34, + // Payload: first 8 bytes of inputMessage + 0x04, 0xD2, 0xFC, 0xF7, 0x00, 0x0D, 0xE8, 0x04, + ] + + // fe80::1cd6:90f7:2466:d31b -> fe80::1444:4d27:b896:d383, fragment 2 of 2: last 5 bytes of inputMessage, MF=0 (Last fragment) + // reassemblyID = 0xABCD1234 + static let twoIPv6FragmentInputPacket2: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x08, 0xAB, 0xCD, 0x12, 0x34, + // Payload: last 5 bytes of inputMessage + 0x68, 0x65, 0x6C, 0x6C, 0x6F, + ] + + // fe80::1cd6:90f7:2466:d31b -> fe80::1444:4d27:b896:d383, fragment 1 of 3: bytes 0-7, MF=1, offset=0 (More fragments) + // reassemblyID = 0xDEADBEEF + static let threeIPv6FragmentInputPacket1: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ] + + // fragment 2 of 3: bytes 8-15, MF=1, offset=8 (More fragments) + // reassemblyID = 0xDEADBEEF + static let threeIPv6FragmentInputPacket2: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x09, 0xDE, 0xAD, 0xBE, 0xEF, + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + ] + + // fragment 3 of 3: bytes 16-23, MF=0, offset=16 (Last fragment) + // reassemblyID = 0xDEADBEEF + static let threeIPv6FragmentInputPacket3: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x10, 0xDE, 0xAD, 0xBE, 0xEF, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + ] + + // Fragment 1 of 2: offset=0, MF=1, 16 payload bytes. Fragment 2 starts at offset=8, overlapping the last 8 + // bytes of fragment 1. RFC 5722 forbids overlapping fragments; the sequence must be rejected. + // reassemblyID = 0xAABBCCDD + static let overlappingIPv6FragmentPacket1: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2C, 0x40, // payloadLength=24 (8 frag hdr + 16 data) + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x01, 0xAA, 0xBB, 0xCC, 0xDD, // offset=0, MF=1 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + ] + // Fragment 2: offset=8 bytes (overlaps last 8 bytes of fragment 1), MF=0. + // reassemblyID = 0xAABBCCDD + static let overlappingIPv6FragmentPacket2: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, // payloadLength=16 (8 frag hdr + 8 data) + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x08, 0xAA, 0xBB, 0xCC, 0xDD, // offset=8, MF=0 — overlaps fragment 1 + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + ] + + // Fragment 1 of 2: offset=0, MF=1, 8 payload bytes. + // Fragment 2 jumps to offset=16, skipping offset=8. (More fragments) + // reassemblyID = 0x11223344 + static let gappedIPv6FragmentPacket1: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x01, 0x11, 0x22, 0x33, 0x44, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ] + // Fragment at offset=16 (skips offset=8 entirely), MF=0. (Last fragment) + // reassemblyID = 0x11223344 + static let gappedIPv6FragmentPacket2: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x10, 0x11, 0x22, 0x33, 0x44, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + ] + + // Two fragments both with MF=1 (no terminal fragment). + // reassemblyID = 0x55667788 + static let allMFSetIPv6FragmentPacket1: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x01, 0x55, 0x66, 0x77, 0x88, // offset=0, MF=1 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ] + // Fragment at offset=8, also MF=1. + // reassemblyID = 0x55667788 + static let allMFSetIPv6FragmentPacket2: [UInt8] = [ + 0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2C, 0x40, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0xD6, 0x90, 0xF7, 0x24, 0x66, 0xD3, 0x1B, + 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x44, 0x4D, 0x27, 0xB8, 0x96, 0xD3, 0x83, + 0x11, 0x00, 0x00, 0x09, 0x55, 0x66, 0x77, 0x88, // offset=8, MF=1 + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + ] + + // Two fragments whose combined innerLength totals 65536 bytes, overflowing UInt16. + // Fragment 1: innerLength=32760, payloadLength=32768 (0x8000) + // reassemblyID = 0x99AABBCC + static let overflowIPv6FragmentPacket1: [UInt8] = { + var packet = [UInt8](repeating: 0, count: 32808) + packet[0] = 0x60 + packet[4] = 0x80 + packet[5] = 0x00 // payloadLength = 32768 + packet[6] = 0x2C + packet[7] = 0x40 + packet[8] = 0xFE + packet[9] = 0x80 + packet[16] = 0x1C + packet[17] = 0xD6 + packet[18] = 0x90 + packet[19] = 0xF7 + packet[20] = 0x24 + packet[21] = 0x66 + packet[22] = 0xD3 + packet[23] = 0x1B + packet[24] = 0xFE + packet[25] = 0x80 + packet[32] = 0x14 + packet[33] = 0x44 + packet[34] = 0x4D + packet[35] = 0x27 + packet[36] = 0xB8 + packet[37] = 0x96 + packet[38] = 0xD3 + packet[39] = 0x83 + packet[40] = 0x11 + packet[41] = 0x00 + packet[42] = 0x00 + packet[43] = 0x01 // offsetFlags: offset=0, MF=1 + packet[44] = 0x99 + packet[45] = 0xAA + packet[46] = 0xBB + packet[47] = 0xCC + return packet + }() + + // Fragment 2: offset=32760 (unit=4095, offsetFlags=0x7FF8), MF=0, innerLength=32776. + // payloadLength = 32784 (0x8010). Combined innerLength: 32760+32776 = 65536 → UInt16 overflow. + // reassemblyID = 0x99AABBCC + static let overflowIPv6FragmentPacket2: [UInt8] = { + var packet = [UInt8](repeating: 0, count: 32824) + packet[0] = 0x60 + packet[4] = 0x80 + packet[5] = 0x10 // payloadLength = 32784 + packet[6] = 0x2C + packet[7] = 0x40 + packet[8] = 0xFE + packet[9] = 0x80 + packet[16] = 0x1C + packet[17] = 0xD6 + packet[18] = 0x90 + packet[19] = 0xF7 + packet[20] = 0x24 + packet[21] = 0x66 + packet[22] = 0xD3 + packet[23] = 0x1B + packet[24] = 0xFE + packet[25] = 0x80 + packet[32] = 0x14 + packet[33] = 0x44 + packet[34] = 0x4D + packet[35] = 0x27 + packet[36] = 0xB8 + packet[37] = 0x96 + packet[38] = 0xD3 + packet[39] = 0x83 + packet[40] = 0x11 + packet[41] = 0x00 + packet[42] = 0x7F + packet[43] = 0xF8 // offsetFlags: offset=32760 (4095 units), MF=0 + packet[44] = 0x99 + packet[45] = 0xAA + packet[46] = 0xBB + packet[47] = 0xCC + return packet + }() + func internalTestIP( localEndpoint: Endpoint, remoteEndpoint: Endpoint, @@ -496,11 +713,15 @@ final class SwiftNetworkIPTests: NetTestCase { } func testThreeAIPv4FragmentsToReassemble() { - let readBytes = processIPv4Fragment(packets: [ - SwiftNetworkIPTests.threeFragmentInputPacket1, - SwiftNetworkIPTests.threeFragmentInputPacket2, - SwiftNetworkIPTests.threeFragmentInputPacket3, - ]) + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.threeFragmentInputPacket1, + SwiftNetworkIPTests.threeFragmentInputPacket2, + SwiftNetworkIPTests.threeFragmentInputPacket3, + ], + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.localIPv4Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.remoteIPv4Address)!, port: 0) + ) XCTAssertNotNil(readBytes, "Failed to receive reassembled IPv4 packet from three fragments") if let readBytes { XCTAssertEqual( @@ -512,10 +733,14 @@ final class SwiftNetworkIPTests: NetTestCase { } func testTwoIPv4FragmentsToReassemble() { - let readBytes = processIPv4Fragment(packets: [ - SwiftNetworkIPTests.twoFragmentInputPacket1, - SwiftNetworkIPTests.twoFragmentInputPacket2, - ]) + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.twoFragmentInputPacket1, + SwiftNetworkIPTests.twoFragmentInputPacket2, + ], + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.localIPv4Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.remoteIPv4Address)!, port: 0) + ) XCTAssertNotNil(readBytes, "Failed to receive reassembled IPv4 packet from two fragments") if let readBytes { XCTAssertEqual(readBytes, SwiftNetworkIPTests.inputMessage, "Reassembled payload did not match expected") @@ -525,35 +750,166 @@ final class SwiftNetworkIPTests: NetTestCase { // Verifies that appendReassembledPackets discards a fragment when it detects an offset gap. // fragment 1 has byte offset 0, fragment 2 jumps to byte offset 16, skipping 8. func testGappedIPv4FragmentsProduceNoReassembledFrame() { - let readBytes = processIPv4Fragment(packets: [ - SwiftNetworkIPTests.gappedFragmentInputPacket1, - SwiftNetworkIPTests.gappedFragmentInputPacket2, - ]) + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.gappedFragmentInputPacket1, + SwiftNetworkIPTests.gappedFragmentInputPacket2, + ], + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.localIPv4Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.remoteIPv4Address)!, port: 0) + ) XCTAssertNil(readBytes, "Unexpectedly received a packet from a gapped fragment sequence") } // Verifies that appendReassembledPackets does not create a reassembled fragment without a terminal MF=0 flag. - func testAllMFSetFragmentsProduceNoReassembledFrame() { - let readBytes = processIPv4Fragment(packets: [ - SwiftNetworkIPTests.noTerminalFragmentPacket1, - SwiftNetworkIPTests.noTerminalFragmentPacket2, - ]) + func testIPv4AllMFSetFragmentsProduceNoReassembledFrame() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.noTerminalFragmentPacket1, + SwiftNetworkIPTests.noTerminalFragmentPacket2, + ], + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.localIPv4Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.remoteIPv4Address)!, port: 0) + ) XCTAssertNil(readBytes, "Unexpectedly received a packet when no terminal fragment was present") } // Verifies that appendReassembledPackets rejects a complete fragment sequence whose combined - // payload would cause rawLength (headerLength + totalPayload) exceeds UInt16.max. - func testReassembledLengthOverflowProducesNoFrame() { - let readBytes = processIPv4Fragment(packets: [ - SwiftNetworkIPTests.overflowFragmentPacket1, - SwiftNetworkIPTests.overflowFragmentPacket2, - ]) + // payload would cause rawLength (headerLength + totalPayload) exceeds UInt16.max + func testIPv4ReassembledLengthOverflowProducesNoFrame() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.overflowFragmentPacket1, + SwiftNetworkIPTests.overflowFragmentPacket2, + ], + localEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.localIPv4Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv4Address(SwiftNetworkIPTests.remoteIPv4Address)!, port: 0) + ) XCTAssertNil(readBytes, "Unexpectedly received a packet when reassembled length overflows UInt16") } - // Sets up a minimal IPv4 harness to test different fragment and reassembly conditions. - private func processIPv4Fragment( + func testTwoIPv6FragmentsToReassemble() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.twoIPv6FragmentInputPacket1, + SwiftNetworkIPTests.twoIPv6FragmentInputPacket2, + ], + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.localIPv6Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.remoteIPv6Address)!, port: 0) + ) + XCTAssertNotNil(readBytes, "Failed to receive reassembled IPv6 packet from two fragments") + if let readBytes { + XCTAssertEqual( + readBytes, + SwiftNetworkIPTests.inputMessage, + "Reassembled IPv6 payload did not match expected" + ) + } + } + + func testThreeIPv6FragmentsToReassemble() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.threeIPv6FragmentInputPacket1, + SwiftNetworkIPTests.threeIPv6FragmentInputPacket2, + SwiftNetworkIPTests.threeIPv6FragmentInputPacket3, + ], + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.localIPv6Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.remoteIPv6Address)!, port: 0) + ) + XCTAssertNotNil(readBytes, "Failed to receive reassembled IPv6 packet from three fragments") + if let readBytes { + XCTAssertEqual( + readBytes, + SwiftNetworkIPTests.threeFragmentPayload, + "Reassembled IPv6 payload did not match expected" + ) + } + } + + // Verifies that a gap between IPv6 fragment offsets prevents reassembly + // Fragment 1 covers bytes 0–7; fragment 2 starts at byte 16, skipping byte 8–15 + func testGappedIPv6FragmentsProduceNoReassembledFrame() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.gappedIPv6FragmentPacket1, + SwiftNetworkIPTests.gappedIPv6FragmentPacket2, + ], + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.localIPv6Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.remoteIPv6Address)!, port: 0) + ) + XCTAssertNil(readBytes, "Unexpectedly received a packet from a gapped IPv6 fragment sequence") + } + + // Verifies that a sequence with no terminal fragment (all MF=1) is never reassembled + func testAllMFSetIPv6FragmentsProduceNoReassembledFrame() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.allMFSetIPv6FragmentPacket1, + SwiftNetworkIPTests.allMFSetIPv6FragmentPacket2, + ], + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.localIPv6Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.remoteIPv6Address)!, port: 0) + ) + XCTAssertNil(readBytes, "Unexpectedly received a packet when no terminal IPv6 fragment was present") + } + + // Verifies that a fragment sequence whose combined innerLength overflows UInt16 is rejected + func testIPv6FragmentOffsetOverflowProducesNoFrame() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.overflowIPv6FragmentPacket1, + SwiftNetworkIPTests.overflowIPv6FragmentPacket2, + ], + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.localIPv6Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.remoteIPv6Address)!, port: 0) + ) + XCTAssertNil(readBytes, "Unexpectedly received a packet when IPv6 fragment offset overflows UInt16") + } + + // Verifies that three fragments arriving out of order (3, 1, 2) are sorted and reassembled correctly + // Verifies that out-of-order IPv6 fragments (3, 1, 2) are sorted and reassembled correctly + func testOutOfOrderIPv6FragmentsReassemble() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.threeIPv6FragmentInputPacket3, + SwiftNetworkIPTests.threeIPv6FragmentInputPacket1, + SwiftNetworkIPTests.threeIPv6FragmentInputPacket2, + ], + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.localIPv6Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.remoteIPv6Address)!, port: 0) + ) + XCTAssertNotNil(readBytes, "Failed to reassemble out-of-order IPv6 fragments") + if let readBytes { + XCTAssertEqual( + readBytes, + SwiftNetworkIPTests.threeFragmentPayload, + "Reassembled out-of-order IPv6 payload did not match expected" + ) + } + } + + // Verifies that overlapping IPv6 fragments are rejected per RFC 5722 + // Fragment 1 covers bytes 0–15 (MF=1) (More Fragments) + // Fragment 2 starts at byte 8, overlapping the last 8 bytes of fragment 1 + // The fragmentOffset (8) != expectedOffset (16) check drops the sequence + func testOverlappingIPv6FragmentsProduceNoReassembledFrame() { + let readBytes = processIPFragment( + packets: [ + SwiftNetworkIPTests.overlappingIPv6FragmentPacket1, + SwiftNetworkIPTests.overlappingIPv6FragmentPacket2, + ], + localEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.localIPv6Address)!, port: 0), + remoteEndpoint: Endpoint(address: IPv6Address(SwiftNetworkIPTests.remoteIPv6Address)!, port: 0) + ) + XCTAssertNil(readBytes, "Unexpectedly received a packet from overlapping IPv6 fragments (RFC 5722 violation)") + } + + // Sets up a minimal IP harness to test different fragment and reassembly conditions + private func processIPFragment( packets: [[UInt8]], + localEndpoint: Endpoint, + remoteEndpoint: Endpoint, logIDNumber: Int = 2 ) -> [UInt8]? { let parameters = Parameters() @@ -574,8 +930,8 @@ final class SwiftNetworkIPTests: NetTestCase { udpOptions.setLogID(prefix: "C", parent: "1", protocolLogIDNumber: 1) parameters.defaultStack.transport = .udp(udpOptions) - let localEndpoint = Endpoint(address: IPv4Address(SwiftNetworkIPTests.localIPv4Address)!, port: 0) - let remoteEndpoint = Endpoint(address: IPv4Address(SwiftNetworkIPTests.remoteIPv4Address)!, port: 0) + let localEndpoint = localEndpoint + let remoteEndpoint = remoteEndpoint let ipLinkage = OutboundDatagramLinkage(reference: ipInstance) guard