Skip to content

Commit

Permalink
Merge pull request #7 from compscidr/jason/ipv4options
Browse files Browse the repository at this point in the history
Ipv4 options
  • Loading branch information
compscidr authored Sep 18, 2024
2 parents 82f3966 + dc17e63 commit c2fc349
Show file tree
Hide file tree
Showing 20 changed files with 1,586 additions and 37 deletions.
37 changes: 25 additions & 12 deletions knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ 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
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.experimental.or
import kotlin.math.ceil

/**
* Internet Protocol Version 4 Header Implementation.
Expand Down Expand Up @@ -47,6 +50,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 +66,31 @@ data class Ipv4Header(
flag = flag or 0x20
}

// dummy check that ihl matches the options length
val optionsLength = options.sumOf { it.size.toInt() }.toUInt()
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",
)
}

// 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 +112,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 +138,9 @@ 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())
}
logger.debug("POS: ${stream.position()}, remaining: ${stream.remaining()}")
// make sure we don't process into a second packet
val limitOfPacket = start + (ihl * IP4_WORD_LENGTH).toInt()
val options = parseOptions(stream, limitOfPacket)

return Ipv4Header(
ihl = ihl,
Expand All @@ -147,6 +156,7 @@ data class Ipv4Header(
headerChecksum = checksum,
sourceAddress = sourceAddress,
destinationAddress = destinationAddress,
options = options,
)
}
}
Expand Down Expand Up @@ -174,11 +184,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
129 changes: 129 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,129 @@
package com.jasonernst.knet.ip.options

import com.jasonernst.knet.PacketTooShortException
import org.slf4j.LoggerFactory
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 {
private val logger = LoggerFactory.getLogger(Ipv4Option::class.java)

fun parseOptions(
stream: ByteBuffer,
limit: Int = stream.limit(),
): 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 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")
}
when (kind) {
Ipv4OptionType.Security.kind -> {
options.add(Ipv4OptionSecurity.fromStream(stream, isCopied, optionClass, length))
}
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)

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 {
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()
if (size.toInt() == 1) {
return byteArrayOf(typeByte)
}
val buffer = ByteBuffer.allocate(2)
buffer.put(typeByte)
buffer.put(size.toByte())
return buffer.array()
}
}
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,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 = false,
override val optionClass: Ipv4OptionClassType = Ipv4OptionClassType.Control,
override val type: Ipv4OptionType = Ipv4OptionType.EndOfOptionList,
override val size: UByte = 1u,
) : Ipv4Option(isCopied, optionClass, type, size)
Loading

0 comments on commit c2fc349

Please sign in to comment.