Skip to content

Commit

Permalink
Merge branch 'main' of github.com:compscidr/icmp
Browse files Browse the repository at this point in the history
  • Loading branch information
compscidr committed Aug 17, 2024
2 parents 43160af + 15a2bd0 commit 08d13c9
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 15 deletions.
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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()})"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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()})"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<IOException> {
icmp.ping("www.gov.za", pingTimeoutMS = 1)
Expand Down

0 comments on commit 08d13c9

Please sign in to comment.