From c1a89ed6f1cfa6716d52e43ffd15537f1a718644 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 13 Sep 2024 15:02:35 +0200 Subject: [PATCH] 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)) } }