From a2083cb345660bc9c8a30d2c118dea0d71702023 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 13 Sep 2024 10:47:31 +0200 Subject: [PATCH 01/11] WiP: ipv4 options support --- .../com/jasonernst/knet/ip/Ipv4Header.kt | 40 ++++++--- .../com/jasonernst/knet/ip/Ipv6Header.kt | 4 +- .../jasonernst/knet/ip/options/Ipv4Option.kt | 90 +++++++++++++++++++ .../knet/ip/options/Ipv4OptionClassType.kt | 26 ++++++ .../ip/options/Ipv4OptionEndOfOptionList.kt | 8 ++ .../knet/ip/options/Ipv4OptionNoOperation.kt | 8 ++ .../knet/ip/options/Ipv4OptionType.kt | 25 ++++++ .../knet/ip/options/Ipv4OptionUnknown.kt | 20 +++++ .../com/jasonernst/knet/ip/Ipv4HeaderTest.kt | 10 --- .../knet/ip/options/Ipv4OptionTest.kt | 43 +++++++++ 10 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionClassType.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt index cac3356..338e6e8 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt @@ -3,6 +3,8 @@ package com.jasonernst.knet.ip import com.jasonernst.icmp_common.Checksum import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader.Companion.IP4_VERSION +import com.jasonernst.knet.ip.options.Ipv4Option +import com.jasonernst.knet.ip.options.Ipv4Option.Companion.parseOptions import org.slf4j.LoggerFactory import java.net.Inet4Address import java.net.InetAddress @@ -47,6 +49,7 @@ data class Ipv4Header( override val sourceAddress: InetAddress = Inet4Address.getLocalHost(), // 32-bits, destination address override val destinationAddress: InetAddress = Inet4Address.getLocalHost(), + val options: List = emptyList(), ) : IpHeader { // 3-bits: set from mayFragment and lastFragment // bit 0: Reserved; must be zero @@ -62,6 +65,15 @@ data class Ipv4Header( flag = flag or 0x20 } + // dummy check that ihl matches the options length + val optionsLength = options.sumOf { it.size.toInt() }.toUInt() + if (ihl * IP4_WORD_LENGTH != (IP4_MIN_HEADER_LENGTH + optionsLength).toUByte().toUInt()) { + val expectedIHL = ((IP4_MIN_HEADER_LENGTH + optionsLength) / IP4_WORD_LENGTH).toUByte() + throw IllegalArgumentException( + "Invalid IPv4 header. IHL does not match the options length, IHL should be $expectedIHL, but was $ihl because options length was $optionsLength", + ) + } + // calculate the checksum for packet creation based on the set fields if (headerChecksum == 0u.toUShort()) { logger.debug("Calculating checksum for IPv4 header") @@ -69,15 +81,15 @@ data class Ipv4Header( // ^ this will compute the checksum and put it in the buffer // note: it's tempting to call the checksum function here but if we do we'll get a zero // checksum because the field hasn't been zero'd out after the toByteArray call. - headerChecksum = ByteBuffer.wrap(buffer).getShort(10).toUShort() + headerChecksum = ByteBuffer.wrap(buffer).getShort(CHECKSUM_OFFSET).toUShort() } } companion object { private val logger = LoggerFactory.getLogger(Ipv4Header::class.java) + private const val CHECKSUM_OFFSET = 10 const val IP4_WORD_LENGTH: UByte = 4u val IP4_MIN_HEADER_LENGTH: UByte = (IP4_WORD_LENGTH * 5u).toUByte() - const val IP4_MAX_HEADER_LENGTH: UByte = 60u fun fromStream(stream: ByteBuffer): Ipv4Header { val start = stream.position() @@ -99,7 +111,7 @@ data class Ipv4Header( // ensure we have enough capacity in the stream to parse out a full header val ihl: UByte = (versionAndHeaderLength.toInt() and 0x0F).toUByte() val headerAvailable = stream.limit() - start - if (headerAvailable < (ihl * 4u).toInt()) { + if (headerAvailable < (ihl * IP4_WORD_LENGTH).toInt()) { throw PacketTooShortException( "Not enough space in stream for IPv4 header, expected ${ihl * 4u} but only have $headerAvailable", ) @@ -125,13 +137,15 @@ data class Ipv4Header( stream[destination] val destinationAddress = Inet4Address.getByAddress(destination) as Inet4Address - // todo (compscidr): parse the options field instead of just dropping them - logger.debug("POS: ${stream.position()}, remaining: ${stream.remaining()}") - if (ihl > 5u) { - logger.debug("Dropping IP options") - stream.position(stream.position() + ((ihl - 5u) * 4u).toInt()) + // make sure we don't process into a second packet + val limitOfPacket = start + (ihl * IP4_WORD_LENGTH).toInt() + val expectedRemaining = limitOfPacket - start - IP4_MIN_HEADER_LENGTH.toInt() + if (stream.remaining() < expectedRemaining) { + throw PacketTooShortException( + "Not enough data in stream to parse Ipv4 options, expecting $expectedRemaining, have ${stream.remaining()}", + ) } - logger.debug("POS: ${stream.position()}, remaining: ${stream.remaining()}") + val options = parseOptions(stream, limitOfPacket) return Ipv4Header( ihl = ihl, @@ -147,6 +161,7 @@ data class Ipv4Header( headerChecksum = checksum, sourceAddress = sourceAddress, destinationAddress = destinationAddress, + options = options, ) } } @@ -174,11 +189,14 @@ data class Ipv4Header( buffer.putShort(0) // zero-out checksum buffer.put(sourceAddress.address) buffer.put(destinationAddress.address) + for (option in options) { + buffer.put(option.toByteArray()) + } buffer.rewind() - // compute checksum and write over the value + // compute checksum and write over the zero value val ipChecksum = Checksum.calculateChecksum(buffer) - buffer.putShort(10, ipChecksum.toShort()) + buffer.putShort(CHECKSUM_OFFSET, ipChecksum.toShort()) return buffer.array() } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt index 0f80e5f..3bfd7ad 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt @@ -72,7 +72,7 @@ data class Ipv6Header( val destinationBuffer = ByteArray(16) stream[destinationBuffer] val destinationAddress = Inet6Address.getByAddress(destinationBuffer) as Inet6Address - val options = Ipv6ExtensionHeader.fromStream(stream, IpType.fromValue(protocol)) + val extensionHeaders = Ipv6ExtensionHeader.fromStream(stream, IpType.fromValue(protocol)) return Ipv6Header( ipVersion, @@ -83,7 +83,7 @@ data class Ipv6Header( hopLimit, sourceAddress, destinationAddress, - options, + extensionHeaders, ) } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt new file mode 100644 index 0000000..4e6cec3 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt @@ -0,0 +1,90 @@ +package com.jasonernst.knet.ip.options + +import com.jasonernst.knet.PacketTooShortException +import java.nio.ByteBuffer +import java.nio.ByteOrder + +// because kotlin doesn't have a direct conversion function apparently... +// https://stackoverflow.com/questions/46401879/boolean-int-conversion-in-kotlin +fun Boolean.toInt() = if (this) 1 else 0 + +/** + * From RFC791, page 15: + * + * The option field is variable in length. There may be zero or more + * options. There are two cases for the format of an option: + * + * Case 1: A single octet of option-type. + * + * Case 2: An option-type octet, an option-length octet, and the + * actual option-data octets. + * + * The option-length octet counts the option-type octet and the + * option-length octet as well as the option-data octets. + * + * The option-type octet is viewed as having 3 fields: + * + * 1 bit copied flag, + * 2 bits option class, + * 5 bits option number. + * + * The copied flag indicates that this option is copied into all + * fragments on fragmentation. + * + * 0 = not copied + * 1 = copied + */ +abstract class Ipv4Option( + open val isCopied: Boolean = true, + open val optionClass: Ipv4OptionClassType, + open val type: Ipv4OptionType, + open val size: UByte, +) { + companion object { + fun parseOptions( + stream: ByteBuffer, + limit: Int, + ): List { + val options = ArrayList() + while (stream.position() + 1 <= limit) { + val kindOctet = stream.get().toUByte() + // high bit is copied flag + val isCopied = kindOctet.toInt() and 0b10000000 == 0b10000000 + val classByte = kindOctet.toInt() and 0b01100000 shr 5 + val optionClass = Ipv4OptionClassType.fromKind(classByte.toUByte()) + val kind = (kindOctet.toInt() and 0b00011111).toUByte() + if (kind == Ipv4OptionType.EndOfOptionList.kind) { + options.add(Ipv4OptionEndOfOptionList(isCopied, optionClass)) + break + } else if (kind == Ipv4OptionType.NoOperation.kind) { + options.add(Ipv4OptionNoOperation(isCopied, optionClass)) + } else { + if (stream.remaining() < 1) { + throw PacketTooShortException("Can't determine length of ipv4 option because we have no bytes left") + } + // this length includes the previous two bytes + val length = (stream.get().toUByte() - 2u).toUByte() + if (stream.remaining() < length.toInt()) { + throw PacketTooShortException("Can't parse ipv4 option because we don't have enough bytes left for the data") + } + val data = ByteArray(length.toInt()) + stream.get(data) + + val type = + try { + Ipv4OptionType.fromKind(kind) + } catch (e: NoSuchElementException) { + Ipv4OptionType.Unknown + } + options.add(Ipv4OptionUnknown(isCopied, optionClass, type, length, data)) + } + } + return options + } + } + + open fun toByteArray(order: ByteOrder = ByteOrder.BIG_ENDIAN): ByteArray { + val typeInt = isCopied.toInt() shl 7 or (optionClass.kind.toInt() shl 5) or type.kind.toInt() + return byteArrayOf(typeInt.toByte()) + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionClassType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionClassType.kt new file mode 100644 index 0000000..d7af406 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionClassType.kt @@ -0,0 +1,26 @@ +package com.jasonernst.knet.ip.options + +/** + * The option classes are: + * + * 0 = control + * 1 = reserved for future use + * 2 = debugging and measurement + * 3 = reserved for future use + * + * Since we only have 2 bits, any other value makes no sense and should + * rightfully throw an exception when trying to parse it. + */ +enum class Ipv4OptionClassType( + val kind: UByte, +) { + Control(0u), + Reserved1(1u), + DebuggingAndMeasurement(2u), + Reserved2(3u), + ; + + companion object { + fun fromKind(kind: UByte) = entries.first { it.kind == kind } + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt new file mode 100644 index 0000000..e45d328 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt @@ -0,0 +1,8 @@ +package com.jasonernst.knet.ip.options + +data class Ipv4OptionEndOfOptionList( + override val isCopied: Boolean, + override val optionClass: Ipv4OptionClassType, + override val type: Ipv4OptionType = Ipv4OptionType.EndOfOptionList, + override val size: UByte = 1u, +) : Ipv4Option(isCopied, optionClass, type, size) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt new file mode 100644 index 0000000..f9ea05c --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt @@ -0,0 +1,8 @@ +package com.jasonernst.knet.ip.options + +data class Ipv4OptionNoOperation( + override val isCopied: Boolean, + override val optionClass: Ipv4OptionClassType, + override val type: Ipv4OptionType = Ipv4OptionType.NoOperation, + override val size: UByte = 1u, +) : Ipv4Option(isCopied, optionClass, type, size) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt new file mode 100644 index 0000000..916ad3d --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt @@ -0,0 +1,25 @@ +package com.jasonernst.knet.ip.options + +/** + * https://datatracker.ietf.org/doc/html/rfc791 page 15 + */ +enum class Ipv4OptionType( + val kind: UByte, +) { + EndOfOptionList(0u), + NoOperation(1u), + Security(2u), + LooseSourceRouting(3u), + StrictSourceRouting(9u), + RecordRoute(7u), + StreamId(8u), + TimeStamp(4u), + + // fake type we defined for when we don't have the type in the enum + Unknown(99u), + ; + + companion object { + fun fromKind(kind: UByte) = entries.first { it.kind == kind } + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt new file mode 100644 index 0000000..e8ae6ce --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt @@ -0,0 +1,20 @@ +package com.jasonernst.knet.ip.options + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +data class Ipv4OptionUnknown( + override val isCopied: Boolean, + override val optionClass: Ipv4OptionClassType, + override val type: Ipv4OptionType, + override val size: UByte, + val data: ByteArray, +) : Ipv4Option(isCopied, optionClass, type, size) { + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(2 + data.size) + buffer.put(super.toByteArray(order)) // get the type byte sorted out + buffer.put(size.toByte()) + buffer.put(data) + return buffer.array() + } +} diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt index 020b419..da36fa2 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt @@ -304,14 +304,4 @@ class Ipv4HeaderTest { Ipv4Header.fromStream(buffer) } } - - // this is only a temp test until we properly implement ipv4 options - @Test fun testOptionDropping() { - val ipv4Packet = Ipv4Header(ihl = 6u) - logger.debug("IPv4 packet: {}", ipv4Packet) - val buffer = ByteBuffer.wrap(ipv4Packet.toByteArray()) - val parsedPacket = IpHeader.fromStream(buffer) - assertEquals(0u, buffer.remaining().toUInt()) - assertEquals(ipv4Packet, parsedPacket) - } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt new file mode 100644 index 0000000..c2db87a --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt @@ -0,0 +1,43 @@ +package com.jasonernst.knet.ip.options + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer + +class Ipv4OptionTest { + private val logger = LoggerFactory.getLogger(javaClass) + + @Test fun parseOptions() { + val options = + arrayListOf( + Ipv4OptionNoOperation(true, Ipv4OptionClassType.Control), + Ipv4OptionEndOfOptionList(true, Ipv4OptionClassType.Control), + ) + val optionSize = options.sumOf { it.size.toInt() } + val stream = ByteBuffer.allocate(optionSize) + for (option in options) { + stream.put(option.toByteArray()) + } + stream.rewind() + val parsedOptions = Ipv4Option.parseOptions(stream, optionSize) + assertEquals(options, parsedOptions) + } + + @Test fun unknownOption() { + // unhandled option, but in list + val stream = ByteBuffer.wrap(byteArrayOf(0xFE.toByte(), 0x04, 0x00, 0x00)) + val parsedOptions = Ipv4Option.parseOptions(stream, 4) + assertTrue(parsedOptions[0] is Ipv4OptionUnknown) + + // unhandled option, not in list + val stream2 = ByteBuffer.wrap(byteArrayOf(0x02.toByte(), 0x04, 0x00, 0x00)) + val parsedOptions2 = Ipv4Option.parseOptions(stream2, 4) + assertTrue(parsedOptions2[0] is Ipv4OptionUnknown) + } + + @Test fun unknownOptionTooShort() { + // wip + } +} From c1a89ed6f1cfa6716d52e43ffd15537f1a718644 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 13 Sep 2024 15:02:35 +0200 Subject: [PATCH 02/11] Added basic ivp4 option handling --- .../jasonernst/knet/ip/options/Ipv4Option.kt | 24 +++-- .../knet/ip/options/Ipv4OptionType.kt | 2 +- .../knet/ip/options/Ipv4OptionUnknown.kt | 40 +++++++-- .../knet/ip/options/Ipv4OptionTest.kt | 90 ++++++++++++++++++- 4 files changed, 139 insertions(+), 17 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt index 4e6cec3..0767d40 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt @@ -1,6 +1,7 @@ package com.jasonernst.knet.ip.options import com.jasonernst.knet.PacketTooShortException +import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder @@ -41,16 +42,18 @@ abstract class Ipv4Option( open val size: UByte, ) { companion object { + private val logger = LoggerFactory.getLogger(Ipv4Option::class.java) + fun parseOptions( stream: ByteBuffer, - limit: Int, + limit: Int = stream.limit(), ): List { val options = ArrayList() while (stream.position() + 1 <= limit) { val kindOctet = stream.get().toUByte() // high bit is copied flag val isCopied = kindOctet.toInt() and 0b10000000 == 0b10000000 - val classByte = kindOctet.toInt() and 0b01100000 shr 5 + val classByte = (kindOctet.toInt() and 0b01100000) shr 5 val optionClass = Ipv4OptionClassType.fromKind(classByte.toUByte()) val kind = (kindOctet.toInt() and 0b00011111).toUByte() if (kind == Ipv4OptionType.EndOfOptionList.kind) { @@ -62,12 +65,14 @@ abstract class Ipv4Option( if (stream.remaining() < 1) { throw PacketTooShortException("Can't determine length of ipv4 option because we have no bytes left") } - // this length includes the previous two bytes - val length = (stream.get().toUByte() - 2u).toUByte() - if (stream.remaining() < length.toInt()) { + // this length includes the previous two bytes which is why we need adjustment + // we don't apply it directly to length because we want to construct the option + // with the correct length which includes the first two fields + val length = (stream.get().toUByte()) + if (stream.remaining() < length.toInt() - 2) { throw PacketTooShortException("Can't parse ipv4 option because we don't have enough bytes left for the data") } - val data = ByteArray(length.toInt()) + val data = ByteArray(length.toInt() - 2) stream.get(data) val type = @@ -84,7 +89,10 @@ abstract class Ipv4Option( } open fun toByteArray(order: ByteOrder = ByteOrder.BIG_ENDIAN): ByteArray { - val typeInt = isCopied.toInt() shl 7 or (optionClass.kind.toInt() shl 5) or type.kind.toInt() - return byteArrayOf(typeInt.toByte()) + val copiedInt = (isCopied.toInt() shl 7) + val classInt = optionClass.kind.toInt() shl 5 + val typeInt = type.kind.toInt() + val typeByte = (copiedInt + classInt + typeInt).toByte() + return byteArrayOf(typeByte) } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt index 916ad3d..5b041b6 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt @@ -16,7 +16,7 @@ enum class Ipv4OptionType( TimeStamp(4u), // fake type we defined for when we don't have the type in the enum - Unknown(99u), + Unknown(31u), // max value since this is 5 bits ; companion object { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt index e8ae6ce..f80dfe5 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt @@ -4,17 +4,45 @@ import java.nio.ByteBuffer import java.nio.ByteOrder data class Ipv4OptionUnknown( - override val isCopied: Boolean, - override val optionClass: Ipv4OptionClassType, - override val type: Ipv4OptionType, - override val size: UByte, - val data: ByteArray, + override val isCopied: Boolean = true, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, + override val type: Ipv4OptionType = Ipv4OptionType.Unknown, + override val size: UByte = MINIMUM_SIZE, + val data: ByteArray = ByteArray(0), ) : Ipv4Option(isCopied, optionClass, type, size) { + companion object { + val MINIMUM_SIZE: UByte = 2u // 1 byte for type, 1 byte for size + } + override fun toByteArray(order: ByteOrder): ByteArray { - val buffer = ByteBuffer.allocate(2 + data.size) + val buffer = ByteBuffer.allocate(MINIMUM_SIZE.toInt() + data.size) buffer.put(super.toByteArray(order)) // get the type byte sorted out buffer.put(size.toByte()) buffer.put(data) return buffer.array() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Ipv4OptionUnknown + + if (isCopied != other.isCopied) return false + if (optionClass != other.optionClass) return false + if (type != other.type) return false + if (size != other.size) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = isCopied.hashCode() + result = 31 * result + optionClass.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + size.hashCode() + result = 31 * result + data.contentHashCode() + return result + } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt index c2db87a..bc76673 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt @@ -1,8 +1,11 @@ package com.jasonernst.knet.ip.options +import com.jasonernst.knet.PacketTooShortException import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.slf4j.LoggerFactory import java.nio.ByteBuffer @@ -25,6 +28,18 @@ class Ipv4OptionTest { assertEquals(options, parsedOptions) } + @Test fun copiedClassTypeTest() { + val option = + Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.StreamId) + val typeByte = option.toByteArray()[0] + val copied = typeByte.toInt() and 0b10000000 == 0b10000000 + assertTrue(copied) + val classByte = (typeByte.toInt() and 0b01100000 shr 5).toUByte() + assertEquals(Ipv4OptionClassType.DebuggingAndMeasurement.kind, classByte) + val kind = (typeByte.toInt() and 0b00011111).toUByte() + assertEquals(Ipv4OptionType.StreamId.kind, kind) + } + @Test fun unknownOption() { // unhandled option, but in list val stream = ByteBuffer.wrap(byteArrayOf(0xFE.toByte(), 0x04, 0x00, 0x00)) @@ -35,9 +50,80 @@ class Ipv4OptionTest { val stream2 = ByteBuffer.wrap(byteArrayOf(0x02.toByte(), 0x04, 0x00, 0x00)) val parsedOptions2 = Ipv4Option.parseOptions(stream2, 4) assertTrue(parsedOptions2[0] is Ipv4OptionUnknown) + + // unknown option good path (failing for some reason) + val unhandledOption = + Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.Unknown) + val stream3 = ByteBuffer.wrap(unhandledOption.toByteArray()) + val parsedOptions3 = Ipv4Option.parseOptions(stream3) + assertEquals(1, parsedOptions3.size) + assertEquals(unhandledOption, parsedOptions3[0]) + } + + @Test fun unknownOptionTooShortLength() { + val stream = ByteBuffer.wrap(byteArrayOf(0xFE.toByte())) + assertThrows { + Ipv4Option.parseOptions(stream) + } + } + + @Test fun unknownOptionTooShortLengthOk() { + val stream = ByteBuffer.wrap(byteArrayOf(0xFE.toByte(), 0x04, 0x00)) + assertThrows { + Ipv4Option.parseOptions(stream, 3) + } + } + + @Test fun unknownOptionEquals() { + val option1 = + Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.Unknown) + val option2 = + Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.Unknown) + assertEquals(option1, option2) + + val option3 = Ipv4OptionEndOfOptionList(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement) + assertNotEquals(option1, option3) + + val option4 = + Ipv4OptionUnknown(isCopied = false, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.Unknown) + assertNotEquals(option1, option4) + + val option5 = Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.Control, type = Ipv4OptionType.Unknown) + assertNotEquals(option1, option5) + + val option6 = + Ipv4OptionUnknown( + isCopied = true, + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + type = Ipv4OptionType.EndOfOptionList, + ) + assertNotEquals(option1, option6) + + val option7 = + Ipv4OptionUnknown( + isCopied = true, + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + type = Ipv4OptionType.Unknown, + size = 10u, + ) + assertNotEquals(option1, option7) + + val option8 = + Ipv4OptionUnknown( + isCopied = true, + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + type = Ipv4OptionType.Unknown, + size = 10u, + data = byteArrayOf(0x00, 0x01, 0x02), + ) + assertNotEquals(option7, option8) } - @Test fun unknownOptionTooShort() { - // wip + @Test fun unknownOptionHashCodeTest() { + val map: MutableMap = mutableMapOf() + val option1 = + Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.Unknown) + map[option1] = "test" + assertTrue(map.containsKey(option1)) } } From dc88ebc9c454ca11f90aab5b4e72240a6268285f Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 13 Sep 2024 16:07:34 +0200 Subject: [PATCH 03/11] WiP: added a couple more ipv4 option types --- .../jasonernst/knet/ip/options/Ipv4Option.kt | 2 + .../ip/options/Ipv4OptionEndOfOptionList.kt | 18 ++++- .../Ipv4OptionLooseSourceAndRecordRoute.kt | 66 ++++++++++++++++ .../knet/ip/options/Ipv4OptionNoOperation.kt | 13 +++- .../knet/ip/options/Ipv4OptionSecurity.kt | 76 +++++++++++++++++++ .../knet/ip/options/Ipv4OptionSecurityType.kt | 29 +++++++ 6 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurityType.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt index 0767d40..82ad0d2 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt @@ -61,6 +61,8 @@ abstract class Ipv4Option( break } else if (kind == Ipv4OptionType.NoOperation.kind) { options.add(Ipv4OptionNoOperation(isCopied, optionClass)) + } else if (kind == Ipv4OptionType.Security.kind) { + options.add(Ipv4OptionSecurity.fromStream(stream)) } else { if (stream.remaining() < 1) { throw PacketTooShortException("Can't determine length of ipv4 option because we have no bytes left") diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt index e45d328..18a294d 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt @@ -1,8 +1,22 @@ package com.jasonernst.knet.ip.options +/** + * + * From RFC 791: + * + * This option indicates the end of the option list. This might + * not coincide with the end of the internet header according to + * the internet header length. This is used at the end of all + * options, not the end of each option, and need only be used if + * the end of the options would not otherwise coincide with the end + * of the internet header. + * + * May be copied, introduced, or deleted on fragmentation, or for + * any other reason. + */ data class Ipv4OptionEndOfOptionList( - override val isCopied: Boolean, - override val optionClass: Ipv4OptionClassType, + override val isCopied: Boolean = false, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, override val type: Ipv4OptionType = Ipv4OptionType.EndOfOptionList, override val size: UByte = 1u, ) : Ipv4Option(isCopied, optionClass, type, size) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt new file mode 100644 index 0000000..a495292 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt @@ -0,0 +1,66 @@ +package com.jasonernst.knet.ip.options + +import com.jasonernst.knet.PacketTooShortException +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * From RFC 791: + * + * The option begins with the option type code. The second octet + * is the option length which includes the option type code and the + * length octet, the pointer octet, and length-3 octets of route + * data. The third octet is the pointer into the route data + * indicating the octet which begins the next source address to be + * processed. The pointer is relative to this option, and the + * smallest legal value for the pointer is 4. + * + * A route data is composed of a series of internet addresses. + * Each internet address is 32 bits or 4 octets. If the pointer is + * greater than the length, the source route is empty (and the + * recorded route full) and the routing is to be based on the + * destination address field. + */ +class Ipv4OptionLooseSourceAndRecordRoute( + override val isCopied: Boolean = true, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, + override val type: Ipv4OptionType = Ipv4OptionType.LooseSourceRouting, + override val size: UByte = MIN_OPTION_SIZE, + val pointer: UByte, + val routeData: ByteArray = ByteArray(0), +) : Ipv4Option(isCopied, optionClass, type, size) { + companion object { + val MIN_OPTION_SIZE: UByte = 3u + + fun fromStream( + stream: ByteBuffer, + dataLength: Int, + ): Ipv4OptionLooseSourceAndRecordRoute { + if (stream.remaining() < MIN_OPTION_SIZE.toInt() - 2) { + throw PacketTooShortException( + "Stream must have at least ${MIN_OPTION_SIZE - 2u} " + + "remaining bytes remaining to parse Ipv4OptionLooseSourceAndRecordRoute, we only have " + + "${stream.remaining()} bytes", + ) + } + val pointer = stream.get().toUByte() + val routingData = ByteArray(dataLength) + stream.get(routingData) + return Ipv4OptionLooseSourceAndRecordRoute( + pointer = pointer, + routeData = routingData, + ) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = + ByteBuffer + .allocate(MIN_OPTION_SIZE.toInt() + routeData.size) + .order(order) + .put(super.toByteArray(order)) + .put(pointer.toByte()) + .put(routeData) + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt index f9ea05c..ce45d1b 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt @@ -1,8 +1,17 @@ package com.jasonernst.knet.ip.options +/** + * From RFC 791: + * + * This option may be used between options, for example, to align + * the beginning of a subsequent option on a 32 bit boundary. + * + * May be copied, introduced, or deleted on fragmentation, or for + * any other reason. + */ data class Ipv4OptionNoOperation( - override val isCopied: Boolean, - override val optionClass: Ipv4OptionClassType, + override val isCopied: Boolean = false, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, override val type: Ipv4OptionType = Ipv4OptionType.NoOperation, override val size: UByte = 1u, ) : Ipv4Option(isCopied, optionClass, type, size) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt new file mode 100644 index 0000000..e9e4e3d --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt @@ -0,0 +1,76 @@ +package com.jasonernst.knet.ip.options + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * From RFC 791: + * - must be copied + * - appears at most once in a datagram + * + * Security: + * Specifies one of 16 levels of security (eight of which are + * reserved for future use). + * + * Compartments: + * An all zero value is used when the information transmitted is + * not compartmented. Other values for the compartments field + * may be obtained from the Defense Intelligence Agency. + * + * Handling Restrictions: + * The values for the control and release markings are + * alphanumeric digraphs and are defined in the Defense + * Intelligence Agency Manual DIAM 65-19, "Standard Security + * Markings". + * + * Provides a means to segregate traffic and define controlled + * communities of interest among subscribers. The TCC values are + * trigraphs, and are available from HQ DCA Code 530. + */ +data class Ipv4OptionSecurity( + override val isCopied: Boolean = true, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, + override val type: Ipv4OptionType = Ipv4OptionType.Security, + override val size: UByte = OPTION_SIZE, + val security: Ipv4OptionSecurityType = Ipv4OptionSecurityType.Unclassified, + val compartment: UShort = 0u, + val handlingRestrictions: UShort = 0u, + val tcc: UInt = 0u, // even though we 32 bits, this field is actually 24...whyyyyy. +) : Ipv4Option(isCopied, optionClass, type, size) { + companion object { + val OPTION_SIZE: UByte = 11u + + fun fromStream(stream: ByteBuffer): Ipv4OptionSecurity { + if (stream.remaining() < OPTION_SIZE.toInt() - 2) { + throw IllegalArgumentException( + "Stream must have at least ${OPTION_SIZE - 2u} " + + "remaining bytes remaining to parse Ipv4OptionSecurity, we only have " + + "${stream.remaining()} bytes", + ) + } + val security = Ipv4OptionSecurityType.fromKind(stream.getShort().toUShort()) + val compartment = stream.getShort().toUShort() + val handlingRestrictions = stream.getShort().toUShort() + val tccHighByte = stream.get().toUInt() shl 16 + val tccLowWord = stream.getShort().toUInt() + val tcc = tccHighByte.toInt() or tccLowWord.toInt() + return Ipv4OptionSecurity( + security = security, + compartment = compartment, + handlingRestrictions = handlingRestrictions, + tcc = tcc.toUInt(), + ) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(size.toInt()) + buffer.put(super.toByteArray(order)) + buffer.putShort(security.kind.toShort()) + buffer.putShort(compartment.toShort()) + buffer.putShort(handlingRestrictions.toShort()) + buffer.put((tcc shr 16).toByte()) + buffer.putShort((tcc and 0xFFFFu).toShort()) + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurityType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurityType.kt new file mode 100644 index 0000000..01f176d --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurityType.kt @@ -0,0 +1,29 @@ +package com.jasonernst.knet.ip.options + +enum class Ipv4OptionSecurityType( + val kind: UShort, +) { + Unclassified(0u), + Confidential(0b1111000100110101u), + EFTO(0b0111100010011010u), + MMMM(0b1011110001001101u), + PROG(0b0101111000100110u), + Restricted(0b1010111100010011u), + Secret(0b1101011110001000u), + TopSecret(0b0110101111000101u), + Reserved(0b0011010111100010u), + Reserved2(0b1001101011110001u), + Reserved3(0b0100110101111000u), + Reserved4(0b0010010010111101u), + Reserved5(0b0001001101011110u), + Reserved6(0b1000100110101111u), + Reserved7(0b1100010011010110u), + Reserved8(0b1110001001101011u), + ; + + companion object { + fun fromKind(kind: UShort): Ipv4OptionSecurityType = + entries.find { it.kind == kind } + ?: throw IllegalArgumentException("Unknown Ipv4OptionSecurityType kind: $kind") + } +} From 33bfde60f58a2a14c771486be62b075b317fc555 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 17 Sep 2024 12:37:59 +0200 Subject: [PATCH 04/11] fix failing test --- .../kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt index bc76673..03b43db 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt @@ -47,11 +47,11 @@ class Ipv4OptionTest { assertTrue(parsedOptions[0] is Ipv4OptionUnknown) // unhandled option, not in list - val stream2 = ByteBuffer.wrap(byteArrayOf(0x02.toByte(), 0x04, 0x00, 0x00)) + val stream2 = ByteBuffer.wrap(byteArrayOf(0x11.toByte(), 0x04, 0x00, 0x00)) val parsedOptions2 = Ipv4Option.parseOptions(stream2, 4) assertTrue(parsedOptions2[0] is Ipv4OptionUnknown) - // unknown option good path (failing for some reason) + // unknown option good path val unhandledOption = Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.Unknown) val stream3 = ByteBuffer.wrap(unhandledOption.toByteArray()) From d008853f90a4862835a3fb0be8980d0cf4003070 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 17 Sep 2024 12:46:46 +0200 Subject: [PATCH 05/11] Added more tests --- .../tcp/options/TcpOptionUnsupported.kt | 2 +- .../transport/tcp/options/TcpOptionTests.kt | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionUnsupported.kt b/knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionUnsupported.kt index fd21ca3..75c4a2f 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionUnsupported.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionUnsupported.kt @@ -46,7 +46,7 @@ data class TcpOptionUnsupported( val kindString = try { TcpOptionTypeSupported.fromKind(kind).toString() - } catch (e: IllegalArgumentException) { + } catch (e: NoSuchElementException) { "Unknown" } return "TCPOptionUnsupported($kindString, kind=$kind, size=$size, data=${data.contentToString()})" diff --git a/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt b/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt index 8100b70..f5a5ebb 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt @@ -14,6 +14,7 @@ import com.jasonernst.knet.transport.tcp.options.TcpOptionTimestamp import com.jasonernst.knet.transport.tcp.options.TcpOptionUnsupported import com.jasonernst.packetdumper.stringdumper.StringPacketDumper import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue @@ -317,4 +318,34 @@ class TcpOptionTests { val tcpHeader2 = TcpHeader(options = arrayListOf(timestamp)) assertNotNull(TcpOptionTimestamp.maybeTimestamp(tcpHeader2)) } + + @Test + fun tcpOptionUnsupportedEquals() { + val option1 = TcpOptionUnsupported(88u, ByteArray(0)) + val option2 = TcpOptionUnsupported(88u, ByteArray(0)) + assertEquals(option1, option2) + + val option3 = TcpOptionUnsupported(88u, ByteArray(1)) + assertNotEquals(option1, option3) + + val option4 = TcpOptionNoOperation() + assertNotEquals(option1, option4) + } + + @Test + fun tcpOptionUnsupportedHashCode() { + val map: MutableMap = mutableMapOf() + val option = TcpOptionUnsupported(88u, ByteArray(0)) + map[option] = "test" + assertTrue(map.containsKey(option)) + } + + @Test + fun tcpOptionUnsupportedToString() { + val option = TcpOptionUnsupported(88u, ByteArray(0)) + assertEquals("TCPOptionUnsupported(Unknown, kind=88, size=2, data=[])", option.toString()) + + val option2 = TcpOptionUnsupported(1u, ByteArray(0)) + assertEquals("TCPOptionUnsupported(NoOperation, kind=1, size=2, data=[])", option2.toString()) + } } From 14b2c2b8a0215a0b510548a06493a30367c7051b Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 17 Sep 2024 14:54:13 +0200 Subject: [PATCH 06/11] more tests --- .../jasonernst/knet/ip/options/Ipv4Option.kt | 43 ++++++++++++------- .../knet/ip/options/Ipv4OptionSecurity.kt | 17 +++++++- .../knet/ip/options/Ipv4OptionUnknown.kt | 1 - .../knet/ip/options/Ipv4OptionTest.kt | 41 ++++++++++++++++++ 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt index 82ad0d2..04cfa50 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt @@ -61,8 +61,6 @@ abstract class Ipv4Option( break } else if (kind == Ipv4OptionType.NoOperation.kind) { options.add(Ipv4OptionNoOperation(isCopied, optionClass)) - } else if (kind == Ipv4OptionType.Security.kind) { - options.add(Ipv4OptionSecurity.fromStream(stream)) } else { if (stream.remaining() < 1) { throw PacketTooShortException("Can't determine length of ipv4 option because we have no bytes left") @@ -70,20 +68,24 @@ abstract class Ipv4Option( // this length includes the previous two bytes which is why we need adjustment // we don't apply it directly to length because we want to construct the option // with the correct length which includes the first two fields - val length = (stream.get().toUByte()) - if (stream.remaining() < length.toInt() - 2) { - throw PacketTooShortException("Can't parse ipv4 option because we don't have enough bytes left for the data") - } - val data = ByteArray(length.toInt() - 2) - stream.get(data) - - val type = - try { - Ipv4OptionType.fromKind(kind) - } catch (e: NoSuchElementException) { - Ipv4OptionType.Unknown + val length = stream.get().toUByte() + if (kind == Ipv4OptionType.Security.kind) { + options.add(Ipv4OptionSecurity.fromStream(stream, isCopied, optionClass, length)) + } else { + if (stream.remaining() < length.toInt() - 2) { + throw PacketTooShortException("Can't parse ipv4 option because we don't have enough bytes left for the data") } - options.add(Ipv4OptionUnknown(isCopied, optionClass, type, length, data)) + val data = ByteArray(length.toInt() - 2) + stream.get(data) + + val type = + try { + Ipv4OptionType.fromKind(kind) + } catch (e: NoSuchElementException) { + Ipv4OptionType.Unknown + } + options.add(Ipv4OptionUnknown(isCopied, optionClass, type, length, data)) + } } } return options @@ -91,10 +93,19 @@ abstract class Ipv4Option( } open fun toByteArray(order: ByteOrder = ByteOrder.BIG_ENDIAN): ByteArray { + if (size.toInt() < 1) { + throw IllegalArgumentException("Size must be at least 1") + } val copiedInt = (isCopied.toInt() shl 7) val classInt = optionClass.kind.toInt() shl 5 val typeInt = type.kind.toInt() val typeByte = (copiedInt + classInt + typeInt).toByte() - return byteArrayOf(typeByte) + if (size.toInt() == 1) { + return byteArrayOf(typeByte) + } + val buffer = ByteBuffer.allocate(2) + buffer.put(typeByte) + buffer.put(size.toByte()) + return buffer.array() } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt index e9e4e3d..9aec57d 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt @@ -1,5 +1,7 @@ package com.jasonernst.knet.ip.options +import com.jasonernst.knet.PacketTooShortException +import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder @@ -38,11 +40,17 @@ data class Ipv4OptionSecurity( val tcc: UInt = 0u, // even though we 32 bits, this field is actually 24...whyyyyy. ) : Ipv4Option(isCopied, optionClass, type, size) { companion object { + private val logger = LoggerFactory.getLogger(javaClass) val OPTION_SIZE: UByte = 11u - fun fromStream(stream: ByteBuffer): Ipv4OptionSecurity { + fun fromStream( + stream: ByteBuffer, + isCopied: Boolean, + optionClass: Ipv4OptionClassType, + size: UByte, + ): Ipv4OptionSecurity { if (stream.remaining() < OPTION_SIZE.toInt() - 2) { - throw IllegalArgumentException( + throw PacketTooShortException( "Stream must have at least ${OPTION_SIZE - 2u} " + "remaining bytes remaining to parse Ipv4OptionSecurity, we only have " + "${stream.remaining()} bytes", @@ -54,7 +62,11 @@ data class Ipv4OptionSecurity( val tccHighByte = stream.get().toUInt() shl 16 val tccLowWord = stream.getShort().toUInt() val tcc = tccHighByte.toInt() or tccLowWord.toInt() + logger.debug("Stream position: ${stream.position()} remaining: ${stream.remaining()}") return Ipv4OptionSecurity( + isCopied = isCopied, + optionClass = optionClass, + size = size, security = security, compartment = compartment, handlingRestrictions = handlingRestrictions, @@ -64,6 +76,7 @@ data class Ipv4OptionSecurity( } override fun toByteArray(order: ByteOrder): ByteArray { + logger.debug("SIZE: $size") val buffer = ByteBuffer.allocate(size.toInt()) buffer.put(super.toByteArray(order)) buffer.putShort(security.kind.toShort()) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt index f80dfe5..7abd367 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt @@ -17,7 +17,6 @@ data class Ipv4OptionUnknown( override fun toByteArray(order: ByteOrder): ByteArray { val buffer = ByteBuffer.allocate(MINIMUM_SIZE.toInt() + data.size) buffer.put(super.toByteArray(order)) // get the type byte sorted out - buffer.put(size.toByte()) buffer.put(data) return buffer.array() } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt index 03b43db..f16d54c 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt @@ -65,6 +65,11 @@ class Ipv4OptionTest { assertThrows { Ipv4Option.parseOptions(stream) } + + val option = Ipv4OptionUnknown(size = 0u) + assertThrows { + option.toByteArray() + } } @Test fun unknownOptionTooShortLengthOk() { @@ -126,4 +131,40 @@ class Ipv4OptionTest { map[option1] = "test" assertTrue(map.containsKey(option1)) } + + @Test fun ipv4OptionSecurity() { + val option = + Ipv4OptionSecurity( + isCopied = true, + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + type = Ipv4OptionType.Security, + security = Ipv4OptionSecurityType.Confidential, + compartment = 1234u, + handlingRestrictions = 5678u, + tcc = 9102u, + ) + val stream = ByteBuffer.wrap(option.toByteArray()) + logger.debug("Stream size: ${stream.limit()}") + val parsedOptions = Ipv4Option.parseOptions(stream) + assertEquals(1, parsedOptions.size) + assertEquals(option, parsedOptions[0]) + } + + @Test fun ipv4OptionSecurityTooShort() { + val option = + Ipv4OptionSecurity( + isCopied = true, + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + type = Ipv4OptionType.Security, + security = Ipv4OptionSecurityType.Confidential, + compartment = 1234u, + handlingRestrictions = 5678u, + tcc = 9102u, + ) + val stream = ByteBuffer.wrap(option.toByteArray()) + stream.limit(stream.limit() - 1) + assertThrows { + Ipv4Option.parseOptions(stream) + } + } } From b12ab647db6ff0e7deacd76575c9367da0896707 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 17 Sep 2024 15:24:12 +0200 Subject: [PATCH 07/11] Added more ipv4 option tests --- .../jasonernst/knet/ip/options/Ipv4Option.kt | 34 +++++++++------ .../Ipv4OptionLooseSourceAndRecordRoute.kt | 43 ++++++++++++++++--- .../knet/ip/options/Ipv4OptionSecurity.kt | 1 + .../knet/ip/options/Ipv4OptionTest.kt | 39 +++++++++++++++++ 4 files changed, 97 insertions(+), 20 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt index 04cfa50..2bd9bea 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt @@ -69,22 +69,28 @@ abstract class Ipv4Option( // we don't apply it directly to length because we want to construct the option // with the correct length which includes the first two fields val length = stream.get().toUByte() - if (kind == Ipv4OptionType.Security.kind) { - options.add(Ipv4OptionSecurity.fromStream(stream, isCopied, optionClass, length)) - } else { - if (stream.remaining() < length.toInt() - 2) { - throw PacketTooShortException("Can't parse ipv4 option because we don't have enough bytes left for the data") + if (stream.remaining() < length.toInt() - 2) { + throw PacketTooShortException("Can't parse ipv4 option because we don't have enough bytes left for the data") + } + when (kind) { + Ipv4OptionType.Security.kind -> { + options.add(Ipv4OptionSecurity.fromStream(stream, isCopied, optionClass, length)) + } + Ipv4OptionType.LooseSourceRouting.kind -> { + options.add(Ipv4OptionLooseSourceAndRecordRoute.fromStream(stream, isCopied, optionClass, length)) } - val data = ByteArray(length.toInt() - 2) - stream.get(data) + else -> { + val data = ByteArray(length.toInt() - 2) + stream.get(data) - val type = - try { - Ipv4OptionType.fromKind(kind) - } catch (e: NoSuchElementException) { - Ipv4OptionType.Unknown - } - options.add(Ipv4OptionUnknown(isCopied, optionClass, type, length, data)) + val type = + try { + Ipv4OptionType.fromKind(kind) + } catch (e: NoSuchElementException) { + Ipv4OptionType.Unknown + } + options.add(Ipv4OptionUnknown(isCopied, optionClass, type, length, data)) + } } } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt index a495292..f0382c1 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt @@ -1,6 +1,7 @@ package com.jasonernst.knet.ip.options import com.jasonernst.knet.PacketTooShortException +import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder @@ -21,32 +22,38 @@ import java.nio.ByteOrder * recorded route full) and the routing is to be based on the * destination address field. */ -class Ipv4OptionLooseSourceAndRecordRoute( +data class Ipv4OptionLooseSourceAndRecordRoute( override val isCopied: Boolean = true, override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, override val type: Ipv4OptionType = Ipv4OptionType.LooseSourceRouting, - override val size: UByte = MIN_OPTION_SIZE, val pointer: UByte, val routeData: ByteArray = ByteArray(0), -) : Ipv4Option(isCopied, optionClass, type, size) { +) : Ipv4Option(isCopied, optionClass, type, size = (routeData.size.toUByte() + MIN_OPTION_SIZE).toUByte()) { companion object { val MIN_OPTION_SIZE: UByte = 3u + private val logger = LoggerFactory.getLogger(Ipv4OptionLooseSourceAndRecordRoute::class.java) fun fromStream( stream: ByteBuffer, - dataLength: Int, + isCopied: Boolean, + optionClass: Ipv4OptionClassType, + size: UByte, ): Ipv4OptionLooseSourceAndRecordRoute { - if (stream.remaining() < MIN_OPTION_SIZE.toInt() - 2) { + logger.debug("SIZE: $size, remaining: ${stream.remaining()}") + if (stream.remaining() < (size - 2u).toInt()) { throw PacketTooShortException( - "Stream must have at least ${MIN_OPTION_SIZE - 2u} " + + "Stream must have at least ${size - 2u} " + "remaining bytes remaining to parse Ipv4OptionLooseSourceAndRecordRoute, we only have " + "${stream.remaining()} bytes", ) } val pointer = stream.get().toUByte() + val dataLength = size.toInt() - MIN_OPTION_SIZE.toInt() val routingData = ByteArray(dataLength) stream.get(routingData) return Ipv4OptionLooseSourceAndRecordRoute( + isCopied = isCopied, + optionClass = optionClass, pointer = pointer, routeData = routingData, ) @@ -63,4 +70,28 @@ class Ipv4OptionLooseSourceAndRecordRoute( .put(routeData) return buffer.array() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Ipv4OptionLooseSourceAndRecordRoute + + if (isCopied != other.isCopied) return false + if (optionClass != other.optionClass) return false + if (type != other.type) return false + if (pointer != other.pointer) return false + if (!routeData.contentEquals(other.routeData)) return false + + return true + } + + override fun hashCode(): Int { + var result = isCopied.hashCode() + result = 31 * result + optionClass.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + pointer.hashCode() + result = 31 * result + routeData.contentHashCode() + return result + } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt index 9aec57d..52ebfd6 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt @@ -78,6 +78,7 @@ data class Ipv4OptionSecurity( override fun toByteArray(order: ByteOrder): ByteArray { logger.debug("SIZE: $size") val buffer = ByteBuffer.allocate(size.toInt()) + buffer.order(order) buffer.put(super.toByteArray(order)) buffer.putShort(security.kind.toShort()) buffer.putShort(compartment.toShort()) diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt index f16d54c..cb6642c 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt @@ -167,4 +167,43 @@ class Ipv4OptionTest { Ipv4Option.parseOptions(stream) } } + + @Test + fun ipv4OptionLooseSourceAndRecordRoute() { + val option = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val stream = ByteBuffer.wrap(option.toByteArray()) + logger.debug("Stream length: ${stream.limit()}") + val parsedOptions = Ipv4Option.parseOptions(stream) + assertEquals(1, parsedOptions.size) + assertEquals(option, parsedOptions[0]) + } + + @Test fun ipv4OptionLooseSourceAndRecordRouteTooShort() { + val option = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val stream = ByteBuffer.wrap(option.toByteArray()) + stream.limit(stream.limit() - 1) + stream.position(2) + assertThrows { + Ipv4OptionLooseSourceAndRecordRoute.fromStream(stream, true, Ipv4OptionClassType.DebuggingAndMeasurement, 6u) + } + } + + @Test fun ipv4OptionLooseSourceAndRecordRouteEquals() { + val option1 = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val option2 = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertEquals(option1, option2) + + val option3 = Ipv4OptionLooseSourceAndRecordRoute(pointer = 1u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option3) + + val option4 = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x03)) + assertNotEquals(option1, option4) + } + + @Test fun ipv4OptionLooseSourceAndRecordRouteHashCode() { + val map: MutableMap = mutableMapOf() + val option1 = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + map[option1] = "test" + assertTrue(map.containsKey(option1)) + } } From 6e60c9b75afbef600176a463e15a0b4e4a27c395 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 17 Sep 2024 15:49:32 +0200 Subject: [PATCH 08/11] tcp mss tests --- .../knet/transport/tcp/options/TcpOptionTests.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt b/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt index f5a5ebb..2682de7 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt @@ -348,4 +348,20 @@ class TcpOptionTests { val option2 = TcpOptionUnsupported(1u, ByteArray(0)) assertEquals("TCPOptionUnsupported(NoOperation, kind=1, size=2, data=[])", option2.toString()) } + + @Test + fun tcpMssOrDefault() { + val tcpHeader = TcpHeader() + assertEquals(TcpOptionMaximumSegmentSize.mssOrDefault(tcpHeader), TcpOptionMaximumSegmentSize.defaultIpv4MSS) + assertEquals(TcpOptionMaximumSegmentSize.mssOrDefault(tcpHeader, false), TcpOptionMaximumSegmentSize.defaultIpv6MSS) + + val mss = TcpOptionMaximumSegmentSize(1000u) + val tcpHeader2 = TcpHeader(options = arrayListOf(mss)) + assertEquals(1000u.toUShort(), TcpOptionMaximumSegmentSize.mssOrDefault(tcpHeader2)) + } + + @Test fun tcpMSSToString() { + val mss = TcpOptionMaximumSegmentSize(1000u) + assertEquals("TCPOptionMaximumSegmentSize(kind=2 size=4 mss=1000)", mss.toString()) + } } From 9e1a39a4ee793bca643773368d31d5869e118d9c Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 17 Sep 2024 19:21:10 +0200 Subject: [PATCH 09/11] Added remaining required Ipv4 options --- .../jasonernst/knet/ip/options/Ipv4Option.kt | 12 ++ .../ip/options/Ipv4OptionInternetTimestamp.kt | 114 +++++++++++++++++ .../Ipv4OptionLooseSourceAndRecordRoute.kt | 8 +- .../knet/ip/options/Ipv4OptionRecordRoute.kt | 121 ++++++++++++++++++ .../ip/options/Ipv4OptionStreamIdentifier.kt | 77 +++++++++++ .../Ipv4OptionStrictSourceAndRecordRoute.kt | 103 +++++++++++++++ 6 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStrictSourceAndRecordRoute.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt index 2bd9bea..3535f19 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt @@ -79,6 +79,18 @@ abstract class Ipv4Option( Ipv4OptionType.LooseSourceRouting.kind -> { options.add(Ipv4OptionLooseSourceAndRecordRoute.fromStream(stream, isCopied, optionClass, length)) } + Ipv4OptionType.StrictSourceRouting.kind -> { + options.add(Ipv4OptionStrictSourceAndRecordRoute.fromStream(stream, isCopied, optionClass, length)) + } + Ipv4OptionType.RecordRoute.kind -> { + options.add(Ipv4OptionRecordRoute.fromStream(stream, isCopied, optionClass, length)) + } + Ipv4OptionType.StreamId.kind -> { + options.add(Ipv4OptionStreamIdentifier.fromStream(stream, isCopied, optionClass, length)) + } + Ipv4OptionType.TimeStamp.kind -> { + options.add(Ipv4OptionInternetTimestamp.fromStream(stream, isCopied, optionClass, length)) + } else -> { val data = ByteArray(length.toInt() - 2) stream.get(data) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt new file mode 100644 index 0000000..d9103d2 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt @@ -0,0 +1,114 @@ +package com.jasonernst.knet.ip.options + +import com.jasonernst.knet.PacketTooShortException +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * The Option Length is the number of octets in the option counting + * the type, length, pointer, and overflow/flag octets (maximum + * length 40). + * + * The Pointer is the number of octets from the beginning of this + * option to the end of timestamps plus one (i.e., it points to the + * octet beginning the space for next timestamp). The smallest + * legal value is 5. The timestamp area is full when the pointer + * is greater than the length. + * + * The Overflow (oflw) [4 bits] is the number of IP modules that + * cannot register timestamps due to lack of space. + * + * The Flag (flg) [4 bits] values are + * + * 0 -- time stamps only, stored in consecutive 32-bit words, + * + * 1 -- each timestamp is preceded with internet address of the + * registering entity, + * + * 3 -- the internet address fields are prespecified. An IP + * module only registers its timestamp if it matches its own + * address with the next specified internet address. + * + * The Timestamp is a right-justified, 32-bit timestamp in + * milliseconds since midnight UT. If the time is not available in + * milliseconds or cannot be provided with respect to midnight UT + * then any time may be inserted as a timestamp provided the high + * order bit of the timestamp field is set to one to indicate the + * use of a non-standard value. + * + * The originating host must compose this option with a large + * enough timestamp data area to hold all the timestamp information + * expected. The size of the option does not change due to adding + * timestamps. The intitial contents of the timestamp data area + * must be zero or internet address/zero pairs. + * + * If the timestamp data area is already full (the pointer exceeds + * the length) the datagram is forwarded without inserting the + * timestamp, but the overflow count is incremented by one. + * + * If there is some room but not enough room for a full timestamp + * to be inserted, or the overflow count itself overflows, the + * original datagram is considered to be in error and is discarded. + * In either case an ICMP parameter problem message may be sent to + * the source host [3]. + * + * The timestamp option is not copied upon fragmentation. It is + * carried in the first fragment. Appears at most once in a + * datagram. + */ +data class Ipv4OptionInternetTimestamp( + override val isCopied: Boolean = false, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.DebuggingAndMeasurement, + override val type: Ipv4OptionType = Ipv4OptionType.TimeStamp, + val pointer: UByte, + val overFlowFlags: UByte, + val internetAddress: UInt, + val timestamps: List, +) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = MIN_OPTION_SIZE) { + companion object { + val MIN_OPTION_SIZE: UByte = 5u + + fun fromStream( + stream: ByteBuffer, + isCopied: Boolean, + optionClass: Ipv4OptionClassType, + size: UByte, + ): Ipv4OptionInternetTimestamp { + if (stream.remaining() < (size - 2u).toInt()) { + throw PacketTooShortException( + "Stream must have at least ${size - 2u} " + + "remaining bytes remaining to parse Ipv4OptionInternetTimestamp, we only have " + + "${stream.remaining()} bytes", + ) + } + val pointer = stream.get().toUByte() + val overFlowFlags = stream.get().toUByte() + val internetAddress = stream.int + val timestamps = ArrayList() + while (stream.remaining() >= 4) { + timestamps.add(stream.int.toUInt()) + } + return Ipv4OptionInternetTimestamp( + isCopied = isCopied, + optionClass = optionClass, + pointer = pointer, + overFlowFlags = overFlowFlags, + internetAddress = internetAddress.toUInt(), + timestamps = timestamps, + ) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = + ByteBuffer + .allocate(MIN_OPTION_SIZE.toInt() + timestamps.size * 4) + .order(order) + .put(super.toByteArray(order)) + .put(pointer.toByte()) + .put(overFlowFlags.toByte()) + .putInt(internetAddress.toInt()) + timestamps.forEach { buffer.putInt(it.toInt()) } + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt index f0382c1..cea274e 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt @@ -8,6 +8,12 @@ import java.nio.ByteOrder /** * From RFC 791: * + * The loose source and record route (LSRR) option provides a means + * for the source of an internet datagram to supply routing + * information to be used by the gateways in forwarding the + * datagram to the destination, and to record the route + * information. + * * The option begins with the option type code. The second octet * is the option length which includes the option type code and the * length octet, the pointer octet, and length-3 octets of route @@ -28,7 +34,7 @@ data class Ipv4OptionLooseSourceAndRecordRoute( override val type: Ipv4OptionType = Ipv4OptionType.LooseSourceRouting, val pointer: UByte, val routeData: ByteArray = ByteArray(0), -) : Ipv4Option(isCopied, optionClass, type, size = (routeData.size.toUByte() + MIN_OPTION_SIZE).toUByte()) { +) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = (routeData.size.toUByte() + MIN_OPTION_SIZE).toUByte()) { companion object { val MIN_OPTION_SIZE: UByte = 3u private val logger = LoggerFactory.getLogger(Ipv4OptionLooseSourceAndRecordRoute::class.java) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt new file mode 100644 index 0000000..9567417 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt @@ -0,0 +1,121 @@ +package com.jasonernst.knet.ip.options + +import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.options.Ipv4OptionLooseSourceAndRecordRoute.Companion.MIN_OPTION_SIZE +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * From the RFC 791: + * + * The record route option provides a means to record the route of + * an internet datagram. + * + * The option begins with the option type code. The second octet + * is the option length which includes the option type code and the + * length octet, the pointer octet, and length-3 octets of route + * data. The third octet is the pointer into the route data + * indicating the octet which begins the next area to store a route + * address. The pointer is relative to this option, and the + * smallest legal value for the pointer is 4. + * + * A recorded route is composed of a series of internet addresses. + * Each internet address is 32 bits or 4 octets. If the pointer is + * greater than the length, the recorded route data area is full. + * The originating host must compose this option with a large + * enough route data area to hold all the address expected. The + * size of the option does not change due to adding addresses. The + * intitial contents of the route data area must be zero. + * + * When an internet module routes a datagram it checks to see if + * the record route option is present. If it is, it inserts its + * own internet address as known in the environment into which this + * datagram is being forwarded into the recorded route begining at + * the octet indicated by the pointer, and increments the pointer + * by four. + + * If the route data area is already full (the pointer exceeds the + * length) the datagram is forwarded without inserting the address + * into the recorded route. If there is some room but not enough + * room for a full address to be inserted, the original datagram is + * considered to be in error and is discarded. In either case an + * ICMP parameter problem message may be sent to the source + * host [3]. + + * Not copied on fragmentation, goes in first fragment only. + * Appears at most once in a datagram. + */ +data class Ipv4OptionRecordRoute( + override val isCopied: Boolean = false, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, + override val type: Ipv4OptionType = Ipv4OptionType.RecordRoute, + val pointer: UByte, + val routeData: ByteArray = ByteArray(0), +) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = MIN_OPTION_SIZE) { + companion object { + val MIN_OPTION_SIZE: UByte = 3u + private val logger = LoggerFactory.getLogger(Ipv4OptionRecordRoute::class.java) + + fun fromStream( + stream: ByteBuffer, + isCopied: Boolean, + optionClass: Ipv4OptionClassType, + size: UByte, + ): Ipv4OptionRecordRoute { + logger.debug("SIZE: $size, remaining: ${stream.remaining()}") + if (stream.remaining() < (size - 2u).toInt()) { + throw PacketTooShortException( + "Stream must have at least ${size - 2u} " + + "remaining bytes remaining to parse Ipv4OptionRecordRoute, we only have " + + "${stream.remaining()} bytes", + ) + } + val pointer = stream.get().toUByte() + val dataLength = size.toInt() - MIN_OPTION_SIZE.toInt() + val routingData = ByteArray(dataLength) + stream.get(routingData) + return Ipv4OptionRecordRoute( + isCopied = isCopied, + optionClass = optionClass, + pointer = pointer, + routeData = routingData, + ) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = + ByteBuffer + .allocate(MIN_OPTION_SIZE.toInt() + routeData.size) + .order(order) + .put(super.toByteArray(order)) + .put(pointer.toByte()) + .put(routeData) + return buffer.array() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Ipv4OptionRecordRoute + + if (isCopied != other.isCopied) return false + if (optionClass != other.optionClass) return false + if (type != other.type) return false + if (pointer != other.pointer) return false + if (!routeData.contentEquals(other.routeData)) return false + + return true + } + + override fun hashCode(): Int { + var result = isCopied.hashCode() + result = 31 * result + optionClass.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + pointer.hashCode() + result = 31 * result + routeData.contentHashCode() + return result + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt new file mode 100644 index 0000000..a9f3300 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt @@ -0,0 +1,77 @@ +package com.jasonernst.knet.ip.options + +import com.jasonernst.knet.PacketTooShortException +import java.nio.ByteBuffer + +/** + * From the RFC 791: + * + * The record route option provides a means to record the route of + * an internet datagram. + * + * The option begins with the option type code. The second octet + * is the option length which includes the option type code and the + * length octet, the pointer octet, and length-3 octets of route + * data. The third octet is the pointer into the route data + * indicating the octet which begins the next area to store a route + * address. The pointer is relative to this option, and the + * smallest legal value for the pointer is 4. + * + * A recorded route is composed of a series of internet addresses. + * Each internet address is 32 bits or 4 octets. If the pointer is + * + * greater than the length, the recorded route data area is full. + * The originating host must compose this option with a large + * enough route data area to hold all the address expected. The + * size of the option does not change due to adding addresses. The + * intitial contents of the route data area must be zero. + * + * When an internet module routes a datagram it checks to see if + * the record route option is present. If it is, it inserts its + * own internet address as known in the environment into which this + * datagram is being forwarded into the recorded route begining at + * the octet indicated by the pointer, and increments the pointer + * by four. + * + * If the route data area is already full (the pointer exceeds the + * length) the datagram is forwarded without inserting the address + * into the recorded route. If there is some room but not enough + * room for a full address to be inserted, the original datagram is + * considered to be in error and is discarded. In either case an + * ICMP parameter problem message may be sent to the source + * host [3]. + * + * Not copied on fragmentation, goes in first fragment only. + * Appears at most once in a datagram. + */ +data class Ipv4OptionStreamIdentifier( + override val isCopied: Boolean = true, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, + override val type: Ipv4OptionType = Ipv4OptionType.StreamId, + val streamId: UShort, +) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = 0u) { + companion object { + const val MIN_OPTION_SIZE: UByte = 3u + + fun fromStream( + stream: ByteBuffer, + isCopied: Boolean, + optionClass: Ipv4OptionClassType, + size: UByte, + ): Ipv4OptionStreamIdentifier { + if (stream.remaining() < (size - 2u).toInt()) { + throw PacketTooShortException( + "Stream must have at least ${size - 2u} " + + "remaining bytes remaining to parse Ipv4OptionStreamIdentifier, we only have " + + "${stream.remaining()} bytes", + ) + } + val streamId = stream.short.toUShort() + return Ipv4OptionStreamIdentifier( + isCopied = isCopied, + optionClass = optionClass, + streamId = streamId, + ) + } + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStrictSourceAndRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStrictSourceAndRecordRoute.kt new file mode 100644 index 0000000..5417c4b --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStrictSourceAndRecordRoute.kt @@ -0,0 +1,103 @@ +package com.jasonernst.knet.ip.options + +import com.jasonernst.knet.PacketTooShortException +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * From RFC 791: + * + * The strict source and record route (SSRR) option provides a + * means for the source of an internet datagram to supply routing + * information to be used by the gateways in forwarding the + * datagram to the destination, and to record the route + * information. + * + * The option begins with the option type code. The second octet + * is the option length which includes the option type code and the + * length octet, the pointer octet, and length-3 octets of route + * data. The third octet is the pointer into the route data + * indicating the octet which begins the next source address to be + * processed. The pointer is relative to this option, and the + * smallest legal value for the pointer is 4. + * + * A route data is composed of a series of internet addresses. + * Each internet address is 32 bits or 4 octets. If the pointer is + * greater than the length, the source route is empty (and the + * recorded route full) and the routing is to be based on the + * destination address field. + */ +data class Ipv4OptionStrictSourceAndRecordRoute( + override val isCopied: Boolean = true, + override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, + override val type: Ipv4OptionType = Ipv4OptionType.StrictSourceRouting, + val pointer: UByte, + val routeData: ByteArray = ByteArray(0), +) : Ipv4Option(isCopied, optionClass, type, size = (routeData.size.toUByte() + MIN_OPTION_SIZE).toUByte()) { + companion object { + val MIN_OPTION_SIZE: UByte = 3u + private val logger = LoggerFactory.getLogger(Ipv4OptionStrictSourceAndRecordRoute::class.java) + + fun fromStream( + stream: ByteBuffer, + isCopied: Boolean, + optionClass: Ipv4OptionClassType, + size: UByte, + ): Ipv4OptionStrictSourceAndRecordRoute { + logger.debug("SIZE: $size, remaining: ${stream.remaining()}") + if (stream.remaining() < (size - 2u).toInt()) { + throw PacketTooShortException( + "Stream must have at least ${size - 2u} " + + "remaining bytes remaining to parse Ipv4OptionLooseSourceAndRecordRoute, we only have " + + "${stream.remaining()} bytes", + ) + } + val pointer = stream.get().toUByte() + val dataLength = size.toInt() - MIN_OPTION_SIZE.toInt() + val routingData = ByteArray(dataLength) + stream.get(routingData) + return Ipv4OptionStrictSourceAndRecordRoute( + isCopied = isCopied, + optionClass = optionClass, + pointer = pointer, + routeData = routingData, + ) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = + ByteBuffer + .allocate(MIN_OPTION_SIZE.toInt() + routeData.size) + .order(order) + .put(super.toByteArray(order)) + .put(pointer.toByte()) + .put(routeData) + return buffer.array() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Ipv4OptionStrictSourceAndRecordRoute + + if (isCopied != other.isCopied) return false + if (optionClass != other.optionClass) return false + if (type != other.type) return false + if (pointer != other.pointer) return false + if (!routeData.contentEquals(other.routeData)) return false + + return true + } + + override fun hashCode(): Int { + var result = isCopied.hashCode() + result = 31 * result + optionClass.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + pointer.hashCode() + result = 31 * result + routeData.contentHashCode() + return result + } +} From 84ebb3b4c9fe8421dd876a43e8841e7971f3691c Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 18 Sep 2024 10:03:30 +0200 Subject: [PATCH 10/11] Added tests for remaining options --- .../ip/options/Ipv4OptionInternetTimestamp.kt | 12 +- .../knet/ip/options/Ipv4OptionRecordRoute.kt | 2 +- .../ip/options/Ipv4OptionStreamIdentifier.kt | 13 +- .../knet/ip/options/Ipv4OptionTest.kt | 211 ++++++++++++++++++ 4 files changed, 233 insertions(+), 5 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt index d9103d2..a438a3a 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt @@ -1,6 +1,7 @@ package com.jasonernst.knet.ip.options import com.jasonernst.knet.PacketTooShortException +import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder @@ -64,9 +65,15 @@ data class Ipv4OptionInternetTimestamp( val overFlowFlags: UByte, val internetAddress: UInt, val timestamps: List, -) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = MIN_OPTION_SIZE) { +) : Ipv4Option( + isCopied = isCopied, + optionClass = optionClass, + type = type, + size = (MIN_OPTION_SIZE.toInt() + timestamps.size * 4).toUByte(), + ) { companion object { - val MIN_OPTION_SIZE: UByte = 5u + private val logger = LoggerFactory.getLogger(Ipv4OptionInternetTimestamp::class.java) + val MIN_OPTION_SIZE: UByte = 8u // 2 for type, size, 1 for pointer, 1 for overflow/flags, 4 for internet address fun fromStream( stream: ByteBuffer, @@ -74,6 +81,7 @@ data class Ipv4OptionInternetTimestamp( optionClass: Ipv4OptionClassType, size: UByte, ): Ipv4OptionInternetTimestamp { + logger.debug("SIZE: $size, remaining: ${stream.remaining()}") if (stream.remaining() < (size - 2u).toInt()) { throw PacketTooShortException( "Stream must have at least ${size - 2u} " + diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt index 9567417..36526ad 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt @@ -52,7 +52,7 @@ data class Ipv4OptionRecordRoute( override val type: Ipv4OptionType = Ipv4OptionType.RecordRoute, val pointer: UByte, val routeData: ByteArray = ByteArray(0), -) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = MIN_OPTION_SIZE) { +) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = (MIN_OPTION_SIZE + routeData.size.toUByte()).toUByte()) { companion object { val MIN_OPTION_SIZE: UByte = 3u private val logger = LoggerFactory.getLogger(Ipv4OptionRecordRoute::class.java) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt index a9f3300..937b05b 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt @@ -2,6 +2,7 @@ package com.jasonernst.knet.ip.options import com.jasonernst.knet.PacketTooShortException import java.nio.ByteBuffer +import java.nio.ByteOrder /** * From the RFC 791: @@ -49,9 +50,9 @@ data class Ipv4OptionStreamIdentifier( override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control, override val type: Ipv4OptionType = Ipv4OptionType.StreamId, val streamId: UShort, -) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = 0u) { +) : Ipv4Option(isCopied = isCopied, optionClass = optionClass, type = type, size = MIN_OPTION_SIZE) { companion object { - const val MIN_OPTION_SIZE: UByte = 3u + const val MIN_OPTION_SIZE: UByte = 4u // two bytes for the type, size, two for the stremaid fun fromStream( stream: ByteBuffer, @@ -74,4 +75,12 @@ data class Ipv4OptionStreamIdentifier( ) } } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(size.toInt()) + buffer.order(order) + buffer.put(super.toByteArray(order)) + buffer.putShort(streamId.toShort()) + return buffer.array() + } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt index cb6642c..e0a68e3 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt @@ -206,4 +206,215 @@ class Ipv4OptionTest { map[option1] = "test" assertTrue(map.containsKey(option1)) } + + @Test fun ipv4OptionStrictSourceAndRecordRoute() { + val option = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val stream = ByteBuffer.wrap(option.toByteArray()) + logger.debug("Stream length: ${stream.limit()}") + val parsedOptions = Ipv4Option.parseOptions(stream) + assertEquals(1, parsedOptions.size) + assertEquals(option, parsedOptions[0]) + } + + @Test fun ipv4OptionStrictSourceAndRecordRouteTooShort() { + val option = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val stream = ByteBuffer.wrap(option.toByteArray()) + stream.limit(stream.limit() - 1) + stream.position(2) + assertThrows { + Ipv4OptionStrictSourceAndRecordRoute.fromStream(stream, true, Ipv4OptionClassType.DebuggingAndMeasurement, 6u) + } + } + + @Test fun ipv4OptionStrictSourceAndRecordRouteEquals() { + val option1 = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val option2 = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertEquals(option1, option2) + + val option3 = Ipv4OptionStrictSourceAndRecordRoute(pointer = 1u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option3) + + val option4 = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x03)) + assertNotEquals(option1, option4) + } + + @Test fun ipv4OptionStrictSourceAndRecordRouteHashCode() { + val map: MutableMap = mutableMapOf() + val option1 = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + map[option1] = "test" + assertTrue(map.containsKey(option1)) + } + + @Test fun ipv4OptionRecordRoute() { + val option = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02, 0x03)) + val stream = ByteBuffer.wrap(option.toByteArray()) + logger.debug("Stream length: ${stream.limit()}") + val parsedOptions = Ipv4Option.parseOptions(stream) + assertEquals(1, parsedOptions.size) + assertEquals(option, parsedOptions[0]) + } + + @Test fun ipv4OptionRecordRouteTooShort() { + val option = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val stream = ByteBuffer.wrap(option.toByteArray()) + stream.limit(stream.limit() - 1) + stream.position(2) + assertThrows { + Ipv4OptionRecordRoute.fromStream(stream, true, Ipv4OptionClassType.DebuggingAndMeasurement, 6u) + } + } + + @Test fun ipv4OptionRecordRouteEquals() { + val option1 = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + val option2 = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertEquals(option1, option2) + + val option3 = Ipv4OptionRecordRoute(pointer = 1u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option3) + + val option4 = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x03)) + assertNotEquals(option1, option4) + } + + @Test fun ipv4OptionRecordRouteHashCode() { + val map: MutableMap = mutableMapOf() + val option1 = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + map[option1] = "test" + assertTrue(map.containsKey(option1)) + } + + @Test fun ipv4OptionStreamIdentifier() { + val option = Ipv4OptionStreamIdentifier(streamId = 0x1234u) + val stream = ByteBuffer.wrap(option.toByteArray()) + logger.debug("Stream length: ${stream.limit()}") + val parsedOptions = Ipv4Option.parseOptions(stream) + assertEquals(1, parsedOptions.size) + assertEquals(option, parsedOptions[0]) + } + + @Test fun ipv4OptionStreamIdentifierTooShort() { + val option = Ipv4OptionStreamIdentifier(streamId = 0x1234u) + val stream = ByteBuffer.wrap(option.toByteArray()) + stream.limit(stream.limit() - 1) + stream.position(2) + assertThrows { + Ipv4OptionStreamIdentifier.fromStream(stream, true, Ipv4OptionClassType.DebuggingAndMeasurement, 6u) + } + } + + @Test fun ipv4OptionStreamIdentifierEquals() { + val option1 = Ipv4OptionStreamIdentifier(streamId = 0x1234u) + val option2 = Ipv4OptionStreamIdentifier(streamId = 0x1234u) + assertEquals(option1, option2) + + val option3 = Ipv4OptionStreamIdentifier(streamId = 0x1235u) + assertNotEquals(option1, option3) + } + + @Test fun ipv4OptionTimestamp() { + val option = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5678u), + ) + val stream = ByteBuffer.wrap(option.toByteArray()) + logger.debug("Stream length: ${stream.limit()}") + val parsedOptions = Ipv4Option.parseOptions(stream) + assertEquals(1, parsedOptions.size) + assertEquals(option, parsedOptions[0]) + } + + @Test fun ipv4OptionTimestampTooShort() { + val option = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5678u), + ) + val stream = ByteBuffer.wrap(option.toByteArray()) + stream.limit(stream.limit() - 1) + stream.position(2) + assertThrows { + Ipv4OptionInternetTimestamp.fromStream(stream, true, Ipv4OptionClassType.DebuggingAndMeasurement, option.size) + } + } + + @Test fun ipv4OptionTimestampEquals() { + val option1 = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5678u), + ) + val option2 = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5678u), + ) + assertEquals(option1, option2) + + val option3 = + Ipv4OptionInternetTimestamp( + pointer = 1u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5678u), + ) + assertNotEquals(option1, option3) + + val option4 = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x02u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5678u), + ) + assertNotEquals(option1, option4) + + val option5 = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5679u, + timestamps = listOf(0x1234u, 0x5678u), + ) + assertNotEquals(option1, option5) + + val option6 = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1235u, 0x5678u), + ) + assertNotEquals(option1, option6) + + val option7 = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5679u), + ) + assertNotEquals(option1, option7) + } + + @Test fun ipv4OptionTimestampHashCode() { + val map: MutableMap = mutableMapOf() + val option1 = + Ipv4OptionInternetTimestamp( + pointer = 0u, + overFlowFlags = 0x01u, + internetAddress = 0x5678u, + timestamps = listOf(0x1234u, 0x5678u), + ) + map[option1] = "test" + assertTrue(map.containsKey(option1)) + } } From dc17e63bed30cc68316b421b7cb1665dca3b44ef Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 18 Sep 2024 13:34:20 +0200 Subject: [PATCH 11/11] added more tests --- .../com/jasonernst/knet/ip/Ipv4Header.kt | 11 +- .../tcp/options/TcpOptionTypeUnsupported.kt | 9 -- .../com/jasonernst/knet/ip/Ipv4HeaderTest.kt | 11 ++ .../knet/ip/options/Ipv4OptionTest.kt | 152 ++++++++++++++++-- .../transport/tcp/options/TcpOptionTests.kt | 37 ++++- 5 files changed, 187 insertions(+), 33 deletions(-) delete mode 100644 knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTypeUnsupported.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt index 338e6e8..07f462b 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt @@ -11,6 +11,7 @@ import java.net.InetAddress import java.nio.ByteBuffer import java.nio.ByteOrder import kotlin.experimental.or +import kotlin.math.ceil /** * Internet Protocol Version 4 Header Implementation. @@ -67,8 +68,8 @@ data class Ipv4Header( // dummy check that ihl matches the options length val optionsLength = options.sumOf { it.size.toInt() }.toUInt() - if (ihl * IP4_WORD_LENGTH != (IP4_MIN_HEADER_LENGTH + optionsLength).toUByte().toUInt()) { - val expectedIHL = ((IP4_MIN_HEADER_LENGTH + optionsLength) / IP4_WORD_LENGTH).toUByte() + val expectedIHL = ceil((IP4_MIN_HEADER_LENGTH.toDouble() + optionsLength.toDouble()) / IP4_WORD_LENGTH.toDouble()).toUInt() + if (ihl.toUInt() != expectedIHL) { throw IllegalArgumentException( "Invalid IPv4 header. IHL does not match the options length, IHL should be $expectedIHL, but was $ihl because options length was $optionsLength", ) @@ -139,12 +140,6 @@ data class Ipv4Header( // make sure we don't process into a second packet val limitOfPacket = start + (ihl * IP4_WORD_LENGTH).toInt() - val expectedRemaining = limitOfPacket - start - IP4_MIN_HEADER_LENGTH.toInt() - if (stream.remaining() < expectedRemaining) { - throw PacketTooShortException( - "Not enough data in stream to parse Ipv4 options, expecting $expectedRemaining, have ${stream.remaining()}", - ) - } val options = parseOptions(stream, limitOfPacket) return Ipv4Header( diff --git a/knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTypeUnsupported.kt b/knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTypeUnsupported.kt deleted file mode 100644 index 86ebca1..0000000 --- a/knet/src/main/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTypeUnsupported.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.jasonernst.knet.transport.tcp.options - -class TcpOptionTypeUnsupported( - override val kind: UByte, -) : TcpOptionType { - companion object { - fun fromKind(kind: UByte) = TcpOptionTypeUnsupported(kind) - } -} diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt index da36fa2..ef6868c 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt @@ -5,6 +5,7 @@ import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader.Companion.IP4_VERSION import com.jasonernst.knet.ip.Ipv4Header.Companion.IP4_MIN_HEADER_LENGTH import com.jasonernst.knet.ip.Ipv4Header.Companion.IP4_WORD_LENGTH +import com.jasonernst.knet.ip.options.Ipv4OptionNoOperation import com.jasonernst.knet.transport.tcp.TcpHeader import com.jasonernst.knet.transport.tcp.options.TcpOptionEndOfOptionList import com.jasonernst.packetdumper.stringdumper.StringPacketDumper @@ -304,4 +305,14 @@ class Ipv4HeaderTest { Ipv4Header.fromStream(buffer) } } + + @Test fun tooShortForOptions() { + val options = listOf(Ipv4OptionNoOperation()) + val ipv4Header = Ipv4Header(ihl = 6u, options = options) + val stream = ByteBuffer.wrap(ipv4Header.toByteArray()) + stream.limit(stream.limit() - 1) + assertThrows { + Ipv4Header.fromStream(stream) + } + } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt index e0a68e3..55bf385 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt @@ -1,6 +1,8 @@ package com.jasonernst.knet.ip.options import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.Ipv4Header +import com.jasonernst.knet.transport.tcp.options.TcpOptionNoOperation import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -28,6 +30,31 @@ class Ipv4OptionTest { assertEquals(options, parsedOptions) } + @Test fun mismatchedOptionsLengthIhl() { + val options = + arrayListOf( + Ipv4OptionNoOperation(true, Ipv4OptionClassType.Control), + Ipv4OptionEndOfOptionList(true, Ipv4OptionClassType.Control), + ) + assertThrows { + Ipv4Header(options = options) + } + } + + @Test fun correctOptionsLengthButTooShort() { + val options = + arrayListOf( + Ipv4OptionNoOperation(true, Ipv4OptionClassType.Control), + Ipv4OptionEndOfOptionList(true, Ipv4OptionClassType.Control), + ) + val ipHeader = Ipv4Header(ihl = 6u, options = options) + val stream = ByteBuffer.wrap(ipHeader.toByteArray()) + stream.limit(stream.limit() - 1) + assertThrows { + Ipv4Header.fromStream(stream) + } + } + @Test fun copiedClassTypeTest() { val option = Ipv4OptionUnknown(isCopied = true, optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, type = Ipv4OptionType.StreamId) @@ -42,13 +69,20 @@ class Ipv4OptionTest { @Test fun unknownOption() { // unhandled option, but in list - val stream = ByteBuffer.wrap(byteArrayOf(0xFE.toByte(), 0x04, 0x00, 0x00)) + val data = byteArrayOf(0x00, 0x00) + val stream = ByteBuffer.wrap(byteArrayOf(0xFE.toByte(), 0x04, data[0], data[1])) val parsedOptions = Ipv4Option.parseOptions(stream, 4) + assertEquals(1, parsedOptions.size) assertTrue(parsedOptions[0] is Ipv4OptionUnknown) + val parsedOption = parsedOptions[0] as Ipv4OptionUnknown + for (i in data.indices) { + assertEquals(data[i], parsedOption.data[i]) + } // unhandled option, not in list val stream2 = ByteBuffer.wrap(byteArrayOf(0x11.toByte(), 0x04, 0x00, 0x00)) val parsedOptions2 = Ipv4Option.parseOptions(stream2, 4) + assertEquals(1, parsedOptions2.size) assertTrue(parsedOptions2[0] is Ipv4OptionUnknown) // unknown option good path @@ -58,6 +92,11 @@ class Ipv4OptionTest { val parsedOptions3 = Ipv4Option.parseOptions(stream3) assertEquals(1, parsedOptions3.size) assertEquals(unhandledOption, parsedOptions3[0]) + + val otherClass = TcpOptionNoOperation() + assertNotEquals(unhandledOption, otherClass) + assertNotEquals(unhandledOption, null) + assertEquals(unhandledOption, unhandledOption) } @Test fun unknownOptionTooShortLength() { @@ -148,24 +187,32 @@ class Ipv4OptionTest { val parsedOptions = Ipv4Option.parseOptions(stream) assertEquals(1, parsedOptions.size) assertEquals(option, parsedOptions[0]) + val parsedOption = parsedOptions[0] as Ipv4OptionSecurity + assertEquals(option.security, parsedOption.security) + assertEquals(option.compartment, parsedOption.compartment) + assertEquals(option.handlingRestrictions, parsedOption.handlingRestrictions) + assertEquals(option.tcc, parsedOption.tcc) } @Test fun ipv4OptionSecurityTooShort() { - val option = - Ipv4OptionSecurity( - isCopied = true, - optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, - type = Ipv4OptionType.Security, - security = Ipv4OptionSecurityType.Confidential, - compartment = 1234u, - handlingRestrictions = 5678u, - tcc = 9102u, - ) + val option = Ipv4OptionSecurity() val stream = ByteBuffer.wrap(option.toByteArray()) stream.limit(stream.limit() - 1) assertThrows { Ipv4Option.parseOptions(stream) } + + stream.position(2) + assertThrows { + Ipv4OptionSecurity.fromStream(stream, option.isCopied, option.optionClass, option.size) + } + } + + @Test + fun ipv4OptionSecurityTypeBad() { + assertThrows { + Ipv4OptionSecurityType.fromKind(0x20u) + } } @Test @@ -198,6 +245,33 @@ class Ipv4OptionTest { val option4 = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x03)) assertNotEquals(option1, option4) + + val option5 = TcpOptionNoOperation() + assertNotEquals(option1, option5) + assertNotEquals(option1, null) + assertEquals(option1, option1) + assertEquals(option1.pointer, option1.pointer) + for (i in option1.routeData.indices) { + assertEquals(option1.routeData[i], option1.routeData[i]) + } + + val option6 = Ipv4OptionLooseSourceAndRecordRoute(isCopied = false, pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option6) + + val option7 = + Ipv4OptionLooseSourceAndRecordRoute( + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + pointer = 0u, + routeData = byteArrayOf(0x00, 0x01, 0x02), + ) + assertNotEquals(option1, option7) + + val option8 = + Ipv4OptionLooseSourceAndRecordRoute(type = Ipv4OptionType.StreamId, pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option8) + + val option9 = Ipv4OptionLooseSourceAndRecordRoute(pointer = 0u) + assertNotEquals(option1, option9) } @Test fun ipv4OptionLooseSourceAndRecordRouteHashCode() { @@ -236,6 +310,33 @@ class Ipv4OptionTest { val option4 = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x03)) assertNotEquals(option1, option4) + + val option5 = TcpOptionNoOperation() + assertNotEquals(option1, option5) + assertNotEquals(option1, null) + assertEquals(option1, option1) + assertEquals(option1.pointer, option1.pointer) + for (i in option1.routeData.indices) { + assertEquals(option1.routeData[i], option1.routeData[i]) + } + + val option6 = Ipv4OptionStrictSourceAndRecordRoute(isCopied = false, pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option6) + + val option7 = + Ipv4OptionStrictSourceAndRecordRoute( + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + pointer = 0u, + routeData = byteArrayOf(0x00, 0x01, 0x02), + ) + assertNotEquals(option1, option7) + + val option8 = + Ipv4OptionStrictSourceAndRecordRoute(type = Ipv4OptionType.StreamId, pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option8) + + val option9 = Ipv4OptionStrictSourceAndRecordRoute(pointer = 0u) + assertNotEquals(option1, option9) } @Test fun ipv4OptionStrictSourceAndRecordRouteHashCode() { @@ -268,12 +369,36 @@ class Ipv4OptionTest { val option1 = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) val option2 = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) assertEquals(option1, option2) + assertEquals(option1.pointer, option2.pointer) + for (i in option1.routeData.indices) { + assertEquals(option1.routeData[i], option2.routeData[i]) + } val option3 = Ipv4OptionRecordRoute(pointer = 1u, routeData = byteArrayOf(0x00, 0x01, 0x02)) assertNotEquals(option1, option3) val option4 = Ipv4OptionRecordRoute(pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x03)) assertNotEquals(option1, option4) + + val option5 = TcpOptionNoOperation() + assertNotEquals(option1, option5) + assertNotEquals(option1, null) + assertEquals(option1, option1) + + val option6 = Ipv4OptionRecordRoute(isCopied = true, pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option6) + + val option7 = + Ipv4OptionRecordRoute( + optionClass = Ipv4OptionClassType.DebuggingAndMeasurement, + pointer = 0u, + routeData = byteArrayOf(0x00, 0x01, 0x02), + ) + assertNotEquals(option1, option7) + + val option8 = + Ipv4OptionRecordRoute(type = Ipv4OptionType.StreamId, pointer = 0u, routeData = byteArrayOf(0x00, 0x01, 0x02)) + assertNotEquals(option1, option8) } @Test fun ipv4OptionRecordRouteHashCode() { @@ -324,6 +449,11 @@ class Ipv4OptionTest { val parsedOptions = Ipv4Option.parseOptions(stream) assertEquals(1, parsedOptions.size) assertEquals(option, parsedOptions[0]) + val parsedOption = parsedOptions[0] as Ipv4OptionInternetTimestamp + assertEquals(option.pointer, parsedOption.pointer) + assertEquals(option.overFlowFlags, parsedOption.overFlowFlags) + assertEquals(option.internetAddress, parsedOption.internetAddress) + assertEquals(option.timestamps, parsedOption.timestamps) } @Test fun ipv4OptionTimestampTooShort() { diff --git a/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt b/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt index 2682de7..cefed38 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/transport/tcp/options/TcpOptionTests.kt @@ -3,6 +3,7 @@ package com.jasonernst.knet.tcp.options import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.options.Ipv4OptionNoOperation import com.jasonernst.knet.nextheader.NextHeader import com.jasonernst.knet.transport.tcp.TcpHeader import com.jasonernst.knet.transport.tcp.options.TcpOption @@ -122,6 +123,12 @@ class TcpOptionTests { assertEquals(tcpHeader, tcpHeaderFromBuffer) } + @Test + fun endOfOptionListToString() { + val endOfOptionList = TcpOptionEndOfOptionList() + assertEquals("TCPOptionEndOfOptionList(kind=0 size=1)", endOfOptionList.toString()) + } + @Test fun optionMssTooShort() { val options = arrayListOf(TcpOptionMaximumSegmentSize(1222u)) @@ -132,7 +139,7 @@ class TcpOptionTests { stream.rewind() stream.limit(stream.limit() - 1) assertThrows { - TcpOption.parseOptions(stream, stream.limit()) + parseOptions(stream, stream.limit()) } } @@ -146,7 +153,7 @@ class TcpOptionTests { stream.rewind() stream.limit(stream.limit() - 1) assertThrows { - TcpOption.parseOptions(stream, stream.limit()) + parseOptions(stream, stream.limit()) } } @@ -160,7 +167,7 @@ class TcpOptionTests { stream.rewind() stream.limit(stream.limit() - 1) assertThrows { - TcpOption.parseOptions(stream, stream.limit()) + parseOptions(stream, stream.limit()) } } @@ -174,7 +181,7 @@ class TcpOptionTests { stream.rewind() stream.limit(stream.limit() - 1) assertThrows { - TcpOption.parseOptions(stream, stream.limit()) + parseOptions(stream, stream.limit()) } } @@ -315,8 +322,15 @@ class TcpOptionTests { assertNull(TcpOptionTimestamp.maybeTimestamp(tcpHeader)) val timestamp = TcpOptionTimestamp(tsval = 1u, tsecr = 2u) - val tcpHeader2 = TcpHeader(options = arrayListOf(timestamp)) + val tcpHeader2 = TcpHeader(options = arrayListOf(TcpOptionMaximumSegmentSize(1000u), timestamp)) assertNotNull(TcpOptionTimestamp.maybeTimestamp(tcpHeader2)) + assertEquals(timestamp, TcpOptionTimestamp.maybeTimestamp(tcpHeader2)) + assertEquals(timestamp.tsecr, TcpOptionTimestamp.maybeTimestamp(tcpHeader2)!!.tsecr) + assertEquals(timestamp.tsval, TcpOptionTimestamp.maybeTimestamp(tcpHeader2)!!.tsval) + + // just here to increase line coverage with setters + timestamp.tsecr = 3u + timestamp.tsval = 4u } @Test @@ -330,6 +344,19 @@ class TcpOptionTests { val option4 = TcpOptionNoOperation() assertNotEquals(option1, option4) + + val option5 = Ipv4OptionNoOperation() + assertNotEquals(option1, option5) + + val option6 = TcpOptionUnsupported(88u, byteArrayOf(0x01)) + val option7 = TcpOptionUnsupported(88u, byteArrayOf(0x02)) + assertNotEquals(option6, option7) + + val option8 = TcpOptionUnsupported(89u, byteArrayOf(0x01)) + assertNotEquals(option6, option8) + + assertEquals(option7.kind, option6.kind) + assertNotEquals(option7.data, option6.data) } @Test