From b35a75d5b1093ad92470789586f1e8aa2b2c0fd9 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Sep 2024 14:00:51 +0300 Subject: [PATCH 01/22] restructured directories --- .../kotlin/com/jasonernst/knet/ip/IpHeader.kt | 4 +- .../jasonernst/knet/ip/{ => v4}/Ipv4Header.kt | 8 ++- .../knet/ip/{ => v4}/options/Ipv4Option.kt | 56 ++++++++++++++++--- .../{ => v4}/options/Ipv4OptionClassType.kt | 2 +- .../options/Ipv4OptionEndOfOptionList.kt | 2 +- .../options/Ipv4OptionInternetTimestamp.kt | 2 +- .../Ipv4OptionLooseSourceAndRecordRoute.kt | 2 +- .../{ => v4}/options/Ipv4OptionNoOperation.kt | 2 +- .../{ => v4}/options/Ipv4OptionRecordRoute.kt | 4 +- .../ip/{ => v4}/options/Ipv4OptionSecurity.kt | 2 +- .../options/Ipv4OptionSecurityType.kt | 2 +- .../options/Ipv4OptionStreamIdentifier.kt | 2 +- .../Ipv4OptionStrictSourceAndRecordRoute.kt | 2 +- .../ip/{ => v4}/options/Ipv4OptionType.kt | 2 +- .../ip/{ => v4}/options/Ipv4OptionUnknown.kt | 2 +- .../jasonernst/knet/ip/{ => v6}/Ipv6Header.kt | 9 ++- .../{ => v6/extenions}/Ipv6ExtensionHeader.kt | 12 +++- .../{ => v6/extenions}/Ipv6HopByHopOption.kt | 4 +- .../kotlin/com/jasonernst/knet/PacketTests.kt | 4 +- .../knet/ip/{ => v4}/Ipv4HeaderTest.kt | 12 ++-- .../ip/{ => v4}/options/Ipv4OptionTest.kt | 17 +++++- .../knet/ip/{ => v6}/Ipv6HeaderTest.kt | 5 +- .../transport/tcp/options/TcpOptionTests.kt | 2 +- 23 files changed, 119 insertions(+), 40 deletions(-) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/Ipv4Header.kt (98%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4Option.kt (69%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionClassType.kt (93%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionEndOfOptionList.kt (94%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionInternetTimestamp.kt (99%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionLooseSourceAndRecordRoute.kt (98%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionNoOperation.kt (93%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionRecordRoute.kt (97%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionSecurity.kt (98%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionSecurityType.kt (95%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionStreamIdentifier.kt (98%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionStrictSourceAndRecordRoute.kt (98%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionType.kt (92%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionUnknown.kt (97%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v6}/Ipv6Header.kt (95%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v6/extenions}/Ipv6ExtensionHeader.kt (89%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/{ => v6/extenions}/Ipv6HopByHopOption.kt (91%) rename knet/src/test/kotlin/com/jasonernst/knet/ip/{ => v4}/Ipv4HeaderTest.kt (96%) rename knet/src/test/kotlin/com/jasonernst/knet/ip/{ => v4}/options/Ipv4OptionTest.kt (96%) rename knet/src/test/kotlin/com/jasonernst/knet/ip/{ => v6}/Ipv6HeaderTest.kt (91%) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt index 985f5ed..3631f58 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt @@ -1,7 +1,9 @@ package com.jasonernst.knet.ip import com.jasonernst.knet.PacketTooShortException -import com.jasonernst.knet.ip.Ipv4Header.Companion.IP4_MIN_HEADER_LENGTH +import com.jasonernst.knet.ip.v4.Ipv4Header +import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_MIN_HEADER_LENGTH +import com.jasonernst.knet.ip.v6.Ipv6Header import org.slf4j.LoggerFactory import java.net.Inet4Address import java.net.Inet6Address diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt similarity index 98% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt index e9cc93d..d0559bd 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv4Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt @@ -1,10 +1,12 @@ -package com.jasonernst.knet.ip +package com.jasonernst.knet.ip.v4 import com.jasonernst.icmp_common.Checksum import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpHeader 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 com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v4.options.Ipv4Option +import com.jasonernst.knet.ip.v4.options.Ipv4Option.Companion.parseOptions import org.slf4j.LoggerFactory import java.net.Inet4Address import java.net.InetAddress diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4Option.kt similarity index 69% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4Option.kt index 3535f19..949b9d1 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4Option.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4Option.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException import org.slf4j.LoggerFactory @@ -74,22 +74,64 @@ abstract class Ipv4Option( } when (kind) { Ipv4OptionType.Security.kind -> { - options.add(Ipv4OptionSecurity.fromStream(stream, isCopied, optionClass, length)) + options.add( + Ipv4OptionSecurity.fromStream( + stream, + isCopied, + optionClass, + length + ) + ) } Ipv4OptionType.LooseSourceRouting.kind -> { - options.add(Ipv4OptionLooseSourceAndRecordRoute.fromStream(stream, isCopied, optionClass, length)) + options.add( + Ipv4OptionLooseSourceAndRecordRoute.fromStream( + stream, + isCopied, + optionClass, + length + ) + ) } Ipv4OptionType.StrictSourceRouting.kind -> { - options.add(Ipv4OptionStrictSourceAndRecordRoute.fromStream(stream, isCopied, optionClass, length)) + options.add( + Ipv4OptionStrictSourceAndRecordRoute.fromStream( + stream, + isCopied, + optionClass, + length + ) + ) } Ipv4OptionType.RecordRoute.kind -> { - options.add(Ipv4OptionRecordRoute.fromStream(stream, isCopied, optionClass, length)) + options.add( + Ipv4OptionRecordRoute.fromStream( + stream, + isCopied, + optionClass, + length + ) + ) } Ipv4OptionType.StreamId.kind -> { - options.add(Ipv4OptionStreamIdentifier.fromStream(stream, isCopied, optionClass, length)) + options.add( + Ipv4OptionStreamIdentifier.fromStream( + stream, + isCopied, + optionClass, + length + ) + ) } Ipv4OptionType.TimeStamp.kind -> { - options.add(Ipv4OptionInternetTimestamp.fromStream(stream, isCopied, optionClass, length)) + options.add( + Ipv4OptionInternetTimestamp.fromStream( + stream, + isCopied, + optionClass, + length + ) + ) } else -> { val data = ByteArray(length.toInt() - 2) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionClassType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionClassType.kt similarity index 93% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionClassType.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionClassType.kt index d7af406..4be914b 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionClassType.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionClassType.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options /** * The option classes are: diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionEndOfOptionList.kt similarity index 94% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionEndOfOptionList.kt index 18a294d..e5c0183 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionEndOfOptionList.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionEndOfOptionList.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options /** * diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionInternetTimestamp.kt similarity index 99% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionInternetTimestamp.kt index a438a3a..420cfa4 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionInternetTimestamp.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionInternetTimestamp.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException import org.slf4j.LoggerFactory diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionLooseSourceAndRecordRoute.kt similarity index 98% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionLooseSourceAndRecordRoute.kt index cea274e..6a29cb8 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionLooseSourceAndRecordRoute.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionLooseSourceAndRecordRoute.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException import org.slf4j.LoggerFactory diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionNoOperation.kt similarity index 93% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionNoOperation.kt index ce45d1b..b3d612f 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionNoOperation.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionNoOperation.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options /** * From RFC 791: diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionRecordRoute.kt similarity index 97% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionRecordRoute.kt index 36526ad..f2dbf62 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionRecordRoute.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionRecordRoute.kt @@ -1,7 +1,7 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException -import com.jasonernst.knet.ip.options.Ipv4OptionLooseSourceAndRecordRoute.Companion.MIN_OPTION_SIZE +import com.jasonernst.knet.ip.v4.options.Ipv4OptionLooseSourceAndRecordRoute.Companion.MIN_OPTION_SIZE import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionSecurity.kt similarity index 98% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionSecurity.kt index 52ebfd6..898ac3d 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurity.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionSecurity.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException import org.slf4j.LoggerFactory diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurityType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionSecurityType.kt similarity index 95% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurityType.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionSecurityType.kt index 01f176d..d331e97 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionSecurityType.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionSecurityType.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options enum class Ipv4OptionSecurityType( val kind: UShort, diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionStreamIdentifier.kt similarity index 98% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionStreamIdentifier.kt index 937b05b..dec72b2 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStreamIdentifier.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionStreamIdentifier.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException import java.nio.ByteBuffer diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStrictSourceAndRecordRoute.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionStrictSourceAndRecordRoute.kt similarity index 98% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStrictSourceAndRecordRoute.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionStrictSourceAndRecordRoute.kt index 5417c4b..76da762 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionStrictSourceAndRecordRoute.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionStrictSourceAndRecordRoute.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException import org.slf4j.LoggerFactory diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionType.kt similarity index 92% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionType.kt index 5b041b6..f512726 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionType.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionType.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options /** * https://datatracker.ietf.org/doc/html/rfc791 page 15 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionUnknown.kt similarity index 97% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionUnknown.kt index 7abd367..89c8dbd 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionUnknown.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionUnknown.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt similarity index 95% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt index 3bfd7ad..c40c621 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt @@ -1,7 +1,10 @@ -package com.jasonernst.knet.ip +package com.jasonernst.knet.ip.v6 import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpHeader.Companion.IP6_VERSION +import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader import org.slf4j.LoggerFactory import java.net.Inet6Address import java.net.InetAddress @@ -72,7 +75,9 @@ data class Ipv6Header( val destinationBuffer = ByteArray(16) stream[destinationBuffer] val destinationAddress = Inet6Address.getByAddress(destinationBuffer) as Inet6Address - val extensionHeaders = Ipv6ExtensionHeader.fromStream(stream, IpType.fromValue(protocol)) + val extensionHeaders = Ipv6ExtensionHeader.fromStream(stream, + IpType.fromValue(protocol) + ) return Ipv6Header( ipVersion, diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt similarity index 89% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6ExtensionHeader.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index eb9e387..4676e7e 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -1,6 +1,7 @@ -package com.jasonernst.knet.ip +package com.jasonernst.knet.ip.v6.extenions import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpType import java.nio.ByteBuffer import java.nio.ByteOrder @@ -37,7 +38,14 @@ open class Ipv6ExtensionHeader( * Authentication * Encapsulating Security Payload */ - val requiredExtensionHeaders = listOf(IpType.HOPOPT, IpType.IPV6_FRAG, IpType.IPV6_OPTS, IpType.IPV6_ROUTE, IpType.AH, IpType.ESP) + val requiredExtensionHeaders = listOf( + IpType.HOPOPT, + IpType.IPV6_FRAG, + IpType.IPV6_OPTS, + IpType.IPV6_ROUTE, + IpType.AH, + IpType.ESP + ) /** * This will continue to process IPv6 extension headers until the nextheader is not one, ie) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6HopByHopOption.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOption.kt similarity index 91% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6HopByHopOption.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOption.kt index f14711b..4041601 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/Ipv6HopByHopOption.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOption.kt @@ -1,4 +1,6 @@ -package com.jasonernst.knet.ip +package com.jasonernst.knet.ip.v6.extenions + +import com.jasonernst.knet.ip.IpType data class Ipv6HopByHopOption( override val nextHeader: UByte = IpType.TCP.value, diff --git a/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt b/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt index f6f950b..1c3e2bb 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt @@ -2,8 +2,8 @@ package com.jasonernst.knet import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpType -import com.jasonernst.knet.ip.Ipv4Header -import com.jasonernst.knet.ip.Ipv6Header +import com.jasonernst.knet.ip.v4.Ipv4Header +import com.jasonernst.knet.ip.v6.Ipv6Header import com.jasonernst.knet.transport.tcp.TcpHeader import com.jasonernst.knet.transport.udp.UdpHeader import org.junit.jupiter.api.Assertions.assertEquals diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4HeaderTest.kt similarity index 96% rename from knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt rename to knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4HeaderTest.kt index 72701bd..37265cf 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv4HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4HeaderTest.kt @@ -1,12 +1,14 @@ -package com.jasonernst.knet.ip +package com.jasonernst.knet.ip.v4 import com.jasonernst.icmp_common.Checksum import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpHeader.Companion.IP4_VERSION -import com.jasonernst.knet.ip.Ipv4Header.Companion.IP4_MIN_FRAGMENT_PAYLOAD -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.ip.IpType +import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_MIN_FRAGMENT_PAYLOAD +import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_MIN_HEADER_LENGTH +import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_WORD_LENGTH +import com.jasonernst.knet.ip.v4.options.Ipv4OptionNoOperation import com.jasonernst.knet.transport.tcp.TcpHeader import com.jasonernst.knet.transport.tcp.options.TcpOptionEndOfOptionList import com.jasonernst.packetdumper.stringdumper.StringPacketDumper diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionTest.kt similarity index 96% rename from knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt rename to knet/src/test/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionTest.kt index 55bf385..e7aac5b 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionTest.kt @@ -1,7 +1,20 @@ -package com.jasonernst.knet.ip.options +package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException -import com.jasonernst.knet.ip.Ipv4Header +import com.jasonernst.knet.ip.v4.Ipv4Header +import com.jasonernst.knet.ip.v4.options.Ipv4Option +import com.jasonernst.knet.ip.v4.options.Ipv4OptionClassType +import com.jasonernst.knet.ip.v4.options.Ipv4OptionEndOfOptionList +import com.jasonernst.knet.ip.v4.options.Ipv4OptionInternetTimestamp +import com.jasonernst.knet.ip.v4.options.Ipv4OptionLooseSourceAndRecordRoute +import com.jasonernst.knet.ip.v4.options.Ipv4OptionNoOperation +import com.jasonernst.knet.ip.v4.options.Ipv4OptionRecordRoute +import com.jasonernst.knet.ip.v4.options.Ipv4OptionSecurity +import com.jasonernst.knet.ip.v4.options.Ipv4OptionSecurityType +import com.jasonernst.knet.ip.v4.options.Ipv4OptionStreamIdentifier +import com.jasonernst.knet.ip.v4.options.Ipv4OptionStrictSourceAndRecordRoute +import com.jasonernst.knet.ip.v4.options.Ipv4OptionType +import com.jasonernst.knet.ip.v4.options.Ipv4OptionUnknown import com.jasonernst.knet.transport.tcp.options.TcpOptionNoOperation import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv6HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt similarity index 91% rename from knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv6HeaderTest.kt rename to knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt index f923225..2486145 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/Ipv6HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt @@ -1,6 +1,9 @@ -package com.jasonernst.knet.ip +package com.jasonernst.knet.ip.v6 import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpHeader +import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader +import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOption import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue 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 cefed38..3e43daf 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,7 +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.ip.v4.options.Ipv4OptionNoOperation import com.jasonernst.knet.nextheader.NextHeader import com.jasonernst.knet.transport.tcp.TcpHeader import com.jasonernst.knet.transport.tcp.options.TcpOption From b5c31a9810e3df0dda82c89ece5ad3eb9f09cdb4 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Sep 2024 14:07:49 +0300 Subject: [PATCH 02/22] Added rough-ins for all the newly required files --- README.md | 2 ++ .../ip/v6/extenions/Ipv6Authentication.kt | 4 +++ .../ip/v6/extenions/Ipv6DestinationOptions.kt | 4 +++ .../v6/extenions/Ipv6EncapsulatingSecurity.kt | 4 +++ .../ip/v6/extenions/Ipv6ExtensionHeader.kt | 2 +- .../knet/ip/v6/extenions/Ipv6Fragment.kt | 4 +++ ...pByHopOption.kt => Ipv6HopByHopOptions.kt} | 6 ++-- .../knet/ip/v6/extenions/Ipv6Routing.kt | 4 +++ .../jasonernst/knet/ip/v6/Ipv6HeaderTest.kt | 28 +++++++++---------- 9 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt rename knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/{Ipv6HopByHopOption.kt => Ipv6HopByHopOptions.kt} (90%) create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt diff --git a/README.md b/README.md index 8fddea7..4f98d4b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Kotlin user-space network stack. This can be used for: - [ ] RFC 2474: https://datatracker.ietf.org/doc/html/rfc2474 - [x] IPv6: - [X] WiP: RFC 8200: https://datatracker.ietf.org/doc/html/rfc8200 + - [ ] RFC: 4302: https://datatracker.ietf.org/doc/html/rfc4302 + - [ ] RFC: 4303: https://datatracker.ietf.org/doc/html/rfc4303 - [x] ICMP (via https://github.com/compscidr/icmp) - [] TCP - [] UDP diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt new file mode 100644 index 0000000..861c0d0 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt @@ -0,0 +1,4 @@ +package com.jasonernst.knet.ip.v6.extenions + +class Ipv6Authentication { +} \ No newline at end of file diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt new file mode 100644 index 0000000..64b0219 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt @@ -0,0 +1,4 @@ +package com.jasonernst.knet.ip.v6.extenions + +class Ipv6DestinationOptions { +} \ No newline at end of file diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt new file mode 100644 index 0000000..c658882 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt @@ -0,0 +1,4 @@ +package com.jasonernst.knet.ip.v6.extenions + +class Ipv6EncapsulatingSecurity { +} \ No newline at end of file diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index 4676e7e..2c883aa 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -68,7 +68,7 @@ open class Ipv6ExtensionHeader( when (currentHeader) { IpType.HOPOPT -> { - extensionList.add(Ipv6HopByHopOption(nextHeader, length, data)) + extensionList.add(Ipv6HopByHopOptions(nextHeader, length, data)) } else -> { throw IllegalArgumentException("Unsupported IPv6 extension header: $currentHeader") diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt new file mode 100644 index 0000000..7125b5d --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt @@ -0,0 +1,4 @@ +package com.jasonernst.knet.ip.v6.extenions + +class Ipv6Fragment { +} \ No newline at end of file diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOption.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt similarity index 90% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOption.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt index 4041601..6212451 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOption.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt @@ -2,20 +2,20 @@ package com.jasonernst.knet.ip.v6.extenions import com.jasonernst.knet.ip.IpType -data class Ipv6HopByHopOption( +data class Ipv6HopByHopOptions( override val nextHeader: UByte = IpType.TCP.value, override val length: UByte = 0u, override val data: ByteArray = ByteArray(0), ) : Ipv6ExtensionHeader(nextHeader, length, data) { companion object { - private val logger = org.slf4j.LoggerFactory.getLogger(Ipv6HopByHopOption::class.java) + private val logger = org.slf4j.LoggerFactory.getLogger(Ipv6HopByHopOptions::class.java) } override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as Ipv6HopByHopOption + other as Ipv6HopByHopOptions if (nextHeader != other.nextHeader) return false if (length != other.length) return false diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt new file mode 100644 index 0000000..675597f --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt @@ -0,0 +1,4 @@ +package com.jasonernst.knet.ip.v6.extenions + +class Ipv6Routing { +} \ No newline at end of file diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt index 2486145..bf5a110 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt @@ -3,7 +3,7 @@ package com.jasonernst.knet.ip.v6 import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader -import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOption +import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -43,7 +43,7 @@ class Ipv6HeaderTest { @Test fun extensionHeaderTest() { - val ipv6Header = Ipv6Header(extensionHeaders = listOf(Ipv6HopByHopOption())) + val ipv6Header = Ipv6Header(extensionHeaders = listOf(Ipv6HopByHopOptions())) val stream = ByteBuffer.wrap(ipv6Header.toByteArray()) val parsedHeader = IpHeader.fromStream(stream) assertEquals(ipv6Header, parsedHeader) @@ -51,25 +51,25 @@ class Ipv6HeaderTest { @Test fun hopByHopHashCode() { - val map: MutableMap = mutableMapOf() - val ipv6HopByHopOption = Ipv6HopByHopOption() - map[ipv6HopByHopOption] = 1 - assertTrue(map.containsKey(ipv6HopByHopOption)) + val map: MutableMap = mutableMapOf() + val ipv6HopByHopOptions = Ipv6HopByHopOptions() + map[ipv6HopByHopOptions] = 1 + assertTrue(map.containsKey(ipv6HopByHopOptions)) } @Test fun notEquals() { - val ipv6HopByHopOption = Ipv6HopByHopOption() + val ipv6HopByHopOptions = Ipv6HopByHopOptions() val otherOption = Ipv6ExtensionHeader(0u, 0u, ByteArray(0)) - assertFalse(ipv6HopByHopOption == otherOption) + assertFalse(ipv6HopByHopOptions == otherOption) - val ipv6HopByHopOption2 = Ipv6HopByHopOption(nextHeader = 0u) - assertFalse(ipv6HopByHopOption == ipv6HopByHopOption2) + val ipv6HopByHopOptions2 = Ipv6HopByHopOptions(nextHeader = 0u) + assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions2) - val ipv6HopByHopOption3 = Ipv6HopByHopOption(length = 5u) - assertFalse(ipv6HopByHopOption == ipv6HopByHopOption3) + val ipv6HopByHopOptions3 = Ipv6HopByHopOptions(length = 5u) + assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions3) - val ipv6HopByHopOption4 = Ipv6HopByHopOption(data = ByteArray(1)) - assertFalse(ipv6HopByHopOption == ipv6HopByHopOption4) + val ipv6HopByHopOptions4 = Ipv6HopByHopOptions(data = ByteArray(1)) + assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions4) } } From 5ca798652f93fc653f0c09679523f7f2b622ff3b Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Sep 2024 14:21:01 +0300 Subject: [PATCH 03/22] Added some rfc links and more documentation --- README.md | 2 ++ .../knet/ip/v6/extenions/Ipv6Authentication.kt | 3 +++ .../ip/v6/extenions/Ipv6EncapsulatingSecurity.kt | 3 +++ .../knet/ip/v6/extenions/Ipv6ExtensionHeader.kt | 12 ++++++++++++ 4 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 4f98d4b..c639c43 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Kotlin user-space network stack. This can be used for: - [ ] RFC 2474: https://datatracker.ietf.org/doc/html/rfc2474 - [x] IPv6: - [X] WiP: RFC 8200: https://datatracker.ietf.org/doc/html/rfc8200 + - [ ] RFC 6564: https://www.rfc-editor.org/rfc/rfc6564 + - [ ] RFC 7045: https://www.rfc-editor.org/rfc/rfc7045.html - [ ] RFC: 4302: https://datatracker.ietf.org/doc/html/rfc4302 - [ ] RFC: 4303: https://datatracker.ietf.org/doc/html/rfc4303 - [x] ICMP (via https://github.com/compscidr/icmp) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt index 861c0d0..7cb78d6 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt @@ -1,4 +1,7 @@ package com.jasonernst.knet.ip.v6.extenions +/** + * https://datatracker.ietf.org/doc/html/rfc4302 + */ class Ipv6Authentication { } \ No newline at end of file diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt index c658882..9da8dc1 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt @@ -1,4 +1,7 @@ package com.jasonernst.knet.ip.v6.extenions +/** + * https://datatracker.ietf.org/doc/html/rfc4303 + */ class Ipv6EncapsulatingSecurity { } \ No newline at end of file diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index 2c883aa..5d7dbd7 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -9,8 +9,20 @@ import java.nio.ByteOrder * Defines a type-length-value (TLV) extension header for IPv6 packets. Note that not all of the * well-defined Ipv6 extension headers support this. * + * https://datatracker.ietf.org/doc/html/rfc8200#section-4.1 * https://www.rfc-editor.org/rfc/rfc6564#page-4 * https://www.rfc-editor.org/rfc/rfc7045.html + * + * Suggested extension header order: + * IPv6 header + * Hop-by-Hop Options header + * Destination Options header (note 1) + * Routing header + * Fragment header + * Authentication header (note 2) + * Encapsulating Security Payload header (note 2) + * Destination Options header (note 3) + * Upper-Layer header */ open class Ipv6ExtensionHeader( open val nextHeader: UByte, From fc4230b7c29e25fd12e690d389fae21462e1993b Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Sep 2024 14:22:49 +0300 Subject: [PATCH 04/22] Added notes --- .../knet/ip/v6/extenions/Ipv6ExtensionHeader.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index 5d7dbd7..bf03e36 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -23,6 +23,17 @@ import java.nio.ByteOrder * Encapsulating Security Payload header (note 2) * Destination Options header (note 3) * Upper-Layer header + * + * note 1: for options to be processed by the first destination that + * appears in the IPv6 Destination Address field plus + * subsequent destinations listed in the Routing header. + * + * note 2: additional recommendations regarding the relative order of + * the Authentication and Encapsulating Security Payload + * headers are given in [RFC4303]. + * + * note 3: for options to be processed only by the final destination + * of the packet. */ open class Ipv6ExtensionHeader( open val nextHeader: UByte, From 9c21c7eda011c5e01d8777b9296ef78b01c3349a Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Sep 2024 14:23:56 +0300 Subject: [PATCH 05/22] added further docs from the rfc --- .../ip/v6/extenions/Ipv6ExtensionHeader.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index bf03e36..29648b1 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -34,6 +34,26 @@ import java.nio.ByteOrder * * note 3: for options to be processed only by the final destination * of the packet. + * + * Each extension header should occur at most once, except for the + * Destination Options header, which should occur at most twice (once + * before a Routing header and once before the upper-layer header). + * + * If the upper-layer header is another IPv6 header (in the case of IPv6 + * being tunneled over or encapsulated in IPv6), it may be followed by + * its own extension headers, which are separately subject to the same + * ordering recommendations. + * + * If and when other extension headers are defined, their ordering + * constraints relative to the above listed headers must be specified. + * + * IPv6 nodes must accept and attempt to process extension headers in + * any order and occurring any number of times in the same packet, + * except for the Hop-by-Hop Options header, which is restricted to + * appear immediately after an IPv6 header only. Nonetheless, it is + * strongly advised that sources of IPv6 packets adhere to the above + * recommended order until and unless subsequent specifications revise + * that recommendation. */ open class Ipv6ExtensionHeader( open val nextHeader: UByte, From 486875e8bd2d37a7c8db4a6302998deaf97db86a Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 25 Sep 2024 13:33:08 +0000 Subject: [PATCH 06/22] Wip ipv6 extension headers --- .../knet/ip/v4/options/Ipv4Option.kt | 24 +-- .../com/jasonernst/knet/ip/v6/Ipv6Header.kt | 18 +- .../ip/v6/extenions/Ipv6Authentication.kt | 19 +- .../ip/v6/extenions/Ipv6DestinationOptions.kt | 38 +++- .../v6/extenions/Ipv6EncapsulatingSecurity.kt | 7 - .../Ipv6EncapsulatingSecurityPayload.kt | 30 +++ .../ip/v6/extenions/Ipv6ExtensionHeader.kt | 59 ++++-- .../knet/ip/v6/extenions/Ipv6Fragment.kt | 38 +++- .../ip/v6/extenions/Ipv6HopByHopOptions.kt | 86 +++++++-- .../knet/ip/v6/extenions/Ipv6Routing.kt | 101 +++++++++- .../knet/ip/v6/extenions/Ipv6Tlv.kt | 175 ++++++++++++++++++ .../type/Ipv6DestinationHopByHopType.kt | 44 +++++ .../ip/v6/extenions/type/Ipv6RoutingType.kt | 24 +++ .../knet/ip/v4/options/Ipv4OptionTest.kt | 13 -- .../jasonernst/knet/ip/v6/Ipv6HeaderTest.kt | 11 +- 15 files changed, 602 insertions(+), 85 deletions(-) delete mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6DestinationHopByHopType.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6RoutingType.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4Option.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4Option.kt index 949b9d1..1e495fb 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4Option.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4Option.kt @@ -79,8 +79,8 @@ abstract class Ipv4Option( stream, isCopied, optionClass, - length - ) + length, + ), ) } Ipv4OptionType.LooseSourceRouting.kind -> { @@ -89,8 +89,8 @@ abstract class Ipv4Option( stream, isCopied, optionClass, - length - ) + length, + ), ) } Ipv4OptionType.StrictSourceRouting.kind -> { @@ -99,8 +99,8 @@ abstract class Ipv4Option( stream, isCopied, optionClass, - length - ) + length, + ), ) } Ipv4OptionType.RecordRoute.kind -> { @@ -109,8 +109,8 @@ abstract class Ipv4Option( stream, isCopied, optionClass, - length - ) + length, + ), ) } Ipv4OptionType.StreamId.kind -> { @@ -119,8 +119,8 @@ abstract class Ipv4Option( stream, isCopied, optionClass, - length - ) + length, + ), ) } Ipv4OptionType.TimeStamp.kind -> { @@ -129,8 +129,8 @@ abstract class Ipv4Option( stream, isCopied, optionClass, - length - ) + length, + ), ) } else -> { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt index c40c621..962841c 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt @@ -75,9 +75,11 @@ data class Ipv6Header( val destinationBuffer = ByteArray(16) stream[destinationBuffer] val destinationAddress = Inet6Address.getByAddress(destinationBuffer) as Inet6Address - val extensionHeaders = Ipv6ExtensionHeader.fromStream(stream, - IpType.fromValue(protocol) - ) + val extensionHeaders = + Ipv6ExtensionHeader.fromStream( + stream, + IpType.fromValue(protocol), + ) return Ipv6Header( ipVersion, @@ -113,7 +115,15 @@ data class Ipv6Header( return buffer.array() } - override fun getHeaderLength(): UShort = (IP6_HEADER_SIZE + (extensionHeaders.sumOf { it.getExtensionLength() }).toUShort()).toUShort() + override fun getHeaderLength(): UShort = + ( + IP6_HEADER_SIZE + + ( + extensionHeaders.sumOf { + it.getExtensionLengthInBytes() + } + ).toUShort() + ).toUShort() override fun getTotalLength(): UShort = (getHeaderLength() + payloadLength).toUShort() diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt index 7cb78d6..1274da9 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt @@ -1,7 +1,22 @@ package com.jasonernst.knet.ip.v6.extenions +import java.nio.ByteBuffer + /** * https://datatracker.ietf.org/doc/html/rfc4302 */ -class Ipv6Authentication { -} \ No newline at end of file +class Ipv6Authentication( + override val nextHeader: UByte, + override val length: UByte, +) : Ipv6ExtensionHeader(nextHeader = nextHeader, length = length) { + companion object { + // nextheader, length, reserved, SPI, sequence number, ICV + const val MIN_LENGTH = 20 + + fun fromStream( + stream: ByteBuffer, + nextheader: UByte, + length: UByte, + ): Ipv6Authentication = Ipv6Authentication(nextheader, length) + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt index 64b0219..79a0cdb 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt @@ -1,4 +1,38 @@ package com.jasonernst.knet.ip.v6.extenions -class Ipv6DestinationOptions { -} \ No newline at end of file +import com.jasonernst.knet.ip.IpType +import java.nio.ByteBuffer +import java.nio.ByteOrder + +data class Ipv6DestinationOptions( + override val nextHeader: UByte = IpType.TCP.value, + override val length: UByte = 0u, + val optionData: List = emptyList(), +) : Ipv6ExtensionHeader(nextHeader, length) { + companion object { + const val MIN_LENGTH = 2 // next header and length with no actual option data + + fun fromStream( + stream: ByteBuffer, + nextheader: UByte, + length: UByte, + ): Ipv6DestinationOptions { + val optionData = mutableListOf() + val start = stream.position() + while (stream.position() - start < length.toInt()) { + optionData.add(Ipv6Tlv.fromStream(stream)) + } + return Ipv6DestinationOptions(nextheader, length, optionData) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(MIN_LENGTH + optionData.sumOf { it.toByteArray().size }) + buffer.order(order) + buffer.put(super.toByteArray(order)) + optionData.forEach { + buffer.put(it.toByteArray()) + } + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt deleted file mode 100644 index 9da8dc1..0000000 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurity.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.jasonernst.knet.ip.v6.extenions - -/** - * https://datatracker.ietf.org/doc/html/rfc4303 - */ -class Ipv6EncapsulatingSecurity { -} \ No newline at end of file diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt new file mode 100644 index 0000000..505a4cb --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt @@ -0,0 +1,30 @@ +package com.jasonernst.knet.ip.v6.extenions + +import com.jasonernst.knet.ip.IpType +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * https://datatracker.ietf.org/doc/html/rfc4303 + */ +class Ipv6EncapsulatingSecurityPayload( + override val nextHeader: UByte = IpType.TCP.value, + override val length: UByte = MIN_LENGTH, +) : Ipv6ExtensionHeader(nextHeader = nextHeader, length = length) { + companion object { + const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data + + fun fromStream( + stream: ByteBuffer, + nextheader: UByte, + length: UByte, + ): Ipv6EncapsulatingSecurityPayload = Ipv6EncapsulatingSecurityPayload(nextheader, length) + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt()) + buffer.order(order) + buffer.put(super.toByteArray(order)) + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index 29648b1..e850880 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -57,19 +57,28 @@ import java.nio.ByteOrder */ open class Ipv6ExtensionHeader( open val nextHeader: UByte, - open val length: UByte, - open val data: ByteArray, + open val length: UByte, // measured in 64-bit / 8-octet units ) { - fun toByteArray(order: ByteOrder = ByteOrder.BIG_ENDIAN): ByteArray { - val buffer = ByteBuffer.allocate(2 + data.size * 8) + /** + * This should be called by the subclass to serialize the extension header to a byte array. + * They can serialize their own data. + */ + open fun toByteArray(order: ByteOrder = ByteOrder.BIG_ENDIAN): ByteArray { + val buffer = ByteBuffer.allocate(2) buffer.order(order) buffer.put(nextHeader.toByte()) buffer.put(length.toByte()) - buffer.put(data) return buffer.array() } - fun getExtensionLength(): Int = 2 + data.size * 8 + /** + * Returns the actual length in bytes of the extension header + * because the length field measures in 8-octet units. + * + * This just simply multiplies it by 8, so it would include + * zero padding. + */ + fun getExtensionLengthInBytes(): Int = (length * 8u).toInt() companion object { /** @@ -81,14 +90,15 @@ open class Ipv6ExtensionHeader( * Authentication * Encapsulating Security Payload */ - val requiredExtensionHeaders = listOf( - IpType.HOPOPT, - IpType.IPV6_FRAG, - IpType.IPV6_OPTS, - IpType.IPV6_ROUTE, - IpType.AH, - IpType.ESP - ) + val requiredExtensionHeaders = + listOf( + IpType.HOPOPT, + IpType.IPV6_FRAG, + IpType.IPV6_OPTS, + IpType.IPV6_ROUTE, + IpType.AH, + IpType.ESP, + ) /** * This will continue to process IPv6 extension headers until the nextheader is not one, ie) @@ -106,14 +116,29 @@ open class Ipv6ExtensionHeader( } val nextHeader = stream.get().toUByte() val length = stream.get().toUByte() - val data = ByteArray(length.toInt() * 8) - stream.get(data) when (currentHeader) { IpType.HOPOPT -> { - extensionList.add(Ipv6HopByHopOptions(nextHeader, length, data)) + extensionList.add(Ipv6HopByHopOptions.fromStream(stream, nextHeader, length)) + } + IpType.IPV6_FRAG -> { + extensionList.add(Ipv6Fragment.fromStream(stream, nextHeader)) + } + IpType.IPV6_OPTS -> { + extensionList.add(Ipv6DestinationOptions.fromStream(stream, nextHeader, length)) + } + IpType.IPV6_ROUTE -> { + extensionList.add(Ipv6Routing.fromStream(stream, nextHeader, length)) + } + IpType.AH -> { + extensionList.add(Ipv6Authentication.fromStream(stream, nextHeader, length)) + } + IpType.ESP -> { + extensionList.add(Ipv6EncapsulatingSecurityPayload.fromStream(stream, nextHeader, length)) } else -> { + // todo: pass over the unsupported extension header leaving the stream position just + // beyond it, log a warning, and return an "Unsupported Ipv6 extension header" object throw IllegalArgumentException("Unsupported IPv6 extension header: $currentHeader") } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt index 7125b5d..0fbf452 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt @@ -1,4 +1,38 @@ package com.jasonernst.knet.ip.v6.extenions -class Ipv6Fragment { -} \ No newline at end of file +import com.jasonernst.knet.ip.IpType +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.experimental.and + +class Ipv6Fragment( + override val nextHeader: UByte = IpType.TCP.value, + override val length: UByte = MIN_LENGTH, + val fragmentOffset: UShort = 0u, + val moreFlag: Boolean = false, + val identification: UInt = 0u, +) : Ipv6ExtensionHeader(nextHeader = nextHeader, length = length) { + companion object { + const val MIN_LENGTH: UByte = 8u // next header, reserved, fragment offset, and identification + + fun fromStream( + stream: ByteBuffer, + nextheader: UByte, + ): Ipv6Fragment { + val fragmentOffsetRMByte = stream.getShort() + val fragmentOffset = ((fragmentOffsetRMByte and 0b111111111111100).toUInt() shr 3).toUShort() + val moreFlag = (fragmentOffsetRMByte and 0b1).toInt() == 1 + val identification = stream.getInt().toUInt() + return Ipv6Fragment(nextheader, MIN_LENGTH, fragmentOffset, moreFlag, identification) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt()) + buffer.order(order) + buffer.put(super.toByteArray(order)) + buffer.putShort((fragmentOffset.toInt() shl 3 or if (moreFlag) 1 else 0).toShort()) + buffer.putInt(identification.toInt()) + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt index 6212451..ac3ac2a 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt @@ -1,33 +1,79 @@ package com.jasonernst.knet.ip.v6.extenions import com.jasonernst.knet.ip.IpType +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.ceil +/** + * https://www.rfc-editor.org/rfc/rfc8200#page-13 + * + * The Hop-by-Hop Options header is used to carry optional information + * that may be examined and processed by every node along a packet's + * delivery path. The Hop-by-Hop Options header is identified by a Next + * Header value of 0 in the IPv6 header and has the following format: + * + * Next Header 8-bit selector. Identifies the type of header + * immediately following the Hop-by-Hop Options + * header. Uses the same values as the IPv4 + * Protocol field [IANA-PN] + * + * Hdr Ext Len 8-bit unsigned integer. Length of the + * Hop-by-Hop Options header in 8-octet units, + * not including the first 8 octets. + * + * Variable-length field, of length such that the + * complete Hop-by-Hop Options header is an + * integer multiple of 8 octets long. Contains + * one or more TLV-encoded options, as described + * in Section 4.2. + */ data class Ipv6HopByHopOptions( override val nextHeader: UByte = IpType.TCP.value, - override val length: UByte = 0u, - override val data: ByteArray = ByteArray(0), -) : Ipv6ExtensionHeader(nextHeader, length, data) { - companion object { - private val logger = org.slf4j.LoggerFactory.getLogger(Ipv6HopByHopOptions::class.java) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false + override val length: UByte = ceil((MIN_LENGTH.toDouble() + Ipv6Tlv().size()) / 8.0).toUInt().toUByte(), + val optionData: List = listOf(Ipv6Tlv()), +) : Ipv6ExtensionHeader(nextHeader, length) { + init { + // dummy check to make sure the length is a multiple of 8 + val optionLength = optionData.sumOf { it.size() } + val totalLength = optionLength + MIN_LENGTH.toInt() + if (totalLength % 8 != 0) { + throw IllegalArgumentException("Option data length + 2 must be a multiple of 8, but have $totalLength") + } - other as Ipv6HopByHopOptions + // dummy check to ensure length matches the option data + val octet8Lengths = ceil(totalLength.toDouble() / 8.0) + if (octet8Lengths != length.toDouble()) { + throw IllegalArgumentException( + "(Option data length + 2) / 8 must match the length field, have $octet8Lengths, expecting $length", + ) + } + } - if (nextHeader != other.nextHeader) return false - if (length != other.length) return false - if (!data.contentEquals(other.data)) return false + companion object { + const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data - return true + fun fromStream( + stream: ByteBuffer, + nextheader: UByte, + length: UByte, + ): Ipv6HopByHopOptions { + val optionData = mutableListOf() + val start = stream.position() + while (stream.position() - start < length.toInt()) { + optionData.add(Ipv6Tlv.fromStream(stream)) + } + return Ipv6HopByHopOptions(nextheader, length, optionData) + } } - override fun hashCode(): Int { - var result = nextHeader.hashCode() - result = 31 * result + length.hashCode() - result = 31 * result + data.contentHashCode() - return result + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(getExtensionLengthInBytes()) + buffer.order(order) + buffer.put(super.toByteArray(order)) + optionData.forEach { + buffer.put(it.toByteArray()) + } + return buffer.array() } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt index 675597f..c8f7148 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt @@ -1,4 +1,101 @@ package com.jasonernst.knet.ip.v6.extenions -class Ipv6Routing { -} \ No newline at end of file +import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v6.extenions.type.Ipv6RoutingType +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * https://www.rfc-editor.org/rfc/rfc8200#section-4.4 + * + * The Routing header is used by an IPv6 source to list one or more + * intermediate nodes to be "visited" on the way to a packet's + * destination. This function is very similar to IPv4's Loose Source + * and Record Route option. The Routing header is identified by a Next + * Header value of 43 in the immediately preceding header and has the + * following format: + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Next Header | Hdr Ext Len | Routing Type | Segments Left | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | + * . . + * . type-specific data . + * . . + * | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Next Header 8-bit selector. Identifies the type of header + * immediately following the Routing header. + * Uses the same values as the IPv4 Protocol + * field [IANA-PN]. + * + * Hdr Ext Len 8-bit unsigned integer. Length of the Routing + * header in 8-octet units, not including the + * first 8 octets. + * + * Routing Type 8-bit identifier of a particular Routing + * header variant. + * + * Segments Left 8-bit unsigned integer. Number of route + * segments remaining, i.e., number of explicitly + * listed intermediate nodes still to be visited + * before reaching the final destination. + * + * type-specific data Variable-length field, of format determined by + * the Routing Type, and of length such that the + * complete Routing header is an integer multiple + * of 8 octets long. + * + * If, while processing a received packet, a node encounters a Routing + * header with an unrecognized Routing Type value, the required behavior + * of the node depends on the value of the Segments Left field, as + * follows: + * + * If Segments Left is zero, the node must ignore the Routing header + * and proceed to process the next header in the packet, whose type + * is identified by the Next Header field in the Routing header. + * + * If Segments Left is non-zero, the node must discard the packet and + * send an ICMP Parameter Problem, Code 0, message to the packet's + * Source Address, pointing to the unrecognized Routing Type. + * + * If, after processing a Routing header of a received packet, an + * intermediate node determines that the packet is to be forwarded onto + * a link whose link MTU is less than the size of the packet, the node + * must discard the packet and send an ICMP Packet Too Big message to + * the packet's Source Address. + * + * The currently defined IPv6 Routing Headers and their status can be + * found at [IANA-RH]. Allocation guidelines for IPv6 Routing Headers + * can be found in [RFC5871]. + */ +data class Ipv6Routing( + override val nextHeader: UByte = IpType.TCP.value, + override val length: UByte = MIN_LENGTH, + val routingType: Ipv6RoutingType, + val segmentsLeft: UByte, +) : Ipv6ExtensionHeader(nextHeader, length) { + companion object { + const val MIN_LENGTH: UByte = 4u // next header, length, routing type, and segments left + + fun fromStream( + stream: ByteBuffer, + nextheader: UByte, + length: UByte, + ): Ipv6Routing { + val routingType = Ipv6RoutingType.fromKind(stream.get().toUByte()) + val segmentsLeft = stream.get().toUByte() + return Ipv6Routing(nextheader, length, routingType, segmentsLeft) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt()) + buffer.order(order) + buffer.put(super.toByteArray(order)) + buffer.put(routingType.kind.toByte()) + buffer.put(segmentsLeft.toByte()) + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt new file mode 100644 index 0000000..bc8ba52 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt @@ -0,0 +1,175 @@ +package com.jasonernst.knet.ip.v6.extenions + +import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.v6.extenions.type.Ipv6DestinationHopByHopType +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * + * Two of the currently defined extension headers specified in this + * document -- the Hop-by-Hop Options header and the Destination Options + * header -- carry a variable number of "options" that are type-length- + * value (TLV) encoded in the following format: + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + * | Option Type | Opt Data Len | Option Data + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + * + * Option Type 8-bit identifier of the type of option. + * + * Opt Data Len 8-bit unsigned integer. Length of the Option + * Data field of this option, in octets. + * + * Option Data Variable-length field. Option-Type-specific + * data. + * + * The sequence of options within a header must be processed strictly in + * the order they appear in the header; a receiver must not, for + * example, scan through the header looking for a particular kind of + * option and process that option prior to processing all preceding + * ones. + * + * The Option Type identifiers are internally encoded such that their + * highest-order 2 bits specify the action that must be taken if the + * processing IPv6 node does not recognize the Option Type: + * + * 00 - skip over this option and continue processing the header. + * + * 01 - discard the packet. + * + * 10 - discard the packet and, regardless of whether or not the + * packet's Destination Address was a multicast address, send an + * ICMP Parameter Problem, Code 2, message to the packet's + * Source Address, pointing to the unrecognized Option Type. + * + * 11 - discard the packet and, only if the packet's Destination + * Address was not a multicast address, send an ICMP Parameter + * Problem, Code 2, message to the packet's Source Address, + * pointing to the unrecognized Option Type. + * + * The third-highest-order bit of the Option Type specifies whether or + * not the Option Data of that option can change en route to the + * packet's final destination. When an Authentication header is present + * in the packet, for any option whose data may change en route, its + * entire Option Data field must be treated as zero-valued octets when + * computing or verifying the packet's authenticating value. + * + * 0 - Option Data does not change en route + * + * 1 - Option Data may change en route + * + * The three high-order bits described above are to be treated as part + * of the Option Type, not independent of the Option Type. That is, a + * particular option is identified by a full 8-bit Option Type, not just + * the low-order 5 bits of an Option Type. + * + * The same Option Type numbering space is used for both the Hop-by-Hop + * Options header and the Destination Options header. However, the + * specification of a particular option may restrict its use to only one + * of those two headers. + * + * Individual options may have specific alignment requirements, to + * ensure that multi-octet values within Option Data fields fall on + * natural boundaries. The alignment requirement of an option is + * specified using the notation xn+y, meaning the Option Type must + * appear at an integer multiple of x octets from the start of the + * header, plus y octets. For example: + * + * 2n means any 2-octet offset from the start of the header. + * 8n+2 means any 8-octet offset from the start of the header, plus + * 2 octets. + * + * There are two padding options that are used when necessary to align + * subsequent options and to pad out the containing header to a multiple + * of 8 octets in length. These padding options must be recognized by + * all IPv6 implementations: + * + * Pad1 option (alignment requirement: none) + * + * +-+-+-+-+-+-+-+-+ + * | 0 | + * +-+-+-+-+-+-+-+-+ + * + * NOTE! the format of the Pad1 option is a special case -- it does + * not have length and value fields. + * + * The Pad1 option is used to insert 1 octet of padding into the + * Options area of a header. If more than one octet of padding is + * required, the PadN option, described next, should be used, rather + * than multiple Pad1 options. + * + * PadN option (alignment requirement: none) + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + * | 1 | Opt Data Len | Option Data + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + * + * The PadN option is used to insert two or more octets of padding + * into the Options area of a header. For N octets of padding, the + * Opt Data Len field contains the value N-2, and the Option Data + * consists of N-2 zero-valued octets. + * + * Appendix A contains formatting guidelines for designing new options. + */ +data class Ipv6Tlv( + val optionType: Ipv6DestinationHopByHopType = Ipv6DestinationHopByHopType.PadN, + val optionDataLength: UByte = 4u, + val optionData: ByteArray = ByteArray(4), +) { + init { + if (optionDataLength.toInt() != optionData.size) { + logger.warn("Option data length does not match option data size") + } + } + + companion object { + const val MIN_TLV_LENGTH = 2 + private val logger = org.slf4j.LoggerFactory.getLogger(Ipv6Tlv::class.java) + + fun fromStream(stream: ByteBuffer): Ipv6Tlv { + if (stream.remaining() < MIN_TLV_LENGTH) { + throw PacketTooShortException("Stream must have at least 2 bytes remaining to parse Ipv6Tlv") + } + val optionType = Ipv6DestinationHopByHopType.fromKind(stream.get().toUByte()) + val optionDataLength = stream.get().toUByte() + if (stream.remaining() < optionDataLength.toInt()) { + throw PacketTooShortException("Stream must have at least $optionDataLength bytes remaining to parse Ipv6Tlv") + } + val optionData = ByteArray(optionDataLength.toInt()) + stream.get(optionData) + return Ipv6Tlv(optionType, optionDataLength, optionData) + } + } + + fun size(): Int = MIN_TLV_LENGTH + optionData.size + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Ipv6Tlv + + if (optionType != other.optionType) return false + if (optionDataLength != other.optionDataLength) return false + if (!optionData.contentEquals(other.optionData)) return false + + return true + } + + override fun hashCode(): Int { + var result = optionType.hashCode() + result = 31 * result + optionDataLength.hashCode() + result = 31 * result + optionData.contentHashCode() + return result + } + + fun toByteArray(order: ByteOrder = ByteOrder.BIG_ENDIAN): ByteArray { + val buffer = ByteBuffer.allocate(2 + optionData.size) + buffer.order(order) + buffer.put(optionType.kind.toByte()) + buffer.put(optionDataLength.toByte()) + buffer.put(optionData) + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6DestinationHopByHopType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6DestinationHopByHopType.kt new file mode 100644 index 0000000..5d79078 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6DestinationHopByHopType.kt @@ -0,0 +1,44 @@ +package com.jasonernst.knet.ip.v6.extenions.type + +/** + * https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#ipv6-parameters-2 + */ +enum class Ipv6DestinationHopByHopType( + val kind: UByte, +) { + Pad1(0u), + PadN(1u), + JumboPayload(0xC2u), + RPLOption(0x23u), + RPLOptionDeprecated(0x63u), + TunnelEncapsulationLimit(0x04u), + RouterAlert(0x05u), + QuickStart(0x26u), + Calipso(0x07u), + SMF_DPD(0x08u), + HomeAddress(0xC9u), + EndpointIdentificationDeprecated(0x8Au), + ILNPNonce(0x8Bu), + LineIdentificationOption(0x8Cu), + Deprecated(0x4Du), + MPLOption(0x6Du), + IP_DFF(0xEEu), + PDM(0x0Fu), + MinimumPathMTUHopByHopOption(0x30u), + IOAMDestinationOptionAndIOAMHopByHopOption(0x11u), + IOAMDestinationOptionAndIOAMHopByHopOption2(0x31u), + AltMark(0x12u), + RFC3692StyleExperiment1(0x1Eu), + RFC3692StyleExperiment2(0x3Eu), + RFC3692StyleExperiment3(0x5Eu), + RFC3692StyleExperiment4(0x7Eu), + RFC3692StyleExperiment5(0x9Eu), + RFC3692StyleExperiment6(0xBEu), + RFC3692StyleExperiment7(0xDEu), + RFC3692StyleExperiment8(0xFEu), + ; + + companion object { + fun fromKind(kind: UByte) = entries.first { it.kind == kind } + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6RoutingType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6RoutingType.kt new file mode 100644 index 0000000..be61ae6 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6RoutingType.kt @@ -0,0 +1,24 @@ +package com.jasonernst.knet.ip.v6.extenions.type + +/** + * https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#ipv6-parameters-3 + */ +enum class Ipv6RoutingType( + val kind: UByte, +) { + SourceRouteDeprecated(0u), + NimrodDeprecated(1u), + Type2RoutingHeader(2u), + RplSourceRoute(3u), + SegmentRouting(4u), + CRH16(5u), + CRH32(6u), + RFC3692StyleExperiment1(253u), + RFC3692StyleExperiment2(254u), + Reserved(255u), + ; + + companion object { + fun fromKind(kind: UByte) = entries.first { it.kind == kind } + } +} diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionTest.kt index e7aac5b..e73d8a9 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/options/Ipv4OptionTest.kt @@ -2,19 +2,6 @@ package com.jasonernst.knet.ip.v4.options import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.v4.Ipv4Header -import com.jasonernst.knet.ip.v4.options.Ipv4Option -import com.jasonernst.knet.ip.v4.options.Ipv4OptionClassType -import com.jasonernst.knet.ip.v4.options.Ipv4OptionEndOfOptionList -import com.jasonernst.knet.ip.v4.options.Ipv4OptionInternetTimestamp -import com.jasonernst.knet.ip.v4.options.Ipv4OptionLooseSourceAndRecordRoute -import com.jasonernst.knet.ip.v4.options.Ipv4OptionNoOperation -import com.jasonernst.knet.ip.v4.options.Ipv4OptionRecordRoute -import com.jasonernst.knet.ip.v4.options.Ipv4OptionSecurity -import com.jasonernst.knet.ip.v4.options.Ipv4OptionSecurityType -import com.jasonernst.knet.ip.v4.options.Ipv4OptionStreamIdentifier -import com.jasonernst.knet.ip.v4.options.Ipv4OptionStrictSourceAndRecordRoute -import com.jasonernst.knet.ip.v4.options.Ipv4OptionType -import com.jasonernst.knet.ip.v4.options.Ipv4OptionUnknown import com.jasonernst.knet.transport.tcp.options.TcpOptionNoOperation import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt index bf5a110..de02470 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt @@ -4,6 +4,8 @@ import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions +import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv +import com.jasonernst.knet.ip.v6.extenions.type.Ipv6DestinationHopByHopType import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -60,16 +62,17 @@ class Ipv6HeaderTest { @Test fun notEquals() { val ipv6HopByHopOptions = Ipv6HopByHopOptions() - val otherOption = Ipv6ExtensionHeader(0u, 0u, ByteArray(0)) + val otherOption = Ipv6ExtensionHeader(0u, 0u) assertFalse(ipv6HopByHopOptions == otherOption) val ipv6HopByHopOptions2 = Ipv6HopByHopOptions(nextHeader = 0u) assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions2) - val ipv6HopByHopOptions3 = Ipv6HopByHopOptions(length = 5u) + val ipv6HopByHopOptions3 = + Ipv6HopByHopOptions(length = 2u, optionData = listOf(Ipv6Tlv(Ipv6DestinationHopByHopType.PadN, 12u, ByteArray(12)))) assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions3) - val ipv6HopByHopOptions4 = Ipv6HopByHopOptions(data = ByteArray(1)) - assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions4) +// val ipv6HopByHopOptions4 = Ipv6HopByHopOptions() +// assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions4) } } From d85306ce10d4c64aa340973a7867f2fc5b9e54c2 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 25 Sep 2024 15:14:41 +0000 Subject: [PATCH 07/22] added ipv6tlv tests --- .../knet/ip/v6/extenions/Ipv6Tlv.kt | 4 +- .../knet/ip/v6/extensions/Ipv6TlvTest.kt | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt index bc8ba52..75d0dbf 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt @@ -119,7 +119,9 @@ data class Ipv6Tlv( ) { init { if (optionDataLength.toInt() != optionData.size) { - logger.warn("Option data length does not match option data size") + throw IllegalArgumentException( + "Option data length does not match option data size, have ${optionData.size} but expected ${optionDataLength.toInt()}", + ) } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt new file mode 100644 index 0000000..967f84a --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt @@ -0,0 +1,53 @@ +package com.jasonernst.knet.ip.v6.extensions + +import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv +import com.jasonernst.knet.ip.v6.extenions.type.Ipv6DestinationHopByHopType +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.nio.ByteBuffer + +class Ipv6TlvTest { + @Test fun optionDataLengthMismatch() { + assertThrows { + Ipv6Tlv(Ipv6DestinationHopByHopType.Pad1, 0u, byteArrayOf(0, 2)) + } + } + + @Test fun equalsTest() { + val tlv1 = Ipv6Tlv(Ipv6DestinationHopByHopType.Pad1, 1u, byteArrayOf(0)) + val tlv2 = Ipv6Tlv(Ipv6DestinationHopByHopType.Pad1, 1u, byteArrayOf(0)) + assertEquals(tlv1, tlv2) + assertEquals(tlv1.optionType, tlv2.optionType) + assertEquals(tlv1.optionDataLength, tlv2.optionDataLength) + assertArrayEquals(tlv1.optionData, tlv2.optionData) + + assertNotEquals(tlv1, null) + assertNotEquals(tlv1, Any()) + assertEquals(tlv1, tlv1) + + val tlv3 = Ipv6Tlv(Ipv6DestinationHopByHopType.PadN, 1u, byteArrayOf(0)) + assertNotEquals(tlv1, tlv3) + + val tlv4 = Ipv6Tlv(Ipv6DestinationHopByHopType.Pad1, 2u, byteArrayOf(0, 0)) + assertNotEquals(tlv1, tlv4) + + val tlv5 = Ipv6Tlv(Ipv6DestinationHopByHopType.Pad1, 1u, byteArrayOf(1)) + assertNotEquals(tlv1, tlv5) + } + + @Test fun tooShort() { + assertThrows { + Ipv6Tlv.fromStream(ByteBuffer.allocate(0)) + } + + assertThrows { + val stream = ByteBuffer.wrap(Ipv6Tlv().toByteArray()) + stream.limit(stream.limit() - 1) + Ipv6Tlv.fromStream(stream) + } + } +} From b688823140e3fe176699ca3c7d3173e0a592027d Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 25 Sep 2024 16:06:04 +0000 Subject: [PATCH 08/22] full coverage of ipv6hopbyhop --- .../ip/v6/extenions/Ipv6HopByHopOptions.kt | 9 ++- .../jasonernst/knet/ip/v6/Ipv6HeaderTest.kt | 30 ---------- .../knet/ip/v6/extensions/Ipv6HopByHopTest.kt | 58 +++++++++++++++++++ 3 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt index ac3ac2a..d344df0 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt @@ -1,6 +1,7 @@ package com.jasonernst.knet.ip.v6.extenions import com.jasonernst.knet.ip.IpType +import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder import kotlin.math.ceil @@ -51,8 +52,12 @@ data class Ipv6HopByHopOptions( } companion object { + private val logger = LoggerFactory.getLogger(Ipv6HopByHopOptions::class.java) const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data + /** + * Assumes that the stream has already had the nextHeader and length parsed from it + */ fun fromStream( stream: ByteBuffer, nextheader: UByte, @@ -61,7 +66,9 @@ data class Ipv6HopByHopOptions( val optionData = mutableListOf() val start = stream.position() while (stream.position() - start < length.toInt()) { - optionData.add(Ipv6Tlv.fromStream(stream)) + val nextTlv = Ipv6Tlv.fromStream(stream) + logger.debug("Parsed TLV: {}", nextTlv) + optionData.add(nextTlv) } return Ipv6HopByHopOptions(nextheader, length, optionData) } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt index de02470..1d80bcf 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt @@ -2,13 +2,8 @@ package com.jasonernst.knet.ip.v6 import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader -import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions -import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv -import com.jasonernst.knet.ip.v6.extenions.type.Ipv6DestinationHopByHopType import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.nio.ByteBuffer @@ -50,29 +45,4 @@ class Ipv6HeaderTest { val parsedHeader = IpHeader.fromStream(stream) assertEquals(ipv6Header, parsedHeader) } - - @Test - fun hopByHopHashCode() { - val map: MutableMap = mutableMapOf() - val ipv6HopByHopOptions = Ipv6HopByHopOptions() - map[ipv6HopByHopOptions] = 1 - assertTrue(map.containsKey(ipv6HopByHopOptions)) - } - - @Test - fun notEquals() { - val ipv6HopByHopOptions = Ipv6HopByHopOptions() - val otherOption = Ipv6ExtensionHeader(0u, 0u) - assertFalse(ipv6HopByHopOptions == otherOption) - - val ipv6HopByHopOptions2 = Ipv6HopByHopOptions(nextHeader = 0u) - assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions2) - - val ipv6HopByHopOptions3 = - Ipv6HopByHopOptions(length = 2u, optionData = listOf(Ipv6Tlv(Ipv6DestinationHopByHopType.PadN, 12u, ByteArray(12)))) - assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions3) - -// val ipv6HopByHopOptions4 = Ipv6HopByHopOptions() -// assertFalse(ipv6HopByHopOptions == ipv6HopByHopOptions4) - } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt new file mode 100644 index 0000000..9b25967 --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt @@ -0,0 +1,58 @@ +package com.jasonernst.knet.ip.v6.extensions + +import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions +import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv +import com.jasonernst.packetdumper.stringdumper.StringPacketDumper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer + +class Ipv6HopByHopTest { + @Test + fun lengthOptionsMismatch() { + assertThrows { + Ipv6HopByHopOptions( + optionData = + listOf( + Ipv6Tlv(), + Ipv6Tlv(), + ), + ) + } + + assertThrows { + Ipv6HopByHopOptions( + length = 3u, + optionData = + listOf( + Ipv6Tlv(), + ), + ) + } + } + + @Test fun optionDataTest() { + val optionData = + listOf( + Ipv6Tlv(), + ) + val hopByHopOptions = Ipv6HopByHopOptions(optionData = optionData) + assertEquals(hopByHopOptions.optionData, optionData) + } + + @Test fun toFromStream() { + val logger = LoggerFactory.getLogger(javaClass) + val hopByHopOptions = Ipv6HopByHopOptions() + val stream = ByteBuffer.wrap(hopByHopOptions.toByteArray()) + val stringPacketDumper = StringPacketDumper(logger) + stringPacketDumper.dumpBuffer(stream) + val parsedNextHeader = stream.get().toUByte() + assertEquals(hopByHopOptions.nextHeader, parsedNextHeader) + val parsedLength = stream.get().toUByte() + assertEquals(hopByHopOptions.length, parsedLength) + val parsedOptions = Ipv6HopByHopOptions.fromStream(stream, parsedNextHeader, parsedLength) + assertEquals(hopByHopOptions, parsedOptions) + } +} From 618a781705638a4abfb605697b61579fb687c41c Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 27 Sep 2024 10:02:40 +0000 Subject: [PATCH 09/22] Added ipv6 fragmentation and reassembly --- .../com/jasonernst/knet/ip/v6/Ipv6Header.kt | 196 +++++++++++++ .../ip/v6/extenions/Ipv6Authentication.kt | 5 +- .../ip/v6/extenions/Ipv6DestinationOptions.kt | 4 +- .../Ipv6EncapsulatingSecurityPayload.kt | 4 +- .../ip/v6/extenions/Ipv6ExtensionHeader.kt | 3 +- .../knet/ip/v6/extenions/Ipv6Fragment.kt | 274 +++++++++++++++++- .../ip/v6/extenions/Ipv6HopByHopOptions.kt | 4 +- .../knet/ip/v6/extenions/Ipv6Routing.kt | 4 +- .../knet/ip/v6/extensions/Ipv6FragmentTest.kt | 25 ++ 9 files changed, 502 insertions(+), 17 deletions(-) create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt index 962841c..62d3256 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt @@ -5,6 +5,8 @@ import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpHeader.Companion.IP6_VERSION import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader +import com.jasonernst.knet.ip.v6.extenions.Ipv6Fragment +import com.jasonernst.knet.nextheader.NextHeader import org.slf4j.LoggerFactory import java.net.Inet6Address import java.net.InetAddress @@ -37,6 +39,17 @@ data class Ipv6Header( private val logger = LoggerFactory.getLogger(Ipv6Header::class.java) private const val IP6_HEADER_SIZE: UShort = 40u // ipv6 header is not variable like ipv4 + // The Per-Fragment headers must consist of the IPv6 header plus any + // extension headers that must be processed by nodes en route to the + // destination, that is, all headers up to and including the Routing + // header if present + private val onRouteHeaders = + listOf( + IpType.HOPOPT, + IpType.IPV6_OPTS, + IpType.IPV6_ROUTE, + ) + fun fromStream(stream: ByteBuffer): Ipv6Header { val start = stream.position() @@ -93,6 +106,189 @@ data class Ipv6Header( extensionHeaders, ) } + + fun reassemble(fragments: List>): Triple { + if (fragments.isEmpty()) { + throw IllegalArgumentException("No fragments to reassemble") + } + + val extensionHeaders = mutableListOf() + for (extensionHeader in fragments[0].first.extensionHeaders) { + if (extensionHeader.type == IpType.IPV6_FRAG) { + continue + } + extensionHeaders.add(extensionHeader) + } + val payloadLength = + fragments.sumOf { + it.third.size + } + + extensionHeaders.sumOf { + it.getExtensionLengthInBytes() + } + fragments[0].second!!.getHeaderLength().toInt() + + val ipv6Header = + Ipv6Header( + version = fragments[0].first.version, + trafficClass = fragments[0].first.trafficClass, + flowLabel = fragments[0].first.flowLabel, + payloadLength = payloadLength.toUShort(), + protocol = fragments[0].first.protocol, + hopLimit = fragments[0].first.hopLimit, + sourceAddress = fragments[0].first.sourceAddress, + destinationAddress = fragments[0].first.destinationAddress, + extensionHeaders = extensionHeaders, + ) + + val payload = + fragments + .flatMap { + it.third.toList() + }.toByteArray() + + val nextHeader = fragments.first().second + + return Triple(ipv6Header, nextHeader!!, payload) + } + } + + /** + * Fragments this ipv6 header into smaller fragments. The fragments have: + * 1) an Ipv6 header with different sets of extension headers, depending on if its the first + * fragment or not + * 2) a next header that is either the next header in the original packet or null if it is a + * fragment + * 3) a payload that is a subset of the original payload + */ + fun fragment( + maxSize: UInt, + nextHeader: NextHeader, + payload: ByteArray, + ): List> { + if (maxSize.toInt() % 8 != 0) { + throw IllegalArgumentException("Max size must be a multiple of 8") + } + + val fragments = mutableListOf>() + + val perFragmentHeaders = mutableListOf() + + // need to figure out type because this could be a mix of extension and upper layer headers (tcp) + val nonPerFragmentExtensionHeaders = mutableListOf() + + // up to an including the routing header if they exist + for (extensionHeader in extensionHeaders) { + if (onRouteHeaders.contains(extensionHeader.type)) { + perFragmentHeaders.add(extensionHeader) + } else { + nonPerFragmentExtensionHeaders.add(extensionHeader) + } + } + + if (perFragmentHeaders.isNotEmpty()) { + perFragmentHeaders.last().nextHeader = IpType.IPV6_FRAG.value + } + + val perFragmentHeaderBytes = + perFragmentHeaders.sumOf { + it.getExtensionLengthInBytes() + } + val extAndUpperBytes = + nonPerFragmentExtensionHeaders.sumOf { + it.getExtensionLengthInBytes() + } + nextHeader.getHeaderLength().toInt() + + val fragmentHeaderNextHeader = + if (nonPerFragmentExtensionHeaders.isEmpty()) { + nextHeader.protocol + } else { + nonPerFragmentExtensionHeaders.first().type.value + } + + val firstFragmentHeader = + Ipv6Fragment( + nextHeader = fragmentHeaderNextHeader, + fragmentOffset = 0u, + moreFlag = true, + identification = Ipv6Fragment.globalIdentificationCounter++, + ) + + val firstHeaderExtensions = mutableListOf() + firstHeaderExtensions.addAll(perFragmentHeaders) + firstHeaderExtensions.add(firstFragmentHeader) + firstHeaderExtensions.addAll(nonPerFragmentExtensionHeaders) + + val firstFragment = + Ipv6Header( + version, + trafficClass, + flowLabel, + (maxSize - IP6_HEADER_SIZE).toUShort(), + if (nonPerFragmentExtensionHeaders.isEmpty()) { + protocol + } else { + IpType.IPV6_FRAG.value + }, + hopLimit, + sourceAddress, + destinationAddress, + firstHeaderExtensions, + ) + val firstPayloadBytes = maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt() - extAndUpperBytes.toUInt() + val firstPair = Triple(firstFragment, nextHeader, payload.sliceArray(0 until firstPayloadBytes.toInt())) + fragments.add(firstPair) + var payloadPosition = firstPayloadBytes.toInt() + + while (payloadPosition < payload.size) { + val nextPayloadBytes = + minOf( + (payload.size - payloadPosition).toUInt(), + maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt(), + ) + val moreFlag = nextPayloadBytes >= maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt() + + val nextFragment = + Ipv6Fragment( + nextHeader = fragmentHeaderNextHeader, + fragmentOffset = (payloadPosition / 8).toUShort(), + moreFlag = moreFlag, + identification = firstFragmentHeader.identification, + ) + + val nextHeaderExtensions = mutableListOf() + nextHeaderExtensions.addAll(perFragmentHeaders) + nextHeaderExtensions.add(nextFragment) + + val nextFragmentHeader = + Ipv6Header( + version, + trafficClass, + flowLabel, + (maxSize - IP6_HEADER_SIZE).toUShort(), + if (nonPerFragmentExtensionHeaders.isEmpty()) { + protocol + } else { + IpType.IPV6_FRAG.value + }, + hopLimit, + sourceAddress, + destinationAddress, + nextHeaderExtensions, + ) + + val nextPair = + Triple( + nextFragmentHeader, + null, + payload.sliceArray( + payloadPosition until (payloadPosition + nextPayloadBytes.toInt()), + ), + ) + fragments.add(nextPair) + payloadPosition += nextPayloadBytes.toInt() + } + + return fragments } override fun toByteArray(order: ByteOrder): ByteArray { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt index 1274da9..697605a 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt @@ -1,14 +1,15 @@ package com.jasonernst.knet.ip.v6.extenions +import com.jasonernst.knet.ip.IpType import java.nio.ByteBuffer /** * https://datatracker.ietf.org/doc/html/rfc4302 */ class Ipv6Authentication( - override val nextHeader: UByte, + override var nextHeader: UByte, override val length: UByte, -) : Ipv6ExtensionHeader(nextHeader = nextHeader, length = length) { +) : Ipv6ExtensionHeader(IpType.AH, nextHeader = nextHeader, length = length) { companion object { // nextheader, length, reserved, SPI, sequence number, ICV const val MIN_LENGTH = 20 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt index 79a0cdb..6da1672 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt @@ -5,10 +5,10 @@ import java.nio.ByteBuffer import java.nio.ByteOrder data class Ipv6DestinationOptions( - override val nextHeader: UByte = IpType.TCP.value, + override var nextHeader: UByte = IpType.TCP.value, override val length: UByte = 0u, val optionData: List = emptyList(), -) : Ipv6ExtensionHeader(nextHeader, length) { +) : Ipv6ExtensionHeader(IpType.IPV6_OPTS, nextHeader, length) { companion object { const val MIN_LENGTH = 2 // next header and length with no actual option data diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt index 505a4cb..4f6c603 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt @@ -8,9 +8,9 @@ import java.nio.ByteOrder * https://datatracker.ietf.org/doc/html/rfc4303 */ class Ipv6EncapsulatingSecurityPayload( - override val nextHeader: UByte = IpType.TCP.value, + override var nextHeader: UByte = IpType.TCP.value, override val length: UByte = MIN_LENGTH, -) : Ipv6ExtensionHeader(nextHeader = nextHeader, length = length) { +) : Ipv6ExtensionHeader(IpType.ESP, nextHeader = nextHeader, length = length) { companion object { const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index e850880..87b525e 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -56,7 +56,8 @@ import java.nio.ByteOrder * that recommendation. */ open class Ipv6ExtensionHeader( - open val nextHeader: UByte, + val type: IpType, + open var nextHeader: UByte, open val length: UByte, // measured in 64-bit / 8-octet units ) { /** diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt index 0fbf452..06953f6 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt @@ -5,15 +5,277 @@ import java.nio.ByteBuffer import java.nio.ByteOrder import kotlin.experimental.and +/** + * + * The Fragment header is used by an IPv6 source to send a packet larger + * than would fit in the path MTU to its destination. (Note: unlike + * IPv4, fragmentation in IPv6 is performed only by source nodes, not by + * routers along a packet's delivery path -- see Section 5.) The + * Fragment header is identified by a Next Header value of 44 in the + * immediately preceding header and has the following format: + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Next Header | Reserved | Fragment Offset |Res|M| + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Identification | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Next Header 8-bit selector. Identifies the initial header + * type of the Fragmentable Part of the original + * packet (defined below). Uses the same values + * as the IPv4 Protocol field [IANA-PN]. + * + * Reserved 8-bit reserved field. Initialized to zero for + * transmission; ignored on reception. + * + * Fragment Offset 13-bit unsigned integer. The offset, in + * 8-octet units, of the data following this + * header, relative to the start of the + * Fragmentable Part of the original packet. + * + * Res 2-bit reserved field. Initialized to zero for + * transmission; ignored on reception. + * + * M flag 1 = more fragments; 0 = last fragment. + * + * Identification 32 bits. See description below. + * + * In order to send a packet that is too large to fit in the MTU of the + * path to its destination, a source node may divide the packet into + * fragments and send each fragment as a separate packet, to be + * reassembled at the receiver. + * + * For every packet that is to be fragmented, the source node generates + * an Identification value. The Identification must be different than + * that of any other fragmented packet sent recently* with the same + * Source Address and Destination Address. If a Routing header is + * present, the Destination Address of concern is that of the final + * destination. + * + * "recently" means within the maximum likely lifetime of a + * packet, including transit time from source to destination and + * time spent awaiting reassembly with other fragments of the same + * packet. However, it is not required that a source node knows + * the maximum packet lifetime. Rather, it is assumed that the + * requirement can be met by implementing an algorithm that + * results in a low identification reuse frequency. Examples of + * algorithms that can meet this requirement are described in + * [RFC7739]. + * + * The initial, large, unfragmented packet is referred to as the + * "original packet", and it is considered to consist of three parts, as + * illustrated: + * + * original packet: + * + * +------------------+-------------------------+---//----------------+ + * | Per-Fragment | Extension & Upper-Layer | Fragmentable | + * | Headers | Headers | Part | + * +------------------+-------------------------+---//----------------+ + * + * The Per-Fragment headers must consist of the IPv6 header plus any + * extension headers that must be processed by nodes en route to the + * destination, that is, all headers up to and including the Routing + * header if present, else the Hop-by-Hop Options header if present, + * else no extension headers. + * + * The Extension headers are all other extension headers that are not + * included in the Per-Fragment headers part of the packet. For this + * purpose, the Encapsulating Security Payload (ESP) is not + * considered an extension header. The Upper-Layer header is the + * first upper-layer header that is not an IPv6 extension header. + * Examples of upper-layer headers include TCP, UDP, IPv4, IPv6, + * ICMPv6, and as noted ESP. + * + * The Fragmentable Part consists of the rest of the packet after the + * upper-layer header or after any header (i.e., initial IPv6 header + * or extension header) that contains a Next Header value of No Next + * Header. + * + * The Fragmentable Part of the original packet is divided into + * fragments. The lengths of the fragments must be chosen such that the + * resulting fragment packets fit within the MTU of the path to the + * packet's destination(s). Each complete fragment, except possibly the + * last ("rightmost") one, is an integer multiple of 8 octets long. + * + * The Identification value generated for the original + * packet. + * + * (3) Extension headers, if any, and the Upper-Layer header. These + * headers must be in the first fragment. Note: This restricts + * the size of the headers through the Upper-Layer header to the + * MTU of the path to the packet's destinations(s). + * + * (4) The first fragment. + * + * The subsequent fragment packets are composed of: + * + * (1) The Per-Fragment headers of the original packet, with the + * Payload Length of the original IPv6 header changed to contain + * the length of this fragment packet only (excluding the length + * of the IPv6 header itself), and the Next Header field of the + * last header of the Per-Fragment headers changed to 44. + * + * (2) A Fragment header containing: + * + * The Next Header value that identifies the first header + * after the Per-Fragment headers of the original packet. + * + * A Fragment Offset containing the offset of the fragment, + * in 8-octet units, relative to the start of the + * Fragmentable Part of the original packet. + * + * An M flag value of 0 if the fragment is the last + * ("rightmost") one, else an M flag value of 1. + * + * The Identification value generated for the original + * packet. + * + * (3) The fragment itself. + * + * Fragments must not be created that overlap with any other fragments + * created from the original packet. + * + * At the destination, fragment packets are reassembled into their + * original, unfragmented form, as illustrated: + * + * reassembled original packet: + * + * +---------------+-----------------+---------+--------+-//--+--------+ + * | Per-Fragment |Ext & Upper-Layer| first | second | | last | + * | Headers | Headers |frag data|fragment|.....|fragment| + * +---------------+-----------------+---------+--------+-//--+--------+ + * + * The following rules govern reassembly: + * + * An original packet is reassembled only from fragment packets that + * have the same Source Address, Destination Address, and Fragment + * Identification. + * + * The Per-Fragment headers of the reassembled packet consists of all + * headers up to, but not including, the Fragment header of the first + * fragment packet (that is, the packet whose Fragment Offset is + * zero), with the following two changes: + * + * The Next Header field of the last header of the Per-Fragment + * headers is obtained from the Next Header field of the first + * fragment's Fragment header. + * + * The Payload Length of the reassembled packet is computed from + * the length of the Per-Fragment headers and the length and + * offset of the last fragment. For example, a formula for + * computing the Payload Length of the reassembled original packet + * is: + * + * PL.orig = PL.first - FL.first - 8 + (8 * FO.last) + FL.last + * + * where + * PL.orig = Payload Length field of reassembled packet. + * PL.first = Payload Length field of first fragment packet. + * FL.first = length of fragment following Fragment header of + * first fragment packet. + * FO.last = Fragment Offset field of Fragment header of last + * fragment packet. + * FL.last = length of fragment following Fragment header of + * last fragment packet. + * + * The Fragmentable Part of the reassembled packet is constructed + * from the fragments following the Fragment headers in each of + * the fragment packets. The length of each fragment is computed + * by subtracting from the packet's Payload Length the length of + * the headers between the IPv6 header and fragment itself; its + * + * relative position in Fragmentable Part is computed from its + * Fragment Offset value. + * + * The Fragment header is not present in the final, reassembled + * packet. + * + * If the fragment is a whole datagram (that is, both the Fragment + * Offset field and the M flag are zero), then it does not need + * any further reassembly and should be processed as a fully + * reassembled packet (i.e., updating Next Header, adjust Payload + * Length, removing the Fragment header, etc.). Any other + * fragments that match this packet (i.e., the same IPv6 Source + * Address, IPv6 Destination Address, and Fragment Identification) + * should be processed independently. + * + * The following error conditions may arise when reassembling fragmented + * packets: + * + * o If insufficient fragments are received to complete reassembly + * of a packet within 60 seconds of the reception of the first- + * arriving fragment of that packet, reassembly of that packet + * must be abandoned and all the fragments that have been received + * for that packet must be discarded. If the first fragment + * (i.e., the one with a Fragment Offset of zero) has been + * received, an ICMP Time Exceeded -- Fragment Reassembly Time + * Exceeded message should be sent to the source of that fragment. + * + * o If the length of a fragment, as derived from the fragment + * packet's Payload Length field, is not a multiple of 8 octets + * and the M flag of that fragment is 1, then that fragment must + * be discarded and an ICMP Parameter Problem, Code 0, message + * should be sent to the source of the fragment, pointing to the + * Payload Length field of the fragment packet. + * + * o If the length and offset of a fragment are such that the + * Payload Length of the packet reassembled from that fragment + * would exceed 65,535 octets, then that fragment must be + * discarded and an ICMP Parameter Problem, Code 0, message should + * be sent to the source of the fragment, pointing to the Fragment + * Offset field of the fragment packet. + * + * o If the first fragment does not include all headers through an + * Upper-Layer header, then that fragment should be discarded and + * an ICMP Parameter Problem, Code 3, message should be sent to + * the source of the fragment, with the Pointer field set to zero. + * + * o If any of the fragments being reassembled overlap with any + * other fragments being reassembled for the same packet, + * reassembly of that packet must be abandoned and all the + * fragments that have been received for that packet must be + * discarded, and no ICMP error messages should be sent. + * + * It should be noted that fragments may be duplicated in the + * network. Instead of treating these exact duplicate fragments + * as overlapping fragments, an implementation may choose to + * detect this case and drop exact duplicate fragments while + * keeping the other fragments belonging to the same packet. + * + * The following conditions are not expected to occur frequently but are + * not considered errors if they do: + * + * The number and content of the headers preceding the Fragment + * header of different fragments of the same original packet may + * differ. Whatever headers are present, preceding the Fragment + * header in each fragment packet, are processed when the packets + * arrive, prior to queueing the fragments for reassembly. Only + * those headers in the Offset zero fragment packet are retained in + * the reassembled packet. + * + * The Next Header values in the Fragment headers of different + * fragments of the same original packet may differ. Only the value + * from the Offset zero fragment packet is used for reassembly. + * + * Other fields in the IPv6 header may also vary across the fragments + * being reassembled. Specifications that use these fields may + * provide additional instructions if the basic mechanism of using + * the values from the Offset zero fragment is not sufficient. For + * example, Section 5.3 of [RFC3168] describes how to combine the + * Explicit Congestion Notification (ECN) bits from different + * fragments to derive the ECN bits of the reassembled packet. + */ class Ipv6Fragment( - override val nextHeader: UByte = IpType.TCP.value, - override val length: UByte = MIN_LENGTH, + override var nextHeader: UByte = IpType.TCP.value, + override val length: UByte = LENGTH, val fragmentOffset: UShort = 0u, val moreFlag: Boolean = false, val identification: UInt = 0u, -) : Ipv6ExtensionHeader(nextHeader = nextHeader, length = length) { +) : Ipv6ExtensionHeader(IpType.IPV6_FRAG, nextHeader = nextHeader, length = length) { companion object { - const val MIN_LENGTH: UByte = 8u // next header, reserved, fragment offset, and identification + const val LENGTH: UByte = 8u // next header, reserved, fragment offset, and identification + var globalIdentificationCounter: UInt = 0u fun fromStream( stream: ByteBuffer, @@ -23,12 +285,12 @@ class Ipv6Fragment( val fragmentOffset = ((fragmentOffsetRMByte and 0b111111111111100).toUInt() shr 3).toUShort() val moreFlag = (fragmentOffsetRMByte and 0b1).toInt() == 1 val identification = stream.getInt().toUInt() - return Ipv6Fragment(nextheader, MIN_LENGTH, fragmentOffset, moreFlag, identification) + return Ipv6Fragment(nextheader, LENGTH, fragmentOffset, moreFlag, identification) } } override fun toByteArray(order: ByteOrder): ByteArray { - val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt()) + val buffer = ByteBuffer.allocate(LENGTH.toInt()) buffer.order(order) buffer.put(super.toByteArray(order)) buffer.putShort((fragmentOffset.toInt() shl 3 or if (moreFlag) 1 else 0).toShort()) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt index d344df0..999a1cc 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt @@ -30,10 +30,10 @@ import kotlin.math.ceil * in Section 4.2. */ data class Ipv6HopByHopOptions( - override val nextHeader: UByte = IpType.TCP.value, + override var nextHeader: UByte = IpType.TCP.value, override val length: UByte = ceil((MIN_LENGTH.toDouble() + Ipv6Tlv().size()) / 8.0).toUInt().toUByte(), val optionData: List = listOf(Ipv6Tlv()), -) : Ipv6ExtensionHeader(nextHeader, length) { +) : Ipv6ExtensionHeader(IpType.HOPOPT, nextHeader, length) { init { // dummy check to make sure the length is a multiple of 8 val optionLength = optionData.sumOf { it.size() } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt index c8f7148..4ed40f2 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt @@ -71,11 +71,11 @@ import java.nio.ByteOrder * can be found in [RFC5871]. */ data class Ipv6Routing( - override val nextHeader: UByte = IpType.TCP.value, + override var nextHeader: UByte = IpType.TCP.value, override val length: UByte = MIN_LENGTH, val routingType: Ipv6RoutingType, val segmentsLeft: UByte, -) : Ipv6ExtensionHeader(nextHeader, length) { +) : Ipv6ExtensionHeader(IpType.IPV6_ROUTE, nextHeader, length) { companion object { const val MIN_LENGTH: UByte = 4u // next header, length, routing type, and segments left diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt new file mode 100644 index 0000000..850f39c --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt @@ -0,0 +1,25 @@ +package com.jasonernst.knet.ip.v6.extensions + +import com.jasonernst.knet.ip.v6.Ipv6Header +import com.jasonernst.knet.transport.tcp.TcpHeader +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.random.Random + +class Ipv6FragmentTest { + @Test + fun fragmentReassembly() { + val payload = ByteArray(5000) + Random.Default.nextBytes(payload) + val tcpHeader = TcpHeader() + val ipPayloadLength = payload.size.toUInt() + tcpHeader.getHeaderLength() + val ipv6Header = Ipv6Header(payloadLength = ipPayloadLength.toUShort()) + val fragments = ipv6Header.fragment(1000u, tcpHeader, payload) + assertEquals(6, fragments.size) // 5 won't quite fit with headers + val reassembled = Ipv6Header.reassemble(fragments) + assertEquals(ipv6Header, reassembled.first) + assertEquals(tcpHeader, reassembled.second) + assertArrayEquals(payload, reassembled.third) + } +} From ef037e4b6410faf4ae278100449aebbbae1650ae Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 27 Sep 2024 10:28:26 +0000 Subject: [PATCH 10/22] fix an edge case where payload fragment was not divisible by 8 --- .../kotlin/com/jasonernst/knet/ip/IpHeader.kt | 7 ++++ .../com/jasonernst/knet/ip/v4/Ipv4Header.kt | 3 +- .../com/jasonernst/knet/ip/v6/Ipv6Header.kt | 35 +++++++++++++++---- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt index 3631f58..2a5400c 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt @@ -20,6 +20,13 @@ interface IpHeader { const val IP4_VERSION: UByte = 4u const val IP6_VERSION: UByte = 6u + /** + * Helper function so that we can ensure the payload length is a multiple of 8 + */ + fun closestDivisibleBy(initialValue: UInt, divisor: UInt): UInt { + return (initialValue + divisor - 1u) / divisor * divisor + } + fun fromStream(stream: ByteBuffer): IpHeader { val start = stream.position() if (stream.remaining() < 1) { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt index d0559bd..806f5de 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt @@ -4,6 +4,7 @@ import com.jasonernst.icmp_common.Checksum import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpHeader.Companion.IP4_VERSION +import com.jasonernst.knet.ip.IpHeader.Companion.closestDivisibleBy import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v4.options.Ipv4Option import com.jasonernst.knet.ip.v4.options.Ipv4Option.Companion.parseOptions @@ -327,7 +328,7 @@ data class Ipv4Header( "The smallest fragment size is ${IP4_MIN_FRAGMENT_PAYLOAD.toInt()} bytes because it must align on a 64-bit boundary", ) } - var payloadPerPacket = maxSize - getHeaderLength() + var payloadPerPacket = closestDivisibleBy(maxSize - getHeaderLength(), 8u) var payloadPosition = 0u var isFirstFragment = true while (payloadPosition < payload.size.toUInt()) { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt index 62d3256..79f95a5 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt @@ -3,6 +3,7 @@ package com.jasonernst.knet.ip.v6 import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpHeader.Companion.IP6_VERSION +import com.jasonernst.knet.ip.IpHeader.Companion.closestDivisibleBy import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader import com.jasonernst.knet.ip.v6.extenions.Ipv6Fragment @@ -61,7 +62,7 @@ data class Ipv6Header( // ensure we have an IPv6 packet val versionAndHeaderLength = stream.get().toUByte() val ipVersion = (versionAndHeaderLength.toInt() shr 4 and 0x0F).toUByte() - if (ipVersion != IpHeader.IP6_VERSION) { + if (ipVersion != IP6_VERSION) { throw IllegalArgumentException("Invalid IPv6 header. IP version should be 6 but was $ipVersion") } @@ -112,13 +113,18 @@ data class Ipv6Header( throw IllegalArgumentException("No fragments to reassemble") } + var firstFragmentHeader: Ipv6Fragment? = null val extensionHeaders = mutableListOf() for (extensionHeader in fragments[0].first.extensionHeaders) { if (extensionHeader.type == IpType.IPV6_FRAG) { + firstFragmentHeader = extensionHeader as Ipv6Fragment continue } extensionHeaders.add(extensionHeader) } + if (firstFragmentHeader == null) { + throw IllegalArgumentException("First fragment does not contain a fragment header") + } val payloadLength = fragments.sumOf { it.third.size @@ -140,11 +146,26 @@ data class Ipv6Header( extensionHeaders = extensionHeaders, ) - val payload = - fragments - .flatMap { - it.third.toList() - }.toByteArray() + val nonHeaderPayloadLength = fragments.sumOf { it.third.size } + val payload = ByteArray(nonHeaderPayloadLength) + for (fragment in fragments) { + val extensionHeaders = fragment.first.extensionHeaders + var fragmentHeader: Ipv6Fragment? = null + for (extensionHeader in extensionHeaders) { + if (extensionHeader.type == IpType.IPV6_FRAG) { + fragmentHeader = extensionHeader as Ipv6Fragment + break + } + } + if (fragmentHeader == null) { + throw IllegalArgumentException("Fragment does not contain a fragment header") + } + if (fragmentHeader.identification != firstFragmentHeader.identification) { + throw IllegalArgumentException("Fragment identification does not match first fragment") + } + val payloadPosition = fragmentHeader.fragmentOffset.toInt() * 8 + fragment.third.copyInto(payload, payloadPosition) + } val nextHeader = fragments.first().second @@ -234,7 +255,7 @@ data class Ipv6Header( destinationAddress, firstHeaderExtensions, ) - val firstPayloadBytes = maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt() - extAndUpperBytes.toUInt() + val firstPayloadBytes = closestDivisibleBy(maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt() - extAndUpperBytes.toUInt(), 8u) val firstPair = Triple(firstFragment, nextHeader, payload.sliceArray(0 until firstPayloadBytes.toInt())) fragments.add(firstPair) var payloadPosition = firstPayloadBytes.toInt() From 29621a386c03faa4ce961b00d91f3d0aeb12c1d1 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 27 Sep 2024 12:17:09 +0000 Subject: [PATCH 11/22] increase test coverage, fixes for fragmentation --- .../kotlin/com/jasonernst/knet/ip/IpHeader.kt | 7 +- .../com/jasonernst/knet/ip/v4/Ipv4Header.kt | 31 +++-- .../com/jasonernst/knet/ip/v6/Ipv6Header.kt | 3 +- .../com/jasonernst/knet/ip/IpHeaderTest.kt | 6 + .../jasonernst/knet/ip/v4/Ipv4FragmentTest.kt | 121 ++++++++++++++++++ .../jasonernst/knet/ip/v4/Ipv4HeaderTest.kt | 21 +-- 6 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4FragmentTest.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt index 2a5400c..149a09f 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/IpHeader.kt @@ -23,9 +23,10 @@ interface IpHeader { /** * Helper function so that we can ensure the payload length is a multiple of 8 */ - fun closestDivisibleBy(initialValue: UInt, divisor: UInt): UInt { - return (initialValue + divisor - 1u) / divisor * divisor - } + fun closestDivisibleBy( + initialValue: UInt, + divisor: UInt, + ): UInt = (initialValue + divisor - 1u) / divisor * divisor fun fromStream(stream: ByteBuffer): IpHeader { val start = stream.position() diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt index 806f5de..d237667 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v4/Ipv4Header.kt @@ -15,6 +15,7 @@ import java.nio.ByteBuffer import java.nio.ByteOrder import kotlin.experimental.or import kotlin.math.ceil +import kotlin.math.min /** * Internet Protocol Version 4 Header Implementation. @@ -29,8 +30,8 @@ data class Ipv4Header( val dscp: UByte = 0u, // 2-bits, explicit congestion notification. val ecn: UByte = 0u, - // 16-bits, IP packet, including the header - private val totalLength: UShort = 0u, + // 16-bits, IP packet, including the header: default to a just the header with no payload + private val totalLength: UShort = IP4_MIN_HEADER_LENGTH.toUShort(), // 16-bits, groups fragments of a single IPv4 datagram. val id: UShort = 0u, // if the packet is marked as don't fragment and we can't fit it in a single packet, drop it. @@ -313,9 +314,12 @@ data class Ipv4Header( * */ fun fragment( - maxSize: UInt, + maxSize: UInt, // max size includes the header size payload: ByteArray, ): List> { + if (maxSize.toInt() % 8 != 0) { + throw IllegalArgumentException("Fragment max size must be divisible by 8") + } if (dontFragment) { throw IllegalArgumentException("Cannot fragment packets marked as don't fragment") } @@ -328,11 +332,17 @@ data class Ipv4Header( "The smallest fragment size is ${IP4_MIN_FRAGMENT_PAYLOAD.toInt()} bytes because it must align on a 64-bit boundary", ) } - var payloadPerPacket = closestDivisibleBy(maxSize - getHeaderLength(), 8u) + var lastFragment = false var payloadPosition = 0u + var payloadPerPacket = min(payload.size - payloadPosition.toInt(), closestDivisibleBy(maxSize - getHeaderLength(), 8u).toInt()) + logger.debug("PAYLOAD PER PACKET: $payloadPerPacket HEADERSIZE: ${getHeaderLength()}") + if (payloadPosition.toInt() + payloadPerPacket == payload.size) { + lastFragment = true + } + var isFirstFragment = true while (payloadPosition < payload.size.toUInt()) { - logger.debug("$payloadPosition:${payloadPosition + payloadPerPacket}") + logger.debug("$payloadPosition:${payloadPosition + payloadPerPacket.toUInt()}") val offsetIn64BitOctets = payloadPosition / 8u var newOptions = options var newIhl = ihl @@ -356,10 +366,10 @@ data class Ipv4Header( ihl = newIhl, dscp = dscp, ecn = ecn, - totalLength = (getHeaderLength() + payloadPerPacket).toUShort(), + totalLength = (getHeaderLength() + payloadPerPacket.toUInt()).toUShort(), id = id, dontFragment = false, - lastFragment = payloadPosition >= getHeaderLength(), + lastFragment = lastFragment, fragmentOffset = offsetIn64BitOctets.toUShort(), ttl = ttl, protocol = protocol, @@ -370,9 +380,10 @@ data class Ipv4Header( logger.debug("payload len:${newHeader.getPayloadLength()}") val newPayload = payload.copyOfRange(payloadPosition.toInt(), payloadPosition.toInt() + payloadPerPacket.toInt()) packetList.add(Pair(newHeader, newPayload)) - payloadPosition += payloadPerPacket - if (payloadPosition + payloadPerPacket > payload.size.toUInt()) { - payloadPerPacket = payload.size.toUInt() - payloadPosition + payloadPosition += payloadPerPacket.toUInt() + if (payloadPosition + payloadPerPacket.toUInt() > payload.size.toUInt()) { + payloadPerPacket = (payload.size.toUInt() - payloadPosition).toInt() + lastFragment = true } } return packetList diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt index 79f95a5..2295d2b 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt @@ -255,7 +255,8 @@ data class Ipv6Header( destinationAddress, firstHeaderExtensions, ) - val firstPayloadBytes = closestDivisibleBy(maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt() - extAndUpperBytes.toUInt(), 8u) + val firstPayloadBytes = + closestDivisibleBy(maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt() - extAndUpperBytes.toUInt(), 8u) val firstPair = Triple(firstFragment, nextHeader, payload.sliceArray(0 until firstPayloadBytes.toInt())) fragments.add(firstPair) var payloadPosition = firstPayloadBytes.toInt() diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/IpHeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/IpHeaderTest.kt index 1db4700..eafc57b 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/IpHeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/IpHeaderTest.kt @@ -8,6 +8,12 @@ import java.net.Inet6Address import java.nio.ByteBuffer class IpHeaderTest { + companion object { + fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } + + fun byteBufferOfInts(vararg ints: Int) = ByteBuffer.wrap(byteArrayOfInts(*ints)) + } + @Test fun tooShortBuffer() { val stream = ByteBuffer.allocate(0) assertThrows { diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4FragmentTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4FragmentTest.kt new file mode 100644 index 0000000..07b882b --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4FragmentTest.kt @@ -0,0 +1,121 @@ +package com.jasonernst.knet.ip.v4 + +import com.jasonernst.knet.ip.IpHeader.Companion.closestDivisibleBy +import com.jasonernst.knet.ip.IpHeaderTest.Companion.byteArrayOfInts +import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_MIN_HEADER_LENGTH +import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_WORD_LENGTH +import com.jasonernst.knet.ip.v4.options.Ipv4OptionEndOfOptionList +import com.jasonernst.knet.ip.v4.options.Ipv4OptionNoOperation +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.net.InetAddress +import kotlin.math.ceil + +class Ipv4FragmentTest { + @Test + fun fragmentationAndReassembly() { + val payload = byteArrayOfInts(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09) + val ipv4Header = Ipv4Header(totalLength = (IP4_MIN_HEADER_LENGTH + payload.size.toUShort()).toUShort(), dontFragment = false) + val fragmentSize = closestDivisibleBy(IP4_MIN_HEADER_LENGTH + (payload.size / 2).toUInt(), 8u) + val fragments = ipv4Header.fragment(fragmentSize, payload) + assertEquals(2, fragments.size) + + val reassembly = Ipv4Header.reassemble(fragments) + assertEquals(ipv4Header, reassembly.first) + assertArrayEquals(payload, reassembly.second) + } + + @Test + fun emptyFragments() { + assertThrows { + Ipv4Header.reassemble(emptyList()) + } + } + + @Test + fun singleFragmentWithoutLastFragmentSetToTrue() { + val ipv4Header = Ipv4Header(lastFragment = false) + assertThrows { + Ipv4Header.reassemble(listOf(Pair(ipv4Header, ByteArray(0)))) + } + } + + @Test + fun singleFragmentLastFragment() { + val payload = byteArrayOfInts(0x01, 0x02, 0x03) + val ipv4Header = Ipv4Header(totalLength = (IP4_MIN_HEADER_LENGTH + payload.size.toUInt()).toUShort(), dontFragment = false) + val fragmentSize = closestDivisibleBy(IP4_MIN_HEADER_LENGTH + 8u, 8u) + val fragmented = ipv4Header.fragment(fragmentSize, payload) + val reassembled = Ipv4Header.reassemble(fragmented) + assertEquals(ipv4Header, reassembled.first) + } + + @Test + fun notDivisbleBy8() { + val ipv4Header = Ipv4Header() + assertThrows { + ipv4Header.fragment(1u, ByteArray(0)) + } + } + + @Test + fun nonMatchingFragmentFields() { + val ipv4Header = Ipv4Header(id = 1u) + val ipv4Header2 = Ipv4Header(id = 2u) + assertThrows { + Ipv4Header.reassemble(listOf(Pair(ipv4Header, ByteArray(0)), Pair(ipv4Header2, ByteArray(0)))) + } + + val ipv4Header3 = Ipv4Header(id = 1u, protocol = IpType.TCP.value) + assertThrows { + Ipv4Header.reassemble(listOf(Pair(ipv4Header, ByteArray(0)), Pair(ipv4Header3, ByteArray(0)))) + } + + val ipv4Header4 = Ipv4Header(id = 1u, sourceAddress = InetAddress.getLoopbackAddress()) + assertThrows { + Ipv4Header.reassemble(listOf(Pair(ipv4Header, ByteArray(0)), Pair(ipv4Header4, ByteArray(0)))) + } + + val ipv4Header5 = Ipv4Header(id = 1u, destinationAddress = InetAddress.getLoopbackAddress()) + assertThrows { + Ipv4Header.reassemble(listOf(Pair(ipv4Header, ByteArray(0)), Pair(ipv4Header5, ByteArray(0)))) + } + } + + @Test + fun fragmentADontFragment() { + val ipv4Header = Ipv4Header(dontFragment = true) + assertThrows { + ipv4Header.fragment(8u, byteArrayOfInts(0x01, 0x02, 0x03)) + } + } + + @Test + fun fragmentTooSmall() { + val ipv4Header = Ipv4Header(dontFragment = false) + assertThrows { + ipv4Header.fragment(0u, byteArrayOfInts(0x01, 0x02, 0x03)) + } + } + + @Test + fun fragmentWithOptions() { + val options = listOf(Ipv4OptionNoOperation(isCopied = true), Ipv4OptionEndOfOptionList(isCopied = false)) + val optionsLength = options.sumOf { it.size.toInt() } + val payload = ByteArray(16) + val totalHeaderLength = (IP4_MIN_HEADER_LENGTH + optionsLength.toUInt()) + val ihl = ceil(totalHeaderLength.toDouble() / IP4_WORD_LENGTH.toDouble()).toUInt().toUByte() + val totalLength = (((ihl * IP4_WORD_LENGTH) + payload.size.toUInt()).toUShort()) // account for zero padding the header + val ipv4Header = Ipv4Header(ihl = ihl, totalLength = totalLength, dontFragment = false, options = options) + val maxSize = closestDivisibleBy(totalHeaderLength + 8u, 8u) + val fragments = ipv4Header.fragment(maxSize, payload) + assertEquals(2, fragments.size) + + val reassembly = Ipv4Header.reassemble(fragments) + assertEquals(ipv4Header, reassembly.first) + assertArrayEquals(payload, reassembly.second) + } +} diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4HeaderTest.kt index 37265cf..b18920a 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v4/Ipv4HeaderTest.kt @@ -4,15 +4,14 @@ import com.jasonernst.icmp_common.Checksum import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpHeader import com.jasonernst.knet.ip.IpHeader.Companion.IP4_VERSION +import com.jasonernst.knet.ip.IpHeaderTest.Companion.byteBufferOfInts import com.jasonernst.knet.ip.IpType -import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_MIN_FRAGMENT_PAYLOAD import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_MIN_HEADER_LENGTH import com.jasonernst.knet.ip.v4.Ipv4Header.Companion.IP4_WORD_LENGTH import com.jasonernst.knet.ip.v4.options.Ipv4OptionNoOperation import com.jasonernst.knet.transport.tcp.TcpHeader import com.jasonernst.knet.transport.tcp.options.TcpOptionEndOfOptionList import com.jasonernst.packetdumper.stringdumper.StringPacketDumper -import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -27,13 +26,6 @@ class Ipv4HeaderTest { private val logger = LoggerFactory.getLogger(javaClass) private val stringPacketDumper = StringPacketDumper() - private fun byteArrayOfInts(vararg ints: Int) = - ByteArray(ints.size) { pos -> - ints[pos].toByte() - } - - private fun byteBufferOfInts(vararg ints: Int) = ByteBuffer.wrap(byteArrayOfInts(*ints)) - @Test fun ipv4checksumTest2() { val buffer = @@ -319,15 +311,4 @@ class Ipv4HeaderTest { Ipv4Header.fromStream(stream) } } - - @Test fun fragmentationAndReassembly() { - val payload = byteArrayOfInts(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09) - val ipv4Header = Ipv4Header(totalLength = (IP4_MIN_HEADER_LENGTH + payload.size.toUShort()).toUShort(), dontFragment = false) - val fragments = ipv4Header.fragment(IP4_MIN_HEADER_LENGTH + IP4_MIN_FRAGMENT_PAYLOAD.toUInt(), payload) - assertEquals(2, fragments.size) - - val reassembly = Ipv4Header.reassemble(fragments) - assertEquals(ipv4Header, reassembly.first) - assertArrayEquals(payload, reassembly.second) - } } From 54755eec10e9c5d81c69dfd45b1db8afc8d5b9af Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 27 Sep 2024 12:28:29 +0000 Subject: [PATCH 12/22] increase test coverage for ipv6 fragment extension header --- .../knet/ip/v6/extenions/Ipv6Fragment.kt | 2 +- .../knet/ip/v6/extensions/Ipv6FragmentTest.kt | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt index 06953f6..8038d31 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt @@ -266,7 +266,7 @@ import kotlin.experimental.and * Explicit Congestion Notification (ECN) bits from different * fragments to derive the ECN bits of the reassembled packet. */ -class Ipv6Fragment( +data class Ipv6Fragment( override var nextHeader: UByte = IpType.TCP.value, override val length: UByte = LENGTH, val fragmentOffset: UShort = 0u, diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt index 850f39c..1421f87 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt @@ -1,13 +1,37 @@ package com.jasonernst.knet.ip.v6.extensions +import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v6.Ipv6Header +import com.jasonernst.knet.ip.v6.extenions.Ipv6Fragment import com.jasonernst.knet.transport.tcp.TcpHeader import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import java.nio.ByteBuffer import kotlin.random.Random class Ipv6FragmentTest { + @Test + fun toAndFromStream() { + val fragmentHeader = Ipv6Fragment() + val stream = ByteBuffer.wrap(fragmentHeader.toByteArray()) + var nextHeader = stream.get().toUByte() + stream.get() // skip over length + var parsedFragmentHeader = Ipv6Fragment.fromStream(stream, nextHeader) + assertEquals(fragmentHeader, parsedFragmentHeader) + + val fragmentWithMore = Ipv6Fragment(moreFlag = true) + fragmentWithMore.nextHeader = IpType.UDP.value // make sure the setter is working + val stream2 = ByteBuffer.wrap(fragmentWithMore.toByteArray()) + nextHeader = stream2.get().toUByte() + stream2.get() // skip over length + parsedFragmentHeader = Ipv6Fragment.fromStream(stream2, nextHeader) + assertEquals(fragmentWithMore, parsedFragmentHeader) + assertTrue(parsedFragmentHeader.moreFlag) + assertEquals(IpType.UDP.value, parsedFragmentHeader.nextHeader) + } + @Test fun fragmentReassembly() { val payload = ByteArray(5000) From a3706ba8495e67c66afec090930fb78c6b041160 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Fri, 27 Sep 2024 12:37:03 +0000 Subject: [PATCH 13/22] increased test coverage --- .../test/kotlin/com/jasonernst/knet/PacketTests.kt | 13 ++++++++++++- .../knet/transport/tcp/options/TcpOptionTests.kt | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt b/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt index 1c3e2bb..aadab33 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/PacketTests.kt @@ -6,8 +6,10 @@ import com.jasonernst.knet.ip.v4.Ipv4Header import com.jasonernst.knet.ip.v6.Ipv6Header import com.jasonernst.knet.transport.tcp.TcpHeader import com.jasonernst.knet.transport.udp.UdpHeader +import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +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 @@ -59,7 +61,7 @@ class PacketTests { } @Test - fun notEquals() { + fun equalityChecks() { val ipHeader = Ipv4Header() val tcpHeader = TcpHeader() val packet = Packet(ipHeader, tcpHeader, ByteArray(0)) @@ -76,5 +78,14 @@ class PacketTests { val packet4 = Packet(ipHeader, tcpHeader, ByteArray(1)) assertFalse(packet == packet4) + + assertNotEquals(packet, null) + assertNotEquals(packet, Any()) + assertEquals(packet, packet) + + val packet5 = packet.copy() + assertEquals(packet.ipHeader, packet5.ipHeader) + assertEquals(packet.nextHeaders, packet5.nextHeaders) + assertArrayEquals(packet.payload, packet5.payload) } } 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 3e43daf..f2cae3a 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 @@ -338,6 +338,9 @@ class TcpOptionTests { val option1 = TcpOptionUnsupported(88u, ByteArray(0)) val option2 = TcpOptionUnsupported(88u, ByteArray(0)) assertEquals(option1, option2) + assertEquals(option1, option1) + assertNotEquals(option1, Any()) + assertNotEquals(option1, null) val option3 = TcpOptionUnsupported(88u, ByteArray(1)) assertNotEquals(option1, option3) From 25cf8c450971e98b67fc6e96ddbc1a9add72bce1 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 10:47:47 -0700 Subject: [PATCH 14/22] WiP: Ipv6 destination options --- .../ip/v6/extenions/Ipv6DestinationOptions.kt | 15 ++++++--- .../extensions/Ipv6DestinationOptionsTest.kt | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt index 6da1672..ed39ae4 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt @@ -1,16 +1,19 @@ package com.jasonernst.knet.ip.v6.extenions +import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpType +import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder data class Ipv6DestinationOptions( override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = 0u, + override val length: UByte = MIN_LENGTH, val optionData: List = emptyList(), ) : Ipv6ExtensionHeader(IpType.IPV6_OPTS, nextHeader, length) { companion object { - const val MIN_LENGTH = 2 // next header and length with no actual option data + val logger = LoggerFactory.getLogger(Ipv6DestinationOptions::class.java) + const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data fun fromStream( stream: ByteBuffer, @@ -19,7 +22,11 @@ data class Ipv6DestinationOptions( ): Ipv6DestinationOptions { val optionData = mutableListOf() val start = stream.position() - while (stream.position() - start < length.toInt()) { + logger.debug("Stream position: ${stream.position()} remaining: ${stream.remaining()}") + if (stream.remaining() < (length.toInt() - MIN_LENGTH.toInt())) { + throw PacketTooShortException("We require: ${length - MIN_LENGTH} bytes left but only have ${stream.remaining()} bytes left") + } + while (stream.position() - start < length.toInt() - MIN_LENGTH.toInt()) { optionData.add(Ipv6Tlv.fromStream(stream)) } return Ipv6DestinationOptions(nextheader, length, optionData) @@ -27,7 +34,7 @@ data class Ipv6DestinationOptions( } override fun toByteArray(order: ByteOrder): ByteArray { - val buffer = ByteBuffer.allocate(MIN_LENGTH + optionData.sumOf { it.toByteArray().size }) + val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt() + optionData.sumOf { it.toByteArray().size }) buffer.order(order) buffer.put(super.toByteArray(order)) optionData.forEach { diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt new file mode 100644 index 0000000..d07bfb3 --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt @@ -0,0 +1,33 @@ +package com.jasonernst.knet.ip.v6.extensions + +import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v6.extenions.Ipv6DestinationOptions +import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer + +class Ipv6DestinationOptionsTest { + @Test fun toFromStream() { + val logger = LoggerFactory.getLogger(javaClass) + val destinationOptions = Ipv6DestinationOptions() + val stream = ByteBuffer.wrap(destinationOptions.toByteArray()) + val nextHeader = stream.get().toUByte() + val length = stream.get().toUByte() + logger.debug("Stream length: ${stream.remaining()}") + val parsedDestinationOptions = Ipv6DestinationOptions.fromStream(stream, nextHeader, length) + assertEquals(destinationOptions, parsedDestinationOptions) + + val options = listOf(Ipv6Tlv()) + val optionsSize = options.sumOf { options.size } + val destinationOptionsWithOptions = Ipv6DestinationOptions(nextHeader = IpType.UDP.value, length = optionsSize.toUByte(), optionData = options) + val stream2 = ByteBuffer.wrap(destinationOptionsWithOptions.toByteArray()) + val nextHeader2 = stream2.get().toUByte() + assertEquals(IpType.UDP.value, nextHeader2) + val length2 = stream2.get().toInt() + assertEquals(optionsSize, length2) + val parsedDestinationOptionsWithOptions = Ipv6DestinationOptions.fromStream(stream2, nextHeader2, length) + assertEquals(destinationOptionsWithOptions, parsedDestinationOptionsWithOptions) + } +} \ No newline at end of file From 42e6fd354578ec959e37ed615d5db18e60a9a7f2 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 12:31:39 -0700 Subject: [PATCH 15/22] Fixed 8-octet length issue --- .../ip/v6/extenions/Ipv6DestinationOptions.kt | 92 +++++++++++++++---- .../ip/v6/extenions/Ipv6ExtensionHeader.kt | 14 ++- .../knet/ip/v6/extenions/Ipv6Fragment.kt | 5 +- .../ip/v6/extenions/Ipv6HopByHopOptions.kt | 21 +---- .../knet/ip/v6/extenions/Ipv6Tlv.kt | 4 +- .../extensions/Ipv6DestinationOptionsTest.kt | 39 ++++++-- .../knet/ip/v6/extensions/Ipv6HopByHopTest.kt | 2 + 7 files changed, 130 insertions(+), 47 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt index ed39ae4..1f65bcd 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt @@ -1,40 +1,100 @@ package com.jasonernst.knet.ip.v6.extenions -import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpType -import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder +/** + * The Destination Options header is used to carry optional information + * that need be examined only by a packet's destination node(s). The + * Destination Options header is identified by a Next Header value of 60 + * in the immediately preceding header and has the following format: + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Next Header | Hdr Ext Len | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + * | | + * . . + * . Options . + * . . + * | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Next Header 8-bit selector. Identifies the type of header + * immediately following the Destination Options + * header. Uses the same values as the IPv4 + * Protocol field [IANA-PN]. + * + * Hdr Ext Len 8-bit unsigned integer. Length of the + * Destination Options header in 8-octet units, + * not including the first 8 octets. + * + * Options Variable-length field, of length such that the + * complete Destination Options header is an + * integer multiple of 8 octets long. Contains + * one or more TLV-encoded options, as described + * in Section 4.2. + * + * The only destination options defined in this document are the Pad1 + * and PadN options specified in Section 4.2. + * + * Note that there are two possible ways to encode optional destination + * information in an IPv6 packet: either as an option in the Destination + * Options header or as a separate extension header. The Fragment + * header and the Authentication header are examples of the latter + * approach. Which approach can be used depends on what action is + * desired of a destination node that does not understand the optional + * information: + * + * o If the desired action is for the destination node to discard + * the packet and, only if the packet's Destination Address is not + * a multicast address, send an ICMP Unrecognized Type message to + * the packet's Source Address, then the information may be + * encoded either as a separate header or as an option in the + * Destination Options header whose Option Type has the value 11 + * in its highest-order 2 bits. The choice may depend on such + * factors as which takes fewer octets, or which yields better + * alignment or more efficient parsing. + * + * o If any other action is desired, the information must be encoded + * as an option in the Destination Options header whose Option + * Type has the value 00, 01, or 10 in its highest-order 2 bits, + * specifying the desired action (see Section 4.2). + */ data class Ipv6DestinationOptions( override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = MIN_LENGTH, - val optionData: List = emptyList(), + override val length: UByte = 1u, + val optionData: List = listOf(Ipv6Tlv()), ) : Ipv6ExtensionHeader(IpType.IPV6_OPTS, nextHeader, length) { - companion object { - val logger = LoggerFactory.getLogger(Ipv6DestinationOptions::class.java) - const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data + init { + // dummy check to ensure length matches the option data + val octet8Lengths = optionData.sumOf { it.size() } / 8.0 + if (octet8Lengths != length.toDouble()) { + throw IllegalArgumentException("(Option data length / 8 must match the length field, have $octet8Lengths, expecting $length") + } + } + companion object { + /** + * Assumes that the stream has already had the nextHeader and length parsed from it + */ fun fromStream( stream: ByteBuffer, - nextheader: UByte, + nextHeader: UByte, length: UByte, ): Ipv6DestinationOptions { val optionData = mutableListOf() val start = stream.position() - logger.debug("Stream position: ${stream.position()} remaining: ${stream.remaining()}") - if (stream.remaining() < (length.toInt() - MIN_LENGTH.toInt())) { - throw PacketTooShortException("We require: ${length - MIN_LENGTH} bytes left but only have ${stream.remaining()} bytes left") - } - while (stream.position() - start < length.toInt() - MIN_LENGTH.toInt()) { - optionData.add(Ipv6Tlv.fromStream(stream)) + while (stream.position() - start < length.toInt()) { + val nextTlv = Ipv6Tlv.fromStream(stream) + optionData.add(nextTlv) } - return Ipv6DestinationOptions(nextheader, length, optionData) + return Ipv6DestinationOptions(nextHeader, length, optionData) } } override fun toByteArray(order: ByteOrder): ByteArray { - val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt() + optionData.sumOf { it.toByteArray().size }) + val buffer = ByteBuffer.allocate(getExtensionLengthInBytes()) buffer.order(order) buffer.put(super.toByteArray(order)) optionData.forEach { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index 87b525e..c0b5ed6 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -58,7 +58,7 @@ import java.nio.ByteOrder open class Ipv6ExtensionHeader( val type: IpType, open var nextHeader: UByte, - open val length: UByte, // measured in 64-bit / 8-octet units + open val length: UByte, // measured in 64-bit / 8-octet units, not including the first 8 octets ) { /** * This should be called by the subclass to serialize the extension header to a byte array. @@ -79,9 +79,18 @@ open class Ipv6ExtensionHeader( * This just simply multiplies it by 8, so it would include * zero padding. */ - fun getExtensionLengthInBytes(): Int = (length * 8u).toInt() + fun getExtensionLengthInBytes(): Int = + if (type == IpType.IPV6_FRAG) { + Ipv6Fragment.LENGTH.toInt() + } else { + MIN_LENGTH_BYTES + (length * 8u).toInt() + } companion object { + // since the length doesn't include the first 8 octets, we assume that we always have at + // least 8 octets + const val MIN_LENGTH_BYTES: Int = 8 + /** * The required option types: https://www.rfc-editor.org/rfc/rfc8200#page-9 * Hop-by-Hop Options @@ -143,7 +152,6 @@ open class Ipv6ExtensionHeader( throw IllegalArgumentException("Unsupported IPv6 extension header: $currentHeader") } } - currentHeader = IpType.fromValue(nextHeader) } return extensionList diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt index 8038d31..fd739fa 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt @@ -265,6 +265,9 @@ import kotlin.experimental.and * example, Section 5.3 of [RFC3168] describes how to combine the * Explicit Congestion Notification (ECN) bits from different * fragments to derive the ECN bits of the reassembled packet. + * + * NOTE: length is overrided to 0u because that field is reserved and should be all zeros according + * to the spec. */ data class Ipv6Fragment( override var nextHeader: UByte = IpType.TCP.value, @@ -272,7 +275,7 @@ data class Ipv6Fragment( val fragmentOffset: UShort = 0u, val moreFlag: Boolean = false, val identification: UInt = 0u, -) : Ipv6ExtensionHeader(IpType.IPV6_FRAG, nextHeader = nextHeader, length = length) { +) : Ipv6ExtensionHeader(IpType.IPV6_FRAG, nextHeader = nextHeader, length = 0u) { companion object { const val LENGTH: UByte = 8u // next header, reserved, fragment offset, and identification var globalIdentificationCounter: UInt = 0u diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt index 999a1cc..4ef41e9 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt @@ -4,7 +4,6 @@ import com.jasonernst.knet.ip.IpType import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder -import kotlin.math.ceil /** * https://www.rfc-editor.org/rfc/rfc8200#page-13 @@ -31,36 +30,26 @@ import kotlin.math.ceil */ data class Ipv6HopByHopOptions( override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = ceil((MIN_LENGTH.toDouble() + Ipv6Tlv().size()) / 8.0).toUInt().toUByte(), + override val length: UByte = 1u, val optionData: List = listOf(Ipv6Tlv()), ) : Ipv6ExtensionHeader(IpType.HOPOPT, nextHeader, length) { init { - // dummy check to make sure the length is a multiple of 8 - val optionLength = optionData.sumOf { it.size() } - val totalLength = optionLength + MIN_LENGTH.toInt() - if (totalLength % 8 != 0) { - throw IllegalArgumentException("Option data length + 2 must be a multiple of 8, but have $totalLength") - } - // dummy check to ensure length matches the option data - val octet8Lengths = ceil(totalLength.toDouble() / 8.0) + val octet8Lengths = optionData.sumOf { it.size() } / 8.0 if (octet8Lengths != length.toDouble()) { - throw IllegalArgumentException( - "(Option data length + 2) / 8 must match the length field, have $octet8Lengths, expecting $length", - ) + throw IllegalArgumentException("(Option data length / 8 must match the length field, have $octet8Lengths, expecting $length") } } companion object { private val logger = LoggerFactory.getLogger(Ipv6HopByHopOptions::class.java) - const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data /** * Assumes that the stream has already had the nextHeader and length parsed from it */ fun fromStream( stream: ByteBuffer, - nextheader: UByte, + nextHeader: UByte, length: UByte, ): Ipv6HopByHopOptions { val optionData = mutableListOf() @@ -70,7 +59,7 @@ data class Ipv6HopByHopOptions( logger.debug("Parsed TLV: {}", nextTlv) optionData.add(nextTlv) } - return Ipv6HopByHopOptions(nextheader, length, optionData) + return Ipv6HopByHopOptions(nextHeader, length, optionData) } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt index 75d0dbf..acfa3cb 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt @@ -114,8 +114,8 @@ import java.nio.ByteOrder */ data class Ipv6Tlv( val optionType: Ipv6DestinationHopByHopType = Ipv6DestinationHopByHopType.PadN, - val optionDataLength: UByte = 4u, - val optionData: ByteArray = ByteArray(4), + val optionDataLength: UByte = 6u, + val optionData: ByteArray = ByteArray(6), ) { init { if (optionDataLength.toInt() != optionData.size) { diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt index d07bfb3..3790643 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt @@ -5,29 +5,50 @@ import com.jasonernst.knet.ip.v6.extenions.Ipv6DestinationOptions import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -import org.slf4j.LoggerFactory +import org.junit.jupiter.api.assertThrows import java.nio.ByteBuffer class Ipv6DestinationOptionsTest { + @Test + fun lengthOptionsMismatch() { + assertThrows { + Ipv6DestinationOptions( + optionData = + listOf( + Ipv6Tlv(), + Ipv6Tlv(), + ), + ) + } + + assertThrows { + Ipv6DestinationOptions( + length = 3u, + optionData = + listOf( + Ipv6Tlv(), + ), + ) + } + } + @Test fun toFromStream() { - val logger = LoggerFactory.getLogger(javaClass) val destinationOptions = Ipv6DestinationOptions() val stream = ByteBuffer.wrap(destinationOptions.toByteArray()) val nextHeader = stream.get().toUByte() val length = stream.get().toUByte() - logger.debug("Stream length: ${stream.remaining()}") val parsedDestinationOptions = Ipv6DestinationOptions.fromStream(stream, nextHeader, length) assertEquals(destinationOptions, parsedDestinationOptions) val options = listOf(Ipv6Tlv()) - val optionsSize = options.sumOf { options.size } - val destinationOptionsWithOptions = Ipv6DestinationOptions(nextHeader = IpType.UDP.value, length = optionsSize.toUByte(), optionData = options) + val optionsSize = (options.sumOf { options.size }) + val destinationOptionsWithOptions = + Ipv6DestinationOptions(nextHeader = IpType.UDP.value, length = optionsSize.toUInt().toUByte(), optionData = options) val stream2 = ByteBuffer.wrap(destinationOptionsWithOptions.toByteArray()) val nextHeader2 = stream2.get().toUByte() assertEquals(IpType.UDP.value, nextHeader2) - val length2 = stream2.get().toInt() - assertEquals(optionsSize, length2) - val parsedDestinationOptionsWithOptions = Ipv6DestinationOptions.fromStream(stream2, nextHeader2, length) + val length2 = stream2.get().toUByte() + val parsedDestinationOptionsWithOptions = Ipv6DestinationOptions.fromStream(stream2, nextHeader2, length2) assertEquals(destinationOptionsWithOptions, parsedDestinationOptionsWithOptions) } -} \ No newline at end of file +} diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt index 9b25967..b042a58 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6HopByHopTest.kt @@ -1,5 +1,6 @@ package com.jasonernst.knet.ip.v6.extensions +import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv import com.jasonernst.packetdumper.stringdumper.StringPacketDumper @@ -45,6 +46,7 @@ class Ipv6HopByHopTest { @Test fun toFromStream() { val logger = LoggerFactory.getLogger(javaClass) val hopByHopOptions = Ipv6HopByHopOptions() + hopByHopOptions.nextHeader = IpType.UDP.value val stream = ByteBuffer.wrap(hopByHopOptions.toByteArray()) val stringPacketDumper = StringPacketDumper(logger) stringPacketDumper.dumpBuffer(stream) From 3fd8118bb0ccdd4ae68c9a7394d8a521e99bbf9c Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 12:38:00 -0700 Subject: [PATCH 16/22] increase code cov --- .../ip/v6/extensions/Ipv6DestinationOptionsTest.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt index 3790643..29eb4ba 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt @@ -32,6 +32,15 @@ class Ipv6DestinationOptionsTest { } } + @Test fun optionDataTest() { + val optionData = + listOf( + Ipv6Tlv(), + ) + val destinationOptions = Ipv6DestinationOptions(optionData = optionData) + assertEquals(destinationOptions.optionData, optionData) + } + @Test fun toFromStream() { val destinationOptions = Ipv6DestinationOptions() val stream = ByteBuffer.wrap(destinationOptions.toByteArray()) @@ -42,8 +51,8 @@ class Ipv6DestinationOptionsTest { val options = listOf(Ipv6Tlv()) val optionsSize = (options.sumOf { options.size }) - val destinationOptionsWithOptions = - Ipv6DestinationOptions(nextHeader = IpType.UDP.value, length = optionsSize.toUInt().toUByte(), optionData = options) + val destinationOptionsWithOptions = Ipv6DestinationOptions(length = optionsSize.toUInt().toUByte(), optionData = options) + destinationOptionsWithOptions.nextHeader = IpType.UDP.value val stream2 = ByteBuffer.wrap(destinationOptionsWithOptions.toByteArray()) val nextHeader2 = stream2.get().toUByte() assertEquals(IpType.UDP.value, nextHeader2) From 9997238581a0115c29ca14d3447e66ef202e2c08 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 13:30:26 -0700 Subject: [PATCH 17/22] WiP: ipv6 routing type --- .../{type => }/Ipv6DestinationHopByHopType.kt | 2 +- .../ip/v6/extenions/Ipv6ExtensionHeader.kt | 3 +- .../knet/ip/v6/extenions/Ipv6Tlv.kt | 1 - .../routing/FatalRoutingException.kt | 14 +++ .../knet/ip/v6/extenions/routing/Ipv6Crh16.kt | 3 + .../knet/ip/v6/extenions/routing/Ipv6Crh32.kt | 3 + .../v6/extenions/routing/Ipv6Rfc3692Exp1.kt | 3 + .../v6/extenions/routing/Ipv6Rfc3692Exp2.kt | 3 + .../v6/extenions/{ => routing}/Ipv6Routing.kt | 65 ++++++++-- .../{type => routing}/Ipv6RoutingType.kt | 2 +- .../extenions/routing/Ipv6RplSourceRouting.kt | 3 + .../extenions/routing/Ipv6SegmentRouting.kt | 3 + .../v6/extenions/routing/Ipv6Type2Routing.kt | 119 ++++++++++++++++++ .../routing/NonFatalRoutingException.kt | 9 ++ .../knet/ip/v6/extensions/Ipv6TlvTest.kt | 2 +- 15 files changed, 220 insertions(+), 15 deletions(-) rename knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/{type => }/Ipv6DestinationHopByHopType.kt (95%) create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/FatalRoutingException.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh16.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh32.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp1.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp2.kt rename knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/{ => routing}/Ipv6Routing.kt (57%) rename knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/{type => routing}/Ipv6RoutingType.kt (90%) create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6RplSourceRouting.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6SegmentRouting.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt create mode 100644 knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/NonFatalRoutingException.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6DestinationHopByHopType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationHopByHopType.kt similarity index 95% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6DestinationHopByHopType.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationHopByHopType.kt index 5d79078..958e4c4 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6DestinationHopByHopType.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationHopByHopType.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.v6.extenions.type +package com.jasonernst.knet.ip.v6.extenions /** * https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#ipv6-parameters-2 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index c0b5ed6..47fa825 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -2,6 +2,7 @@ package com.jasonernst.knet.ip.v6.extenions import com.jasonernst.knet.PacketTooShortException import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v6.extenions.routing.Ipv6Routing import java.nio.ByteBuffer import java.nio.ByteOrder @@ -138,7 +139,7 @@ open class Ipv6ExtensionHeader( extensionList.add(Ipv6DestinationOptions.fromStream(stream, nextHeader, length)) } IpType.IPV6_ROUTE -> { - extensionList.add(Ipv6Routing.fromStream(stream, nextHeader, length)) + extensionList.add(Ipv6Routing.Companion.fromStream(stream, nextHeader, length)) } IpType.AH -> { extensionList.add(Ipv6Authentication.fromStream(stream, nextHeader, length)) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt index acfa3cb..d225b31 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt @@ -1,7 +1,6 @@ package com.jasonernst.knet.ip.v6.extenions import com.jasonernst.knet.PacketTooShortException -import com.jasonernst.knet.ip.v6.extenions.type.Ipv6DestinationHopByHopType import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/FatalRoutingException.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/FatalRoutingException.kt new file mode 100644 index 0000000..e1cc4d6 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/FatalRoutingException.kt @@ -0,0 +1,14 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +/** + * When we throw this, we expect that a handling system would generate an ICMPv6 error message as + * spelled out in the Ipv6Routing usecase where we have a non-zero segment. We also expect this + * if we get a nimrod packet. + * + * If Segments Left is non-zero, the node must discard the packet and + * send an ICMP Parameter Problem, Code 0, message to the packet's + * Source Address, pointing to the unrecognized Routing Type. + */ +class FatalRoutingException( + message: String, +) : Exception(message) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh16.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh16.kt new file mode 100644 index 0000000..96335a2 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh16.kt @@ -0,0 +1,3 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +class Ipv6Crh16 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh32.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh32.kt new file mode 100644 index 0000000..1c1f874 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Crh32.kt @@ -0,0 +1,3 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +class Ipv6Crh32 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp1.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp1.kt new file mode 100644 index 0000000..c002db9 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp1.kt @@ -0,0 +1,3 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +class Ipv6Rfc3692Exp1 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp2.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp2.kt new file mode 100644 index 0000000..4123b47 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Rfc3692Exp2.kt @@ -0,0 +1,3 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +class Ipv6Rfc3692Exp2 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Routing.kt similarity index 57% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Routing.kt index 4ed40f2..58b691c 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Routing.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Routing.kt @@ -1,7 +1,7 @@ -package com.jasonernst.knet.ip.v6.extenions +package com.jasonernst.knet.ip.v6.extenions.routing import com.jasonernst.knet.ip.IpType -import com.jasonernst.knet.ip.v6.extenions.type.Ipv6RoutingType +import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader import java.nio.ByteBuffer import java.nio.ByteOrder @@ -70,28 +70,73 @@ import java.nio.ByteOrder * found at [IANA-RH]. Allocation guidelines for IPv6 Routing Headers * can be found in [RFC5871]. */ -data class Ipv6Routing( +open class Ipv6Routing( override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = MIN_LENGTH, + override val length: UByte = 0u, val routingType: Ipv6RoutingType, - val segmentsLeft: UByte, + val segmentsLeft: UByte = 0u, ) : Ipv6ExtensionHeader(IpType.IPV6_ROUTE, nextHeader, length) { companion object { - const val MIN_LENGTH: UByte = 4u // next header, length, routing type, and segments left - fun fromStream( stream: ByteBuffer, - nextheader: UByte, + nextHeader: UByte, length: UByte, ): Ipv6Routing { val routingType = Ipv6RoutingType.fromKind(stream.get().toUByte()) val segmentsLeft = stream.get().toUByte() - return Ipv6Routing(nextheader, length, routingType, segmentsLeft) + + when (routingType) { + Ipv6RoutingType.SourceRouteDeprecated -> { + // https://www.rfc-editor.org/rfc/rfc5095.html + // An IPv6 node that receives a packet with a destination address + // assigned to it and that contains an RH0 extension header MUST NOT + // execute the algorithm specified in the latter part of Section 4.4 of + // [RFC2460] for RH0. Instead, such packets MUST be processed according + // to the behaviour specified in Section 4.4 of [RFC2460] for a datagram + // that includes an unrecognised Routing Type value, namely: + // + // If Segments Left is zero, the node must ignore the Routing header + // and proceed to process the next header in the packet, whose type + // is identified by the Next Header field in the Routing header. + // + // If Segments Left is non-zero, the node must discard the packet and + // send an ICMP Parameter Problem, Code 0, message to the packet's + // Source Address, pointing to the unrecognized Routing Type. + // + // IPv6 implementations are no longer required to implement RH0 in any + // way. + if (segmentsLeft.toInt() == 0) { + val remaining = (length.toInt() * 8) - MIN_LENGTH_BYTES + stream.position(stream.position() + remaining) + throw NonFatalRoutingException("SourceRouteDeprecated is deprecated") + } else { + throw FatalRoutingException("SourceRouteDeprecated is deprecated") + } + } + Ipv6RoutingType.NimrodDeprecated -> { + // https://www.rfc-editor.org/rfc/rfc2775.html + // The Nimrod Routing Header is deprecated and MUST NOT be used. + throw FatalRoutingException("NimrodDeprecated is deprecated") + } + Ipv6RoutingType.Type2RoutingHeader -> return Ipv6Type2Routing.fromStream( + nextHeader, + length, + routingType, + segmentsLeft, + stream, + ) + // TODO: there's 6 more routing types to implement here + else -> throw IllegalArgumentException("Unsupported routing type: $routingType") + } } } + /** + * This will only put the common fields of the routing header, any type-specific data must be + * handled by the subclass. + */ override fun toByteArray(order: ByteOrder): ByteArray { - val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt()) + val buffer = ByteBuffer.allocate(MIN_LENGTH_BYTES.toInt()) buffer.order(order) buffer.put(super.toByteArray(order)) buffer.put(routingType.kind.toByte()) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6RoutingType.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6RoutingType.kt similarity index 90% rename from knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6RoutingType.kt rename to knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6RoutingType.kt index be61ae6..6de892c 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/type/Ipv6RoutingType.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6RoutingType.kt @@ -1,4 +1,4 @@ -package com.jasonernst.knet.ip.v6.extenions.type +package com.jasonernst.knet.ip.v6.extenions.routing /** * https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#ipv6-parameters-3 diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6RplSourceRouting.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6RplSourceRouting.kt new file mode 100644 index 0000000..663a85e --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6RplSourceRouting.kt @@ -0,0 +1,3 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +class Ipv6RplSourceRouting diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6SegmentRouting.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6SegmentRouting.kt new file mode 100644 index 0000000..6c72e44 --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6SegmentRouting.kt @@ -0,0 +1,3 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +class Ipv6SegmentRouting diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt new file mode 100644 index 0000000..a2d5c7a --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt @@ -0,0 +1,119 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpType +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * https://www.rfc-editor.org/rfc/rfc6275.html#section-6.4 + * + * The type 2 routing header has the following format: + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Next Header | Hdr Ext Len=2 | Routing Type=2|Segments Left=1| + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Reserved | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | + * + + + * | | + * + Home Address + + * | | + * + + + * | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Next Header + * + * 8-bit selector. Identifies the type of header immediately + * following the routing header. Uses the same values as the IPv6 + * Next Header field [6]. + * + * Hdr Ext Len + * + * 2 (8-bit unsigned integer); length of the routing header in + * 8-octet units, not including the first 8 octets. + * + * Routing Type + * + * 2 (8-bit unsigned integer). + * + * Segments Left + * + * 1 (8-bit unsigned integer). + * + * Reserved + * + * 32-bit reserved field. The value MUST be initialized to zero by + * the sender, and MUST be ignored by the receiver. + * + * Home Address + * + * The home address of the destination mobile node. + * + * For a type 2 routing header, the Hdr Ext Len MUST be 2. The Segments + * Left value describes the number of route segments remaining, i.e., + * number of explicitly listed intermediate nodes still to be visited + * before reaching the final destination. Segments Left MUST be 1. The + * ordering rules for extension headers in an IPv6 packet are described + * in Section 4.1 of RFC 2460 [6]. The type 2 routing header defined + * for Mobile IPv6 follows the same ordering as other routing headers. + * If another routing header is present along with a type 2 routing + * header, the type 2 routing header should follow the other routing + * header. A packet containing such nested encapsulation should be + * created as if the inner (type 2) routing header was constructed first + * and then treated as an original packet by header construction process + * for the other routing header. + * + * In addition, the general procedures defined by IPv6 for routing + * headers suggest that a received routing header MAY be automatically + * "reversed" to construct a routing header for use in any response + * packets sent by upper-layer protocols, if the received packet is + * authenticated [6]. This MUST NOT be done automatically for type 2 + * routing headers. + */ +data class Ipv6Type2Routing( + override var nextHeader: UByte = IpType.TCP.value, + val homeAddress: ByteArray, +) : Ipv6Routing( + nextHeader = nextHeader, + length = 2u, + routingType = Ipv6RoutingType.Type2RoutingHeader, + segmentsLeft = 1u, + ) { + companion object { + fun fromStream( + nextHeader: UByte, + length: UByte, + routingType: Ipv6RoutingType, + segmentsLeft: UByte, + stream: ByteBuffer, + ): Ipv6Type2Routing { + val expectedRemaining = ((length * 8u) - 8u).toInt() + if (stream.remaining() < expectedRemaining) { + throw PacketTooShortException("Expected $expectedRemaining bytes, but only have ${stream.remaining()} bytes") + } + if (routingType != Ipv6RoutingType.Type2RoutingHeader) { + throw IllegalArgumentException("Expected routing type 2, but got $routingType") + } + if (segmentsLeft.toUInt() != 1u) { + throw IllegalArgumentException("Expected segments left to be 1, but got $segmentsLeft") + } + if (expectedRemaining != 16) { + throw IllegalArgumentException("Expected 16 bytes of data, but got $expectedRemaining") + } + val homeAddress = ByteArray(16) + stream.get(homeAddress) + return Ipv6Type2Routing(nextHeader, homeAddress) + } + } + + override fun toByteArray(order: ByteOrder): ByteArray { + val buffer = ByteBuffer.allocate(MIN_LENGTH_BYTES.toInt() + (length * 8u).toInt()) + buffer.order(order) + buffer.put(super.toByteArray(order)) + buffer.put(homeAddress) + return buffer.array() + } +} diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/NonFatalRoutingException.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/NonFatalRoutingException.kt new file mode 100644 index 0000000..8c7af3c --- /dev/null +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/NonFatalRoutingException.kt @@ -0,0 +1,9 @@ +package com.jasonernst.knet.ip.v6.extenions.routing + +/** + * When we throw this, we expect that a handling system would skip the routing header and continue + * processing the packet. For instance is we have a source route header, but segments left is 0. + */ +class NonFatalRoutingException( + message: String, +) : Exception(message) diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt index 967f84a..df5070d 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt @@ -1,8 +1,8 @@ package com.jasonernst.knet.ip.v6.extensions import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.v6.extenions.Ipv6DestinationHopByHopType import com.jasonernst.knet.ip.v6.extenions.Ipv6Tlv -import com.jasonernst.knet.ip.v6.extenions.type.Ipv6DestinationHopByHopType import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals From 2613237dc993e3fb97503e11fcdeb95774775351 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 17:42:46 -0700 Subject: [PATCH 18/22] test fixes, increased coverage --- .../com/jasonernst/knet/ip/v6/Ipv6Header.kt | 40 +++-- .../ip/v6/extenions/Ipv6DestinationOptions.kt | 18 +- .../ip/v6/extenions/Ipv6ExtensionHeader.kt | 18 +- .../knet/ip/v6/extenions/Ipv6Fragment.kt | 3 +- .../ip/v6/extenions/Ipv6HopByHopOptions.kt | 15 +- .../knet/ip/v6/extenions/Ipv6Tlv.kt | 4 +- .../ip/v6/extenions/routing/Ipv6Routing.kt | 2 +- .../v6/extenions/routing/Ipv6Type2Routing.kt | 28 ++- .../jasonernst/knet/ip/v6/Ipv6HeaderTest.kt | 13 ++ .../extensions/Ipv6DestinationOptionsTest.kt | 4 +- .../v6/extensions/Ipv6ExtensionHeaderTest.kt | 58 +++++++ .../knet/ip/v6/extensions/Ipv6FragmentTest.kt | 77 +++++++++ .../v6/extensions/routing/Ipv6RoutingTests.kt | 159 ++++++++++++++++++ 13 files changed, 402 insertions(+), 37 deletions(-) create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt create mode 100644 knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/routing/Ipv6RoutingTests.kt diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt index 2295d2b..4bcc496 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt @@ -8,7 +8,6 @@ import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader import com.jasonernst.knet.ip.v6.extenions.Ipv6Fragment import com.jasonernst.knet.nextheader.NextHeader -import org.slf4j.LoggerFactory import java.net.Inet6Address import java.net.InetAddress import java.nio.ByteBuffer @@ -36,8 +35,14 @@ data class Ipv6Header( override val destinationAddress: InetAddress = Inet6Address.getByName("::1"), val extensionHeaders: List = emptyList(), ) : IpHeader { + init { + if (flowLabel > 0xFFFFFu) { + // can't be more than 20 bits + throw IllegalArgumentException("Flow label must be less than 0xFFFFF") + } + } + companion object { - private val logger = LoggerFactory.getLogger(Ipv6Header::class.java) private const val IP6_HEADER_SIZE: UShort = 40u // ipv6 header is not variable like ipv4 // The Per-Fragment headers must consist of the IPv6 header plus any @@ -67,7 +72,7 @@ data class Ipv6Header( } // ensure we have enough capacity in the stream to parse out a full header - val headerAvailable = stream.limit() - stream.position() + val headerAvailable = stream.limit() - start if (headerAvailable < IP6_HEADER_SIZE.toInt()) { throw PacketTooShortException( "Minimum Ipv6 header length is $IP6_HEADER_SIZE bytes. There are only $headerAvailable bytes available", @@ -333,22 +338,25 @@ data class Ipv6Header( return buffer.array() } - override fun getHeaderLength(): UShort = - ( - IP6_HEADER_SIZE + - ( - extensionHeaders.sumOf { - it.getExtensionLengthInBytes() - } - ).toUShort() - ).toUShort() + override fun getHeaderLength(): UShort { + val extensionHeadersLength = + extensionHeaders.sumOf { + it.getExtensionLengthInBytes() + } + return (IP6_HEADER_SIZE.toInt() + extensionHeadersLength).toUShort() + } override fun getTotalLength(): UShort = (getHeaderLength() + payloadLength).toUShort() override fun getPayloadLength(): UShort = payloadLength - override fun toString(): String = - "IPv6Header(version=$version, trafficClass=$trafficClass, flowLabel=$flowLabel, " + - "payloadLength=$payloadLength, protocol=$protocol, hopLimit=$hopLimit, " + - "sourceAddress=$sourceAddress, destinationAddress=$destinationAddress)" + override fun toString(): String { + val extensionHeaderString = extensionHeaders.joinToString(", ", "[", "]") + val string = + "IPv6Header(version=$version, trafficClass=$trafficClass, flowLabel=$flowLabel, " + + "payloadLength=$payloadLength, protocol=$protocol, hopLimit=$hopLimit, " + + "sourceAddress=$sourceAddress, destinationAddress=$destinationAddress, " + + "extensionHeaders=$extensionHeaderString)" + return string + } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt index 1f65bcd..97a97a1 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6DestinationOptions.kt @@ -1,6 +1,7 @@ package com.jasonernst.knet.ip.v6.extenions import com.jasonernst.knet.ip.IpType +import org.slf4j.LoggerFactory import java.nio.ByteBuffer import java.nio.ByteOrder @@ -63,18 +64,26 @@ import java.nio.ByteOrder */ data class Ipv6DestinationOptions( override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = 1u, + override val length: UByte = 0u, val optionData: List = listOf(Ipv6Tlv()), ) : Ipv6ExtensionHeader(IpType.IPV6_OPTS, nextHeader, length) { + private val logger = LoggerFactory.getLogger(javaClass) + init { // dummy check to ensure length matches the option data - val octet8Lengths = optionData.sumOf { it.size() } / 8.0 + val optionDataLength = optionData.sumOf { it.size() } + logger.debug("Option data length: {}", optionDataLength) + val fullLength = 2 + optionDataLength + logger.debug("Full length: {}", fullLength) + val octet8Lengths = (fullLength / 8.0) - 1 if (octet8Lengths != length.toDouble()) { throw IllegalArgumentException("(Option data length / 8 must match the length field, have $octet8Lengths, expecting $length") } } companion object { + private val logger = LoggerFactory.getLogger(Ipv6DestinationOptions::class.java) + /** * Assumes that the stream has already had the nextHeader and length parsed from it */ @@ -83,10 +92,13 @@ data class Ipv6DestinationOptions( nextHeader: UByte, length: UByte, ): Ipv6DestinationOptions { + val limit = (((length + 1u) * 8u) - 2u).toInt() val optionData = mutableListOf() val start = stream.position() - while (stream.position() - start < length.toInt()) { + logger.debug("LENGTH: {} POSITION: {} LIMIT: {}", length, start, limit) + while (stream.position() - start < limit) { val nextTlv = Ipv6Tlv.fromStream(stream) + logger.debug("Parsed TLV: {}", nextTlv) optionData.add(nextTlv) } return Ipv6DestinationOptions(nextHeader, length, optionData) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt index 47fa825..d299c6a 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6ExtensionHeader.kt @@ -121,18 +121,27 @@ open class Ipv6ExtensionHeader( ): List { var currentHeader = firstHeader val extensionList = mutableListOf() - while (currentHeader in requiredExtensionHeaders) { - if (stream.remaining() < 2) { - throw PacketTooShortException("Not enough bytes remaining to determine the length") + + // if its not in the required extension headers, we will break out of the loop + // and return the list we've made so far. This should happen when we hit the TCP, UDP, + // etc header + while (currentHeader in requiredExtensionHeaders && stream.hasRemaining()) { + if (stream.remaining() < MIN_LENGTH_BYTES) { + throw PacketTooShortException( + "Not enough bytes to read the extension header, " + + "require: $MIN_LENGTH_BYTES, remaining: ${stream.remaining()}", + ) } val nextHeader = stream.get().toUByte() val length = stream.get().toUByte() when (currentHeader) { IpType.HOPOPT -> { + println("GOT HOP BY HOP with next header: $nextHeader and length $length") extensionList.add(Ipv6HopByHopOptions.fromStream(stream, nextHeader, length)) } IpType.IPV6_FRAG -> { + println("GOT FRAGMENT with next header: $nextHeader and length $length") extensionList.add(Ipv6Fragment.fromStream(stream, nextHeader)) } IpType.IPV6_OPTS -> { @@ -148,8 +157,7 @@ open class Ipv6ExtensionHeader( extensionList.add(Ipv6EncapsulatingSecurityPayload.fromStream(stream, nextHeader, length)) } else -> { - // todo: pass over the unsupported extension header leaving the stream position just - // beyond it, log a warning, and return an "Unsupported Ipv6 extension header" object + // can't really get here, but kotlin requires an else case throw IllegalArgumentException("Unsupported IPv6 extension header: $currentHeader") } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt index fd739fa..1641f49 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Fragment.kt @@ -271,7 +271,6 @@ import kotlin.experimental.and */ data class Ipv6Fragment( override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = LENGTH, val fragmentOffset: UShort = 0u, val moreFlag: Boolean = false, val identification: UInt = 0u, @@ -288,7 +287,7 @@ data class Ipv6Fragment( val fragmentOffset = ((fragmentOffsetRMByte and 0b111111111111100).toUInt() shr 3).toUShort() val moreFlag = (fragmentOffsetRMByte and 0b1).toInt() == 1 val identification = stream.getInt().toUInt() - return Ipv6Fragment(nextheader, LENGTH, fragmentOffset, moreFlag, identification) + return Ipv6Fragment(nextheader, fragmentOffset, moreFlag, identification) } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt index 4ef41e9..914d3e4 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6HopByHopOptions.kt @@ -30,12 +30,18 @@ import java.nio.ByteOrder */ data class Ipv6HopByHopOptions( override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = 1u, + override val length: UByte = 0u, val optionData: List = listOf(Ipv6Tlv()), ) : Ipv6ExtensionHeader(IpType.HOPOPT, nextHeader, length) { + private val logger = LoggerFactory.getLogger(javaClass) + init { // dummy check to ensure length matches the option data - val octet8Lengths = optionData.sumOf { it.size() } / 8.0 + val optionDataLength = optionData.sumOf { it.size() } + logger.debug("Option data length: {}", optionDataLength) + val fullLength = 2 + optionDataLength + logger.debug("Full length: {}", fullLength) + val octet8Lengths = (fullLength / 8.0) - 1 if (octet8Lengths != length.toDouble()) { throw IllegalArgumentException("(Option data length / 8 must match the length field, have $octet8Lengths, expecting $length") } @@ -52,9 +58,11 @@ data class Ipv6HopByHopOptions( nextHeader: UByte, length: UByte, ): Ipv6HopByHopOptions { + val limit = (((length + 1u) * 8u) - 2u).toInt() val optionData = mutableListOf() val start = stream.position() - while (stream.position() - start < length.toInt()) { + logger.debug("LENGTH: {} POSITION: {} LIMIT: {}", length, start, limit) + while (stream.position() - start < limit) { val nextTlv = Ipv6Tlv.fromStream(stream) logger.debug("Parsed TLV: {}", nextTlv) optionData.add(nextTlv) @@ -67,6 +75,7 @@ data class Ipv6HopByHopOptions( val buffer = ByteBuffer.allocate(getExtensionLengthInBytes()) buffer.order(order) buffer.put(super.toByteArray(order)) + LoggerFactory.getLogger(javaClass).debug("POS: {} LIMIT: {}", buffer.position(), buffer.limit()) optionData.forEach { buffer.put(it.toByteArray()) } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt index d225b31..4934855 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Tlv.kt @@ -113,8 +113,8 @@ import java.nio.ByteOrder */ data class Ipv6Tlv( val optionType: Ipv6DestinationHopByHopType = Ipv6DestinationHopByHopType.PadN, - val optionDataLength: UByte = 6u, - val optionData: ByteArray = ByteArray(6), + val optionDataLength: UByte = 4u, + val optionData: ByteArray = ByteArray(4), ) { init { if (optionDataLength.toInt() != optionData.size) { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Routing.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Routing.kt index 58b691c..9785fb3 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Routing.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Routing.kt @@ -106,7 +106,7 @@ open class Ipv6Routing( // IPv6 implementations are no longer required to implement RH0 in any // way. if (segmentsLeft.toInt() == 0) { - val remaining = (length.toInt() * 8) - MIN_LENGTH_BYTES + val remaining = (length.toInt() * 8) stream.position(stream.position() + remaining) throw NonFatalRoutingException("SourceRouteDeprecated is deprecated") } else { diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt index a2d5c7a..21dbb31 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/routing/Ipv6Type2Routing.kt @@ -82,6 +82,12 @@ data class Ipv6Type2Routing( routingType = Ipv6RoutingType.Type2RoutingHeader, segmentsLeft = 1u, ) { + init { + if (homeAddress.size != 16) { + throw IllegalArgumentException("Home address must be 16 bytes, but got ${homeAddress.size}") + } + } + companion object { fun fromStream( nextHeader: UByte, @@ -90,7 +96,7 @@ data class Ipv6Type2Routing( segmentsLeft: UByte, stream: ByteBuffer, ): Ipv6Type2Routing { - val expectedRemaining = ((length * 8u) - 8u).toInt() + val expectedRemaining = 4 + (length * 8u).toInt() if (stream.remaining() < expectedRemaining) { throw PacketTooShortException("Expected $expectedRemaining bytes, but only have ${stream.remaining()} bytes") } @@ -100,9 +106,7 @@ data class Ipv6Type2Routing( if (segmentsLeft.toUInt() != 1u) { throw IllegalArgumentException("Expected segments left to be 1, but got $segmentsLeft") } - if (expectedRemaining != 16) { - throw IllegalArgumentException("Expected 16 bytes of data, but got $expectedRemaining") - } + stream.position(stream.position() + 4) // skip reserved field val homeAddress = ByteArray(16) stream.get(homeAddress) return Ipv6Type2Routing(nextHeader, homeAddress) @@ -116,4 +120,20 @@ data class Ipv6Type2Routing( buffer.put(homeAddress) return buffer.array() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Ipv6Type2Routing) return false + + if (nextHeader != other.nextHeader) return false + if (!homeAddress.contentEquals(other.homeAddress)) return false + + return true + } + + override fun hashCode(): Int { + var result = nextHeader.hashCode() + result = 31 * result + homeAddress.contentHashCode() + return result + } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt index 1d80bcf..96aa030 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/Ipv6HeaderTest.kt @@ -45,4 +45,17 @@ class Ipv6HeaderTest { val parsedHeader = IpHeader.fromStream(stream) assertEquals(ipv6Header, parsedHeader) } + + @Test fun toFromStream() { + val ipv6Header = Ipv6Header() + val stream = ByteBuffer.wrap(ipv6Header.toByteArray()) + val parsed = Ipv6Header.fromStream(stream) + assertEquals(ipv6Header, parsed) + } + + @Test fun flowLabelTooBig() { + assertThrows { + Ipv6Header(flowLabel = 0x123456u) + } + } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt index 29eb4ba..d86a60a 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6DestinationOptionsTest.kt @@ -51,7 +51,9 @@ class Ipv6DestinationOptionsTest { val options = listOf(Ipv6Tlv()) val optionsSize = (options.sumOf { options.size }) - val destinationOptionsWithOptions = Ipv6DestinationOptions(length = optionsSize.toUInt().toUByte(), optionData = options) + val totalSize = 2 + optionsSize + val octect8Lengths = ((totalSize / 8.0) - 1).toUInt().toUByte() + val destinationOptionsWithOptions = Ipv6DestinationOptions(length = octect8Lengths, optionData = options) destinationOptionsWithOptions.nextHeader = IpType.UDP.value val stream2 = ByteBuffer.wrap(destinationOptionsWithOptions.toByteArray()) val nextHeader2 = stream2.get().toUByte() diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt new file mode 100644 index 0000000..873936a --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt @@ -0,0 +1,58 @@ +package com.jasonernst.knet.ip.v6.extensions + +import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v6.Ipv6Header +import com.jasonernst.knet.ip.v6.extenions.Ipv6DestinationOptions +import com.jasonernst.knet.ip.v6.extenions.Ipv6ExtensionHeader +import com.jasonernst.knet.ip.v6.extenions.Ipv6Fragment +import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions +import com.jasonernst.knet.ip.v6.extenions.routing.Ipv6Type2Routing +import com.jasonernst.packetdumper.stringdumper.StringPacketDumper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.slf4j.LoggerFactory +import java.net.Inet6Address +import java.nio.ByteBuffer +import kotlin.random.Random + +class Ipv6ExtensionHeaderTest { + @Test fun packetTooShort() { + val stream = ByteBuffer.wrap(byteArrayOf(0x00)) + assertThrows { + Ipv6ExtensionHeader.fromStream(stream, IpType.IPV6_FRAG) + } + } + + @Test fun multipleExtensionHeaders() { + val homeAddress = ByteArray(16) + Random.nextBytes(homeAddress) + val ipv6ExtensionHeaders = + listOf( + Ipv6HopByHopOptions(nextHeader = IpType.IPV6_FRAG.value), + Ipv6Fragment(nextHeader = IpType.IPV6_OPTS.value), + Ipv6DestinationOptions(nextHeader = IpType.IPV6_ROUTE.value), + Ipv6Type2Routing(nextHeader = IpType.TCP.value, homeAddress), + ) + val payloadLength = ipv6ExtensionHeaders.sumOf { it.length.toInt() } + val ipv6Header = + Ipv6Header( + trafficClass = 0x09u, + flowLabel = 0x12345u, + protocol = IpType.HOPOPT.value, + hopLimit = 0x40u, + sourceAddress = Inet6Address.getByName("::1"), + destinationAddress = Inet6Address.getByName("::1"), + extensionHeaders = ipv6ExtensionHeaders, + payloadLength = payloadLength.toUShort(), + ) + val stream = ByteBuffer.wrap(ipv6Header.toByteArray()) + + val logger = LoggerFactory.getLogger(javaClass) + logger.debug("{}", StringPacketDumper().dumpBufferToString(stream)) + + val parsed = Ipv6Header.fromStream(stream) + assertEquals(ipv6Header, parsed) + } +} diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt index 1421f87..b712313 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt @@ -3,11 +3,13 @@ package com.jasonernst.knet.ip.v6.extensions import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v6.Ipv6Header import com.jasonernst.knet.ip.v6.extenions.Ipv6Fragment +import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions import com.jasonernst.knet.transport.tcp.TcpHeader import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import java.nio.ByteBuffer import kotlin.random.Random @@ -46,4 +48,79 @@ class Ipv6FragmentTest { assertEquals(tcpHeader, reassembled.second) assertArrayEquals(payload, reassembled.third) } + + @Test fun reassembleEmptyFragments() { + assertThrows { + Ipv6Header.reassemble(listOf()) + } + } + + @Test fun fragmentBadMaxSize() { + assertThrows { + Ipv6Header().fragment(1u, TcpHeader(), ByteArray(0)) + } + } + + @Test fun reassmbleWithoutFirstFragmentHeader() { + val fragments = listOf(Triple(Ipv6Header(), TcpHeader(), ByteArray(0))) + assertThrows { + Ipv6Header.reassemble(fragments) + } + } + + // tests that the reassembly works when the fragments are out of order, + // ie), the first fragment is not the first in the list + @Test fun reassemblyWithExtraExtensionHeaders() { + val payload = ByteArray(5000) + Random.Default.nextBytes(payload) + val tcpHeader = TcpHeader() + val ipPayloadLength = payload.size.toUInt() + tcpHeader.getHeaderLength() + val extensionHeaders = listOf(Ipv6HopByHopOptions()) + val extensionHeaderLength = extensionHeaders.sumOf { it.getExtensionLengthInBytes() } + val totalLength = ipPayloadLength + extensionHeaderLength.toUInt() + val ipv6Header = Ipv6Header(payloadLength = totalLength.toUShort(), extensionHeaders = extensionHeaders) + val fragments = ipv6Header.fragment(1000u, tcpHeader, payload) + assertEquals(6, fragments.size) // 5 won't quite fit with headers + + val reassembled = Ipv6Header.reassemble(fragments) + assertEquals(ipv6Header, reassembled.first) + assertEquals(tcpHeader, reassembled.second) + assertArrayEquals(payload, reassembled.third) + } + + @Test fun reassemblyWithFragmentWithoutFragmentHeader() { + val payload = ByteArray(5000) + Random.Default.nextBytes(payload) + val tcpHeader = TcpHeader() + val ipPayloadLength = payload.size.toUInt() + tcpHeader.getHeaderLength() + val extensionHeaders = listOf(Ipv6HopByHopOptions()) + val extensionHeaderLength = extensionHeaders.sumOf { it.getExtensionLengthInBytes() } + val totalLength = ipPayloadLength + extensionHeaderLength.toUInt() + val ipv6Header = Ipv6Header(payloadLength = totalLength.toUShort(), extensionHeaders = extensionHeaders) + val fragments = ipv6Header.fragment(1000u, tcpHeader, payload) as MutableList + assertEquals(6, fragments.size) // 5 won't quite fit with headers + + val removableExtensionHeaders = fragments[1].first.extensionHeaders + var removableExtensionHeader: Ipv6Fragment? = null + for (extensionHeader in removableExtensionHeaders) { + if (extensionHeader.type == IpType.IPV6_FRAG) { + removableExtensionHeader = extensionHeader as Ipv6Fragment + break + } + } + (removableExtensionHeaders as MutableList).remove(removableExtensionHeader!!) + + assertThrows { Ipv6Header.reassemble(fragments) } + } + + @Test fun reassmbleWithMismatchedId() { + val fragment1 = Ipv6Fragment(identification = 1u) + val fragment2 = Ipv6Fragment(identification = 2u) + val fragments = + listOf( + Triple(Ipv6Header(extensionHeaders = listOf(fragment1)), TcpHeader(), ByteArray(0)), + Triple(Ipv6Header(extensionHeaders = listOf(fragment2)), null, ByteArray(0)), + ) + assertThrows { Ipv6Header.reassemble(fragments) } + } } diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/routing/Ipv6RoutingTests.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/routing/Ipv6RoutingTests.kt new file mode 100644 index 0000000..834a314 --- /dev/null +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/routing/Ipv6RoutingTests.kt @@ -0,0 +1,159 @@ +package com.jasonernst.knet.ip.v6.extensions.routing + +import com.jasonernst.knet.PacketTooShortException +import com.jasonernst.knet.ip.IpType +import com.jasonernst.knet.ip.v6.extenions.routing.FatalRoutingException +import com.jasonernst.knet.ip.v6.extenions.routing.Ipv6Routing +import com.jasonernst.knet.ip.v6.extenions.routing.Ipv6RoutingType +import com.jasonernst.knet.ip.v6.extenions.routing.Ipv6Type2Routing +import com.jasonernst.knet.ip.v6.extenions.routing.NonFatalRoutingException +import com.jasonernst.packetdumper.stringdumper.StringPacketDumper +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import kotlin.random.Random + +class Ipv6RoutingTests { + @Test fun toFromStream() { + val logger = LoggerFactory.getLogger(javaClass) + val homeAddress = ByteArray(16) + Random.nextBytes(homeAddress) + val ipv6Type2Routing = Ipv6Type2Routing(homeAddress = homeAddress) + ipv6Type2Routing.nextHeader = IpType.UDP.value + val stream = ByteBuffer.wrap(ipv6Type2Routing.toByteArray()) + val stringDumper = StringPacketDumper() + logger.debug("{}", stringDumper.dumpBufferToString(stream)) + val nextHeader = stream.get().toUByte() + val length = stream.get().toUByte() + val parsed = Ipv6Routing.fromStream(stream = stream, nextHeader = nextHeader, length = length) + assertEquals(ipv6Type2Routing, parsed) + assertArrayEquals(ipv6Type2Routing.homeAddress, (parsed as Ipv6Type2Routing).homeAddress) + } + + @Test fun type2HashcodeTest() { + val homeAddress = ByteArray(16) + Random.nextBytes(homeAddress) + val ipv6Type2Routing = Ipv6Type2Routing(homeAddress = homeAddress) + + val hashMap = hashMapOf(ipv6Type2Routing to "test") + assertEquals("test", hashMap[ipv6Type2Routing]) + } + + @Test fun type2EqualsTest() { + val homeAddress = ByteArray(16) + Random.nextBytes(homeAddress) + val ipv6Type2Routing = Ipv6Type2Routing(homeAddress = homeAddress) + val ipv6Type2Routing2 = Ipv6Type2Routing(homeAddress = homeAddress) + assertEquals(ipv6Type2Routing, ipv6Type2Routing2) + assertEquals(ipv6Type2Routing, ipv6Type2Routing) + assertNotEquals(ipv6Type2Routing, null) + + val homeAddress2 = ByteArray(16) + Random.nextBytes(homeAddress2) + val ipv6Type2Routing3 = Ipv6Type2Routing(homeAddress = homeAddress2) + assertNotEquals(ipv6Type2Routing, ipv6Type2Routing3) + + val ipv6Type2Routing4 = Ipv6Type2Routing(homeAddress = homeAddress, nextHeader = 1u) + assertNotEquals(ipv6Type2Routing, ipv6Type2Routing4) + } + + @Test fun type2BadHomeAddressSize() { + val homeAddress = ByteArray(15) + assertThrows { + Ipv6Type2Routing(homeAddress = homeAddress) + } + + assertThrows { + val stream = ByteBuffer.wrap(byteArrayOf(0, 0, 0, 0)) + Ipv6Type2Routing.fromStream( + IpType.TCP.value, + 2u, + Ipv6RoutingType.Type2RoutingHeader, + 1u, + stream, + ) + } + } + + @Test fun type2BadStream() { + val homeAddress = ByteArray(20) + Random.nextBytes(homeAddress) + val stream = ByteBuffer.wrap(homeAddress) + assertThrows { + Ipv6Type2Routing.fromStream( + IpType.TCP.value, + 2u, + Ipv6RoutingType.SourceRouteDeprecated, + 1u, + stream, + ) + } + + assertThrows { + Ipv6Type2Routing.fromStream( + IpType.TCP.value, + 2u, + Ipv6RoutingType.Type2RoutingHeader, + 5u, + stream, + ) + } + } + + @Test fun fatalNonFatalException() { + val logger = LoggerFactory.getLogger(javaClass) + val sourceDeprecated = Ipv6Routing(IpType.TCP.value, 0u, Ipv6RoutingType.SourceRouteDeprecated, 0u) + val stream = ByteBuffer.wrap(sourceDeprecated.toByteArray()) + logger.debug("{}", StringPacketDumper().dumpBufferToString(stream)) + val nextHeader = stream.get().toUByte() + val length = stream.get().toUByte() + assertThrows { + Ipv6Routing.fromStream( + stream = stream, + nextHeader = nextHeader, + length = length, + ) + } + val sourceDeprecated2 = Ipv6Routing(IpType.TCP.value, 0u, Ipv6RoutingType.SourceRouteDeprecated, 1u) + val stream2 = ByteBuffer.wrap(sourceDeprecated2.toByteArray()) + val nextHeader2 = stream2.get().toUByte() + val length2 = stream2.get().toUByte() + assertThrows { + Ipv6Routing.fromStream( + stream = stream2, + nextHeader = nextHeader2, + length = length2, + ) + } + + val nimrodDeprecated = Ipv6Routing(IpType.TCP.value, 0u, Ipv6RoutingType.NimrodDeprecated, 0u) + val stream3 = ByteBuffer.wrap(nimrodDeprecated.toByteArray()) + val nextHeader3 = stream3.get().toUByte() + val length3 = stream3.get().toUByte() + assertThrows { + Ipv6Routing.fromStream( + stream = stream3, + nextHeader = nextHeader3, + length = length3, + ) + } + } + + @Test fun unsupportedRoutingType() { + val unsupported = Ipv6Routing(IpType.TCP.value, 0u, Ipv6RoutingType.Reserved, 0u) + val stream = ByteBuffer.wrap(unsupported.toByteArray()) + val nextHeader = stream.get().toUByte() + val length = stream.get().toUByte() + assertThrows { + Ipv6Routing.fromStream( + stream = stream, + nextHeader = nextHeader, + length = length, + ) + } + } +} From 69d20ae796e61a87e47baab254b5a093941c4d85 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 17:58:24 -0700 Subject: [PATCH 19/22] Fix a bug with small payload fragmentation, increase coverage of ipv6header --- .../kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt | 5 +++-- .../knet/ip/v6/extenions/Ipv6Authentication.kt | 2 +- .../ip/v6/extensions/Ipv6ExtensionHeaderTest.kt | 1 + .../knet/ip/v6/extensions/Ipv6FragmentTest.kt | 13 ++++++++++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt index 4bcc496..12b70ae 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/Ipv6Header.kt @@ -262,9 +262,10 @@ data class Ipv6Header( ) val firstPayloadBytes = closestDivisibleBy(maxSize - IP6_HEADER_SIZE - perFragmentHeaderBytes.toUInt() - extAndUpperBytes.toUInt(), 8u) - val firstPair = Triple(firstFragment, nextHeader, payload.sliceArray(0 until firstPayloadBytes.toInt())) + val minPayloadBytes = minOf(firstPayloadBytes, payload.size.toUInt()) + val firstPair = Triple(firstFragment, nextHeader, payload.sliceArray(0 until minPayloadBytes.toInt())) fragments.add(firstPair) - var payloadPosition = firstPayloadBytes.toInt() + var payloadPosition = minPayloadBytes.toInt() while (payloadPosition < payload.size) { val nextPayloadBytes = diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt index 697605a..ee95b5f 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt @@ -8,7 +8,7 @@ import java.nio.ByteBuffer */ class Ipv6Authentication( override var nextHeader: UByte, - override val length: UByte, + override val length: UByte = 0u, ) : Ipv6ExtensionHeader(IpType.AH, nextHeader = nextHeader, length = length) { companion object { // nextheader, length, reserved, SPI, sequence number, ICV diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt index 873936a..537c663 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6ExtensionHeaderTest.kt @@ -34,6 +34,7 @@ class Ipv6ExtensionHeaderTest { Ipv6Fragment(nextHeader = IpType.IPV6_OPTS.value), Ipv6DestinationOptions(nextHeader = IpType.IPV6_ROUTE.value), Ipv6Type2Routing(nextHeader = IpType.TCP.value, homeAddress), + // Ipv6Authentication(nextHeader = IpType.TCP.value), ) val payloadLength = ipv6ExtensionHeaders.sumOf { it.length.toInt() } val ipv6Header = diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt index b712313..d7deea7 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6FragmentTest.kt @@ -2,6 +2,7 @@ package com.jasonernst.knet.ip.v6.extensions import com.jasonernst.knet.ip.IpType import com.jasonernst.knet.ip.v6.Ipv6Header +import com.jasonernst.knet.ip.v6.extenions.Ipv6Authentication import com.jasonernst.knet.ip.v6.extenions.Ipv6Fragment import com.jasonernst.knet.ip.v6.extenions.Ipv6HopByHopOptions import com.jasonernst.knet.transport.tcp.TcpHeader @@ -113,7 +114,7 @@ class Ipv6FragmentTest { assertThrows { Ipv6Header.reassemble(fragments) } } - @Test fun reassmbleWithMismatchedId() { + @Test fun reassembleWithMismatchedId() { val fragment1 = Ipv6Fragment(identification = 1u) val fragment2 = Ipv6Fragment(identification = 2u) val fragments = @@ -123,4 +124,14 @@ class Ipv6FragmentTest { ) assertThrows { Ipv6Header.reassemble(fragments) } } + + @Test fun fragmentWithNonRouterHeader() { + val extensionHeaders = listOf(Ipv6HopByHopOptions(), Ipv6Authentication(IpType.TCP.value)) + val extensionSize = extensionHeaders.sumOf { it.getExtensionLengthInBytes() } + val payload = ByteArray(5000) + Random.Default.nextBytes(payload) + val totalSize = TcpHeader().getHeaderLength() + extensionSize.toUInt() + payload.size.toUInt() + val ipv6Header = Ipv6Header(extensionHeaders = extensionHeaders, payloadLength = totalSize.toUShort()) + ipv6Header.fragment(1000u, TcpHeader(), payload) + } } From 274ca656945672e6d1270f3e904e90e124e338f0 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 18:04:01 -0700 Subject: [PATCH 20/22] updated with rfc6564 ticked off --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c639c43..ba4c3af 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ Kotlin user-space network stack. This can be used for: - [ ] RFC 2474: https://datatracker.ietf.org/doc/html/rfc2474 - [x] IPv6: - [X] WiP: RFC 8200: https://datatracker.ietf.org/doc/html/rfc8200 - - [ ] RFC 6564: https://www.rfc-editor.org/rfc/rfc6564 + - [X] RFC 6564: https://www.rfc-editor.org/rfc/rfc6564 - [ ] RFC 7045: https://www.rfc-editor.org/rfc/rfc7045.html - [ ] RFC: 4302: https://datatracker.ietf.org/doc/html/rfc4302 - [ ] RFC: 4303: https://datatracker.ietf.org/doc/html/rfc4303 - [x] ICMP (via https://github.com/compscidr/icmp) -- [] TCP -- [] UDP +- [ ] TCP +- [ ] UDP From 00e7320378d0d8cfff359e17df18fe20be333baa Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 18:13:04 -0700 Subject: [PATCH 21/22] mark as todo --- .../knet/ip/v6/extenions/Ipv6Authentication.kt | 9 ++++----- .../Ipv6EncapsulatingSecurityPayload.kt | 17 ++++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt index ee95b5f..e2a1047 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6Authentication.kt @@ -11,13 +11,12 @@ class Ipv6Authentication( override val length: UByte = 0u, ) : Ipv6ExtensionHeader(IpType.AH, nextHeader = nextHeader, length = length) { companion object { - // nextheader, length, reserved, SPI, sequence number, ICV - const val MIN_LENGTH = 20 - fun fromStream( stream: ByteBuffer, - nextheader: UByte, + nextHeader: UByte, length: UByte, - ): Ipv6Authentication = Ipv6Authentication(nextheader, length) + ): Ipv6Authentication { + TODO() + } } } diff --git a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt index 4f6c603..f2f24b6 100644 --- a/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt +++ b/knet/src/main/kotlin/com/jasonernst/knet/ip/v6/extenions/Ipv6EncapsulatingSecurityPayload.kt @@ -8,23 +8,22 @@ import java.nio.ByteOrder * https://datatracker.ietf.org/doc/html/rfc4303 */ class Ipv6EncapsulatingSecurityPayload( + val securityParametersIndex: UInt, + val sequenceNumber: UInt, override var nextHeader: UByte = IpType.TCP.value, - override val length: UByte = MIN_LENGTH, + override val length: UByte = 0u, ) : Ipv6ExtensionHeader(IpType.ESP, nextHeader = nextHeader, length = length) { companion object { - const val MIN_LENGTH: UByte = 2u // next header and length with no actual option data - fun fromStream( stream: ByteBuffer, - nextheader: UByte, + nextHeader: UByte, length: UByte, - ): Ipv6EncapsulatingSecurityPayload = Ipv6EncapsulatingSecurityPayload(nextheader, length) + ): Ipv6EncapsulatingSecurityPayload { + TODO() + } } override fun toByteArray(order: ByteOrder): ByteArray { - val buffer = ByteBuffer.allocate(MIN_LENGTH.toInt()) - buffer.order(order) - buffer.put(super.toByteArray(order)) - return buffer.array() + TODO() } } From 50690e713d6a2da656c029e6c0f27edac70c6850 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 8 Oct 2024 18:14:15 -0700 Subject: [PATCH 22/22] add hash test --- .../com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt index df5070d..9181513 100644 --- a/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt +++ b/knet/src/test/kotlin/com/jasonernst/knet/ip/v6/extensions/Ipv6TlvTest.kt @@ -50,4 +50,10 @@ class Ipv6TlvTest { Ipv6Tlv.fromStream(stream) } } + + @Test fun hashTest() { + val tlv1 = Ipv6Tlv(Ipv6DestinationHopByHopType.Pad1, 1u, byteArrayOf(0)) + val hashMap = hashMapOf(tlv1 to 1) + assertEquals(1, hashMap[tlv1]) + } }