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 + } +}