diff --git a/README.md b/README.md index 6cfe2ef..20c1d35 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,22 @@ A simple library to ping a host using ICMP on Android or Linux JVM. +Currently, can: +- creates a non-privileged ICMPv4 or ICMPv6 socket see: https://keith.github.io/xcode-man-pages/icmp.4.html#Non-privileged_ICMP +- uses the android.system.Os.socket() method on Android and a JNI implementation for Linux JVM +- separate DNS resolution timeout and ICMP timeout +- ping a host using ICMPv4 or ICMPv6 depending on the host resolution, receive and parse the response +- can produce and parse the following ICMP packets: + - echo request + - echo reply + - destination unreachable + - time exceeded + +See: https://www.rfc-editor.org/rfc/rfc792.html and https://datatracker.ietf.org/doc/html/rfc4443 + +TODO: +- a proper android example + ## Usage ### Android @@ -71,20 +87,6 @@ And each consumer of the library can pass the appropriate ICMP implementation. If changes are made to icmp-common, you will need to publish it locally. This can be done by running the `publishToMavenLocal` task in the icmp-common module. -## Todo: -- [x] Structure into a library and an app that uses it -- [x] Release the library to maven central so it can be used -- [ ] Implement icmp v4 -- [ ] Implement icmp v6 -- [x] Implement tests for both Ipv4 and Ipv6 - - tests working on both JVM and Android Instrumented Tests -- [ ] Hookup instrumented tests in CI/CD with actual phone on self-hosted runner -- [x] Investigate whether there is a similar function call for android.system.Os.socket() for - non-Android Kotlin. This will make it possible to run tests on a non-Android JVM, ie) the unit - tests. - - Implemented an abstraction that still uses cmake / native code for the non-android JVM. This - also means this could be released as a cross-platform android / JVM library. - ## Inspirations - Originally motivated by https://github.com/kirillF/icmp-android but updated since it no longer worked diff --git a/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4Header.kt b/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4Header.kt index 8559822..06cc2c5 100644 --- a/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4Header.kt +++ b/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4Header.kt @@ -23,6 +23,9 @@ open class ICMPv4Header( ICMPv4Type.DESTINATION_UNREACHABLE -> { ICMPv4DestinationUnreachablePacket.fromStream(buffer, code, checksum, order) } + ICMPv4Type.TIME_EXCEEDED -> { + ICMPv4TimeExceededPacket.fromStream(buffer, code, checksum, order) + } else -> { throw PacketHeaderException("Unsupported ICMPv4 type") } diff --git a/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4TimeExceededCodes.kt b/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4TimeExceededCodes.kt new file mode 100644 index 0000000..f0ddfab --- /dev/null +++ b/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4TimeExceededCodes.kt @@ -0,0 +1,13 @@ +package com.jasonernst.icmp_common.v4 + +import com.jasonernst.icmp_common.ICMPType + +enum class ICMPv4TimeExceededCodes(override val value: UByte) : ICMPType { + TTL_EXCEEDED(0U), + FRAGMENT_REASSEMBLY_TIME_EXCEEDED(1U) + ; + + companion object { + fun fromValue(value: UByte) = ICMPv4DestinationUnreachableCodes.entries.first { it.value == value } + } +} \ No newline at end of file diff --git a/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4TimeExceededPacket.kt b/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4TimeExceededPacket.kt new file mode 100644 index 0000000..7572b63 --- /dev/null +++ b/icmp-common/src/main/java/com/jasonernst/icmp_common/v4/ICMPv4TimeExceededPacket.kt @@ -0,0 +1,43 @@ +package com.jasonernst.icmp_common.v4 + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class ICMPv4TimeExceededPacket(code: ICMPv4DestinationUnreachableCodes, checksum: UShort = 0u, val data: ByteArray = ByteArray(0)): ICMPv4Header(ICMPv4Type.TIME_EXCEEDED, code.value, checksum) { + companion object { + fun fromStream(buffer: ByteBuffer, code: UByte, checksum: UShort, order: ByteOrder = ByteOrder.BIG_ENDIAN): ICMPv4TimeExceededPacket { + buffer.order(order) + val remainingBuffer = ByteArray(buffer.remaining()) + buffer.get(remainingBuffer) + return ICMPv4TimeExceededPacket(ICMPv4TimeExceededCodes.fromValue(code), checksum, data = remainingBuffer) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ICMPv4TimeExceededPacket + + return data.contentEquals(other.data) + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + data.contentHashCode() + return result + } + + override fun toByteArray(order: ByteOrder): ByteArray { + ByteBuffer.allocate(ICMP_HEADER_MIN_LENGTH.toInt() + data.size).apply { + order(order) + put(super.toByteArray(order)) + put(data) + return array() + } + } + + override fun toString(): String { + return "ICMPv4TimeExceededPacket(code=$code, checksum=$checksum, data=${data.contentToString()})" + } +} \ No newline at end of file diff --git a/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6Header.kt b/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6Header.kt index 3324df6..fbe0cc2 100644 --- a/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6Header.kt +++ b/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6Header.kt @@ -26,6 +26,9 @@ open class ICMPv6Header( ICMPv6Type.DESTINATION_UNREACHABLE -> { ICMPv6DestinationUnreachablePacket.fromStream(buffer, code, checksum, order) } + ICMPv6Type.TIME_EXCEEDED -> { + ICMPv6TimeExceededPacket.fromStream(buffer, code, checksum, order) + } else -> { throw PacketHeaderException("Unsupported ICMPv6 type") } diff --git a/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6TimeExceededCodes.kt b/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6TimeExceededCodes.kt new file mode 100644 index 0000000..932bfb6 --- /dev/null +++ b/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6TimeExceededCodes.kt @@ -0,0 +1,13 @@ +package com.jasonernst.icmp_common.v6 + +import com.jasonernst.icmp_common.ICMPType + +enum class ICMPv6TimeExceededCodes(override val value: UByte) : ICMPType { + HOP_LIMIT_EXCEEDED(0U), + FRAGMENT_REASSEMBLY_TIME_EXCEEDED(1U) + ; + + companion object { + fun fromValue(value: UByte) = ICMPv6TimeExceededCodes.entries.first { it.value == value } + } +} \ No newline at end of file diff --git a/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6TimeExceededPacket.kt b/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6TimeExceededPacket.kt new file mode 100644 index 0000000..9b4b470 --- /dev/null +++ b/icmp-common/src/main/java/com/jasonernst/icmp_common/v6/ICMPv6TimeExceededPacket.kt @@ -0,0 +1,44 @@ +package com.jasonernst.icmp_common.v6 + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class ICMPv6TimeExceededPacket(code: ICMPv6TimeExceededCodes, checksum: UShort = 0u, val data: ByteArray = ByteArray(0)): ICMPv6Header( + ICMPv6Type.TIME_EXCEEDED, code.value, checksum) { + companion object { + fun fromStream(buffer: ByteBuffer, code: UByte, checksum: UShort, order: ByteOrder = ByteOrder.BIG_ENDIAN): ICMPv6TimeExceededPacket { + buffer.order(order) + val remainingBuffer = ByteArray(buffer.remaining()) + buffer.get(remainingBuffer) + return ICMPv6TimeExceededPacket(ICMPv6TimeExceededCodes.fromValue(code), checksum, data = remainingBuffer) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ICMPv6TimeExceededPacket + + return data.contentEquals(other.data) + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + data.contentHashCode() + return result + } + + override fun toByteArray(order: ByteOrder): ByteArray { + ByteBuffer.allocate(ICMP_HEADER_MIN_LENGTH.toInt() + data.size).apply { + order(order) + put(super.toByteArray(order)) + put(data) + return array() + } + } + + override fun toString(): String { + return "ICMPv6TimeExceededPacket(code=$code, checksum=$checksum, data=${data.contentToString()})" + } +} \ No newline at end of file diff --git a/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v4/ICMPv4Test.kt b/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v4/ICMPv4Test.kt index 68b1770..a54b683 100644 --- a/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v4/ICMPv4Test.kt +++ b/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v4/ICMPv4Test.kt @@ -54,4 +54,17 @@ class ICMPv4Test { val parsedPacket = ICMPHeader.fromStream(buffer, true) assertEquals(icmpV4DestinationUnreachablePacket, parsedPacket) } + + @Test + fun timeExceededTest() { + val icmPv4TimeExceededPacket = ICMPv4TimeExceededPacket( + checksum = 0u, + code = ICMPv4DestinationUnreachableCodes.DESTINATION_HOST_UNKNOWN, + data = byteArrayOf(0x01, 0x02, 0x03, 0x04) + ) + val buffer = ByteBuffer.wrap(icmPv4TimeExceededPacket.toByteArray()) + stringDumper.dumpBuffer(buffer, 0, buffer.limit()) + val parsedPacket = ICMPHeader.fromStream(buffer, true) + assertEquals(icmPv4TimeExceededPacket, parsedPacket) + } } \ No newline at end of file diff --git a/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v6/ICMPv6Test.kt b/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v6/ICMPv6Test.kt index 8ce7971..7e06d42 100644 --- a/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v6/ICMPv6Test.kt +++ b/icmp-common/src/test/kotlin/com/jasonernst/icmp_common/v6/ICMPv6Test.kt @@ -55,4 +55,17 @@ class ICMPv6Test { val parsedPacket = ICMPHeader.fromStream(buffer, false) assertEquals(icmpV6DestinationUnreachablePacket, parsedPacket) } + + @Test + fun timeExceededTest() { + val icmPv6TimeExceededPacket = ICMPv6TimeExceededPacket( + checksum = 0u, + code = ICMPv6TimeExceededCodes.HOP_LIMIT_EXCEEDED, + data = byteArrayOf(0x01, 0x02, 0x03, 0x04) + ) + val buffer = ByteBuffer.wrap(icmPv6TimeExceededPacket.toByteArray()) + stringDumper.dumpBuffer(buffer, 0, buffer.limit()) + val parsedPacket = ICMPHeader.fromStream(buffer, false) + assertEquals(icmPv6TimeExceededPacket, parsedPacket) + } } \ No newline at end of file diff --git a/icmp-linux/src/test/kotlin/com/jasonernst/icmp_linux/JVMPingTest.kt b/icmp-linux/src/test/kotlin/com/jasonernst/icmp_linux/JVMPingTest.kt index 72ff631..0133d0a 100644 --- a/icmp-linux/src/test/kotlin/com/jasonernst/icmp_linux/JVMPingTest.kt +++ b/icmp-linux/src/test/kotlin/com/jasonernst/icmp_linux/JVMPingTest.kt @@ -51,7 +51,7 @@ class JVMPingTest { @Test fun pingTimeout() { // first do a ping with a normal timeout to make sure the host works - icmp.ping("www.gov.za") + icmp.ping("www.gov.za", pingTimeoutMS = 2000) // then do one with an aggressive timeout assertThrows { icmp.ping("www.gov.za", pingTimeoutMS = 1)