Skip to content

Commit

Permalink
WiP: ipv4 options support
Browse files Browse the repository at this point in the history
  • Loading branch information
compscidr committed Sep 13, 2024
1 parent 82f3966 commit a2083cb
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 23 deletions.
40 changes: 29 additions & 11 deletions knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Ipv4Option> = emptyList(),
) : IpHeader {
// 3-bits: set from mayFragment and lastFragment
// bit 0: Reserved; must be zero
Expand All @@ -62,22 +65,31 @@ 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")
val buffer = toByteArray()
// ^ 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()
Expand All @@ -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",
)
Expand All @@ -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,
Expand All @@ -147,6 +161,7 @@ data class Ipv4Header(
headerChecksum = checksum,
sourceAddress = sourceAddress,
destinationAddress = destinationAddress,
options = options,
)
}
}
Expand Down Expand Up @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -83,7 +83,7 @@ data class Ipv6Header(
hopLimit,
sourceAddress,
destinationAddress,
options,
extensionHeaders,
)
}
}
Expand Down
90 changes: 90 additions & 0 deletions knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt
Original file line number Diff line number Diff line change
@@ -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<Ipv4Option> {
val options = ArrayList<Ipv4Option>()
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())
}
}
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
10 changes: 0 additions & 10 deletions knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit a2083cb

Please sign in to comment.