Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
compscidr committed Aug 7, 2024
1 parent 59478f3 commit ace03df
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 0 deletions.
1 change: 1 addition & 0 deletions packetdumper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
38 changes: 38 additions & 0 deletions packetdumper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
id("java-library")
alias(libs.plugins.jetbrains.kotlin.jvm)
alias(libs.plugins.kotlinter)
id("jacoco")
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
jvmToolchain(17)
}

tasks.jacocoTestReport {
reports {
xml.required = true
html.required = false
}
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
finalizedBy("jacocoTestReport")
}

jacoco {
toolVersion = "0.8.12"
}

dependencies {
api(libs.slf4j.api)
testImplementation(libs.bundles.test)
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.logback.classic)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.jasonernst.packetdumper

import java.nio.ByteBuffer
import java.util.concurrent.atomic.AtomicBoolean

abstract class AbstractPacketDumper {
protected var running = AtomicBoolean(false)

/**
* Dumps a ByteBuffer starting at position offset, and going until position length. If length
* + offset is greater than buffer.remaining(), it will dump until the end of the buffer.
*
* If addresses is true, it will dump the address offsets with offset being position zero. The
* addresses will be dumped at the left hand side of the dump, followed by whitespace, and then
* the hexdump of the buffer. This only really applies to text dumpers or string dumpers and not
* pcap dumpers.
*
* If etherType is set, a dummy ethernet header will be prepended to to the start of the dump.
* This is particularly useful for pcap format with wireshark since it allows for wireshark to
* just recognize the traffic immediately.
*
* Dumping the buffer should not change the:
* - position, limit, etc of the buffer.
*/
abstract fun dumpBuffer(
buffer: ByteBuffer,
offset: Int = 0,
length: Int = buffer.remaining(),
addresses: Boolean = false,
etherType: EtherType? = null,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.jasonernst.packetdumper

abstract class AbstractPcapNgDumper : AbstractPacketDumper()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.jasonernst.packetdumper

import java.nio.ByteBuffer

/**
* A dummy implementation of the packet dumper that does nothing. Useful so that you can use a
* real dumper in a debug implementation, and switch to the DummyPacketDumper for production.
*/
object DummyPacketDumper : AbstractPacketDumper() {
override fun dumpBuffer(
buffer: ByteBuffer,
offset: Int,
length: Int,
addresses: Boolean,
etherType: EtherType?,
) {
// do nothing
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.jasonernst.packetdumper

/**
* https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml#ieee-802-numbers-1
* https://en.wikipedia.org/wiki/EtherType
*/
enum class EtherType(val value: UShort) {
BUMP(0x0101u),
IPv4(0x0800u),
ARP(0x0806u),
IPv6(0x86DDu),
DETECT(0xFFFFu), // used if we want to detect the type from the packet itself
;

companion object {
fun fromValue(value: UShort) = entries.first { it.value == value }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.jasonernst.packetdumper

import org.slf4j.LoggerFactory
import java.nio.ByteBuffer
import java.security.InvalidParameterException

/**
* Bare minimal Ethernet frame.
*
* Mostly used to help us easily add a dummy header to packet dumps for Wireshark.
*
* https://en.wikipedia.org/wiki/Ethernet_frame
*/
class EthernetHeader(private var destination: MACAddress, private var source: MACAddress, private var type: EtherType) {
companion object {
const val IP4_VERSION: UByte = 4u
const val IP6_VERSION: UByte = 6u
const val ETHERNET_HEADER_LENGTH = 14u
private val logger = LoggerFactory.getLogger(this::class.java)

/**
* Returns a new ByteBuffer with the Ethernet header prepended to the front of the buffer.
*
* The buffer is copied starting from offset continuing for length bytes, or until the end of
* the buffer if length + offset > buffer.limit(). The source and destination addresses are
*/
fun prependDummyHeader(
buffer: ByteBuffer,
offset: Int = 0,
length: Int = 0,
etherType: EtherType
): ByteBuffer {
val startingPosition = buffer.position()
val totalLength = minOf(length, buffer.limit() - offset)
if (totalLength < length) {
logger.warn("Trying to dump more bytes than are in the buffer. Dumping up to buffer limit.")
}
val newBuffer = ByteBuffer.allocate(totalLength + ETHERNET_HEADER_LENGTH.toInt())
val detectedEtherType = if (etherType == EtherType.DETECT) {
val ipVersion = buffer.get(offset).toUByte()
buffer.position(startingPosition)
getEtherTypeFromIPVersionByte(ipVersion)
} else {
etherType
}
newBuffer.put(dummyEthernet(detectedEtherType).toBytes())
newBuffer.put(buffer.array(), offset, totalLength)
newBuffer.rewind()
return newBuffer
}

fun fromStream(stream: ByteBuffer): EthernetHeader {
val destination = MACAddress.fromStream(stream)
val source = MACAddress.fromStream(stream)
val type = EtherType.fromValue(stream.short.toUShort())
return EthernetHeader(destination, source, type)
}

/**
* Attempts to detect the ethertype based on the byte passed to it. If its not a known
* ethertype, it will just leave the type as 0xFFFF (detect) which maps to RESERVED in the
* actual mapping.
*/
fun getEtherTypeFromIPVersionByte(ipVersion: UByte): EtherType {
return when (ipVersion) {
IP4_VERSION -> EtherType.IPv4
IP6_VERSION -> EtherType.IPv6
else -> {
logger.warn("Couldn't detect etherType, got $ipVersion")
EtherType.DETECT
}
}
}

fun dummyEthernet(etherType: EtherType): EthernetHeader {
return EthernetHeader(MACAddress.DUMMY_MAC_SOURCE, MACAddress.DUMMY_MAC_DEST, etherType)
}
}

fun size() = ETHERNET_HEADER_LENGTH

fun toBytes(): ByteArray {
val buffer = ByteBuffer.allocate(ETHERNET_HEADER_LENGTH.toInt())
buffer.put(destination.bytes)
buffer.put(source.bytes)
buffer.putShort(type.value.toShort())
return buffer.array()
}

override fun toString(): String {
return "EthernetHeader(destination=$destination, source=$source, type=$type, size=${size()})"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.jasonernst.packetdumper

import java.nio.ByteBuffer

/**
* Represents an Ethernet or Wi-Fi MAC address.
*/
class MACAddress {
var bytes: ByteArray

init {
bytes = ByteArray(6)
}

constructor(bytes: ByteArray) {
if (bytes.size != 6) {
throw IllegalArgumentException("MAC address must be 6 bytes")
}
this.bytes = bytes
}

constructor(address: String) {
this.bytes = address.split(":").map { it.toInt(16).toByte() }.toByteArray()
}

override fun toString(): String {
return bytes.joinToString(":") { it.toString(16).padStart(2, '0') }
}

companion object {
fun fromStream(stream: ByteBuffer): MACAddress {
val bytes = ByteArray(6)
stream.get(bytes)
return MACAddress(bytes)
}

val DUMMY_MAC_SOURCE = MACAddress("14:c0:3e:55:0b:35")
val DUMMY_MAC_DEST = MACAddress("74:d0:2b:29:a5:18")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jasonernst.packetdumper.filedumper

import com.jasonernst.packetdumper.AbstractPacketDumper

abstract class AbstractFilePacketDumper : AbstractPacketDumper() {
abstract var filename: String

abstract fun open()

abstract fun close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.jasonernst.packetdumper.serverdumper

import com.jasonernst.packetdumper.AbstractPacketDumper

abstract class AbstractServerPacketDumper : AbstractPacketDumper() {
abstract fun start()

abstract fun stop()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.jasonernst.packetdumper.stringdumper

import com.jasonernst.packetdumper.AbstractPacketDumper
import com.jasonernst.packetdumper.EtherType
import com.jasonernst.packetdumper.EthernetHeader
import com.jasonernst.packetdumper.EthernetHeader.Companion
import com.jasonernst.packetdumper.EthernetHeader.Companion.prependDummyHeader
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.ByteBuffer

/**
* A packet dumper that will dump packets to a string. This is useful for live debugging. It can
* either log to a slf4f logger if one is provided, or it can write to stdout. By default it does
* neither, however, it can still be used manually by calling `dumpBufferToString`.
* @param packetLogger The slf4j logger to dump the buffers to.
* @param writeToStdOut If true, will write to stdout (if the logger is null)
*/
class StringPacketDumper(private val packetLogger: Logger? = null, private val writeToStdOut: Boolean = false) : AbstractPacketDumper() {
private val logger = LoggerFactory.getLogger(javaClass)

/**
* In this case calling dump buffer will:
* a) write to an slf4j logger if the constructor arg is not null
* b) write to stdout if no logger has been above, and if the writeToStdout constructor arg is true
*/
override fun dumpBuffer(
buffer: ByteBuffer,
offset: Int,
length: Int,
addresses: Boolean,
etherType: EtherType?,
) {
val hexString = dumpBufferToString(buffer, offset, length, addresses, etherType)
if (packetLogger != null) {
packetLogger.info(hexString)
} else if (writeToStdOut) {
println(hexString)
}
}

/**
* This function will dump the buffer to a string and return it.
*
* Given the byte buffer, converts the bytes to a Hexadecimal string. Puts the buffer back to
* the position it started in before returning.
*
* - Spaces each byte
* - Newline after every 16 bytes
*
* Optionally:
* - Prints the offset addresses at the start of the line
* - Addresses start at 0x00000000 and increment by 16, regardless of the offset. The buffer
* position + the offset = the starting address of 0.
*
* @param buffer the buffer to convert
* @param offset the offset to start at
* @param length the number of bytes to convert
* (if offset + length > buffer.limit(), will print up to buffer.limit())
* @param addresses if true, will print the offset addresses at the start of the line
* (compatible with a wireshark hex dump import)
* @param etherType the ethertype of the buffer if known, or 0xFFFF if unknown. null if it is
* not desired to prepend with an ethernet header
*/
fun dumpBufferToString(
buffer: ByteBuffer,
offset: Int = 0,
length: Int = 0,
addresses: Boolean = false,
etherType: EtherType? = null,
): String {
val startingPosition = buffer.position()
// optionally prepend the ethernet dummy header
val conversionBuffer = if (etherType != null) {
prependDummyHeader(buffer, offset, length, etherType)
} else {
val totalLength = minOf(length, buffer.limit() - offset)
if (totalLength < length) {
logger.warn("Trying to dump more bytes than are in the buffer. Dumping up to buffer limit.")
}
val newBuffer = ByteBuffer.allocate(totalLength)
newBuffer.put(buffer.array(), offset, totalLength)
newBuffer.rewind()
newBuffer
}
val output = StringBuilder()
var i = 0
while (i < conversionBuffer.limit()) {
if (addresses && (i % 16 == offset % 16)) {
output.append(String.format("%08X ", i - (offset % 16) - (16 * (offset / 16))))
}
output.append(String.format("%02X", buffer.get()))
i++
if (i > offset && i % 16 == offset % 16) {
output.append("\n")
} else if (i < (length + offset)) {
output.append(" ")
}
}
buffer.position(startingPosition)
return output.toString()
}
}
Loading

0 comments on commit ace03df

Please sign in to comment.