diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/AbstractPcapNgDumper.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/AbstractPcapNgDumper.kt deleted file mode 100644 index 5475f1e..0000000 --- a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/AbstractPcapNgDumper.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.jasonernst.packetdumper - -abstract class AbstractPcapNgDumper : AbstractPacketDumper() diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/AbstractFilePacketDumper.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/AbstractFilePacketDumper.kt index 25d3649..9b4667d 100644 --- a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/AbstractFilePacketDumper.kt +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/AbstractFilePacketDumper.kt @@ -1,11 +1,43 @@ package com.jasonernst.packetdumper.filedumper import com.jasonernst.packetdumper.AbstractPacketDumper +import org.slf4j.LoggerFactory +import java.io.File +import java.time.LocalDateTime +import java.util.concurrent.atomic.AtomicBoolean -abstract class AbstractFilePacketDumper : AbstractPacketDumper() { - abstract var filename: String +/** + * The base class for all file packet dumpers. We leave it as an abstract class so that we don't + * have to implement the dumpBuffer method, and it can be implemented by the specific file dumper. + */ +abstract class AbstractFilePacketDumper( + path: String, + name: String, + ext: String, +) : AbstractPacketDumper() { + private val logger = LoggerFactory.getLogger(javaClass) - abstract fun open() + // keep the filename public to make testing easier + val filename: String = "$path/${name}_${LocalDateTime.now()}.$ext" + protected val isOpen = AtomicBoolean(false) + protected lateinit var file: File + protected var loggedError = false - abstract fun close() + open fun open() { + if (isOpen.get()) { + logger.error("Trying to open a file that is already open") + return + } + file = File(filename) + logger.debug("Opened file $filename") + isOpen.set(true) + } + + open fun close() { + if (!isOpen.get()) { + logger.error("Trying to close a file that is already closed") + return + } + isOpen.set(false) + } } diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/PcapNgFilePacketDumper.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/PcapNgFilePacketDumper.kt index 5bc13e2..7c92783 100644 --- a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/PcapNgFilePacketDumper.kt +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/PcapNgFilePacketDumper.kt @@ -1,19 +1,46 @@ package com.jasonernst.packetdumper.filedumper import com.jasonernst.packetdumper.ethernet.EtherType +import com.jasonernst.packetdumper.ethernet.EthernetHeader.Companion.prependDummyHeader +import com.jasonernst.packetdumper.pcapng.PcapNgInterfaceDescriptionBlock +import com.jasonernst.packetdumper.pcapng.PcapNgSectionHeaderBlockLive +import com.jasonernst.packetdumper.pcapng.PcapNgSimplePacketBlock +import java.io.BufferedOutputStream import java.nio.ByteBuffer +/** + * Dumps packets to a file in the PCAP-NG format. + * + * @param path The path to the file + * @param name The name of the file + * @param isSimple If true, the file will be written in the simple format. + * If false, it will be written in the enhanced format. + */ class PcapNgFilePacketDumper( - override var filename: String, -) : AbstractFilePacketDumper() { + path: String, + name: String, + private val isSimple: Boolean = true, +) : AbstractFilePacketDumper(path, name, "pcapng") { + private lateinit var outputStreamWriter: BufferedOutputStream + override fun open() { - TODO("Not yet implemented") + super.open() + outputStreamWriter = BufferedOutputStream(file.outputStream()) + outputStreamWriter.write(PcapNgSectionHeaderBlockLive.toBytes()) + outputStreamWriter.flush() + outputStreamWriter.write(PcapNgInterfaceDescriptionBlock().toBytes()) + outputStreamWriter.flush() } override fun close() { - TODO("Not yet implemented") + super.close() + outputStreamWriter.flush() + outputStreamWriter.close() } + /** + * Writes a packet to the file. In this case, the address parameter is ignored. + */ override fun dumpBuffer( buffer: ByteBuffer, offset: Int, @@ -21,6 +48,20 @@ class PcapNgFilePacketDumper( addresses: Boolean, etherType: EtherType?, ) { - TODO("Not yet implemented") + // optionally prepend the ethernet dummy header + val conversionBuffer = + if (etherType != null) { + prependDummyHeader(buffer, offset, length, etherType) + } else { + buffer + } + + if (isSimple) { + val packetBlock = PcapNgSimplePacketBlock(conversionBuffer.array()) + outputStreamWriter.write(packetBlock.toBytes()) + outputStreamWriter.flush() + } else { + TODO() + } } } diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/TextFilePacketDumper.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/TextFilePacketDumper.kt index 29ae53d..05cc0ae 100644 --- a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/TextFilePacketDumper.kt +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/filedumper/TextFilePacketDumper.kt @@ -3,56 +3,34 @@ package com.jasonernst.packetdumper.filedumper import com.jasonernst.packetdumper.ethernet.EtherType import com.jasonernst.packetdumper.stringdumper.StringPacketDumper import org.slf4j.LoggerFactory -import java.io.BufferedWriter -import java.io.File import java.nio.ByteBuffer -import java.time.LocalDateTime -import java.util.concurrent.atomic.AtomicBoolean /** - * Dumps buffers into hexdump text files that can be imported by wireshark. The format is as follows: + * Dumps packets to a text file in hexdump format. * - * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline - * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline - * - * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline - * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline - * - * Each block of offsets is separated by a blank newline, and represents a new packet / dump. When - * the new dump starts, the offset can restart at 0. + * If addresses and EtherType is set, the output will be compatible with Wireshark Hexdump. It + * can be imported with `File -> Import from Hexdump.` */ class TextFilePacketDumper( - private val path: String, - private val name: String, -) : AbstractFilePacketDumper() { + path: String, + name: String, +) : AbstractFilePacketDumper(path, name, "txt") { private val logger = LoggerFactory.getLogger(javaClass) private val stringDumper = StringPacketDumper() - private val isOpen = AtomicBoolean(false) - override lateinit var filename: String - private lateinit var file: File - private lateinit var bufferedWriter: BufferedWriter - private var loggedError = false - - override fun open() { - if (isOpen.get()) { - logger.error("Trying to open a file that is already open") - return - } - filename = "$path/${name}_${LocalDateTime.now()}.dump" - file = File(filename) - logger.debug("TextFilePacketDumper opened file $filename") - isOpen.set(true) - } - - override fun close() { - if (!isOpen.get()) { - logger.error("Trying to close a file that is already closed") - return - } - isOpen.set(false) - } - + /** + * Dumps buffers into hexdump text files that can be imported by wireshark. The format is as follows: + * + * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline + * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline + * + * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline + * offset(hex) up to 16 bytes of data per line, separated by a space, ending in a newline + * + * Each block of offsets is separated by a blank newline, and represents a new packet / dump. When + * the new dump starts, the offset can restart at 0. + * + */ override fun dumpBuffer( buffer: ByteBuffer, offset: Int, diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/AbstractPcapNgDumper.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/AbstractPcapNgDumper.kt new file mode 100644 index 0000000..a3591d2 --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/AbstractPcapNgDumper.kt @@ -0,0 +1,5 @@ +package com.jasonernst.packetdumper.pcapng + +import com.jasonernst.packetdumper.AbstractPacketDumper + +abstract class AbstractPcapNgDumper : AbstractPacketDumper() diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgBlock.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgBlock.kt new file mode 100644 index 0000000..87f163b --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgBlock.kt @@ -0,0 +1,7 @@ +package com.jasonernst.packetdumper.pcapng + +interface PcapNgBlock { + fun size(): UInt + + fun toBytes(): ByteArray +} diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgBlockType.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgBlockType.kt new file mode 100644 index 0000000..d2e0ac0 --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgBlockType.kt @@ -0,0 +1,18 @@ +package com.jasonernst.packetdumper.pcapng + +// https://www.ietf.org/staging/draft-tuexen-opsawg-pcapng-02.html#name-block-types +enum class PcapNgBlockType( + val value: UInt, +) { + INTERFACE_DESCRIPTION_BLOCK(1u), + SIMPLE_PACKET_BLOCK(3u), + NAME_RESOLUTION_BLOCK(4u), + INTERFACE_STATISTICS_BLOCK(5u), + ENHANCED_PACKET_BLOCK(6u), + SECTION_HEADER_BLOCK(0x0A0D0D0AU), + ; + + companion object { + fun fromValue(value: UInt) = entries.first { it.value == value } + } +} diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgEnhancedPacketBlock.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgEnhancedPacketBlock.kt new file mode 100644 index 0000000..c8472f2 --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgEnhancedPacketBlock.kt @@ -0,0 +1,3 @@ +package com.jasonernst.packetdumper.pcapng + +class PcapNgEnhancedPacketBlock diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgException.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgException.kt new file mode 100644 index 0000000..831c83c --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgException.kt @@ -0,0 +1,12 @@ +package com.jasonernst.packetdumper.pcapng + +import java.lang.Exception + +class PcapNgException : Exception { + constructor(message: String?) : super(message) {} + constructor(message: String?, throwable: Throwable?) : super(message, throwable) {} + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgInterfaceDescriptionBlock.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgInterfaceDescriptionBlock.kt new file mode 100644 index 0000000..ad17ee5 --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgInterfaceDescriptionBlock.kt @@ -0,0 +1,69 @@ +package com.jasonernst.packetdumper.pcapng + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-interface-description-block + */ +class PcapNgInterfaceDescriptionBlock( + private val linkType: PcapNgLinkType = PcapNgLinkType.ETHERNET, +) : PcapNgBlock { + companion object { + // block type (4) + 2x block len (4) + link type (2) + reserved (2) + snap len (4) + private const val HEADER_BLOCK_LENGTH = 20u + + /** + * Reads a section header block from a stream, throws a PcapNgException if the block is not a + * section header live block. + */ + fun fromStream(stream: ByteBuffer): PcapNgInterfaceDescriptionBlock { + val startingPosition = stream.position() + stream.order(ByteOrder.LITTLE_ENDIAN) + val blockType = stream.int + if (blockType != PcapNgBlockType.INTERFACE_DESCRIPTION_BLOCK.value.toInt()) { + throw PcapNgException( + "Block type is not an interface description block, expected ${PcapNgBlockType.INTERFACE_DESCRIPTION_BLOCK.value} got $blockType", + ) + } + val blockLength = stream.int + if (blockLength != HEADER_BLOCK_LENGTH.toInt()) { + throw PcapNgException("Block length is not the expected length") + } + val linkType = stream.short + val reserved = stream.short + val snapLen = stream.int + if (snapLen != 0) { + throw PcapNgException("Snap length is not the expected value") + } + val secondBlockLength = stream.int + if (secondBlockLength != blockLength) { + throw PcapNgException("Second block length is not the expected value") + } + if (stream.position() - startingPosition != HEADER_BLOCK_LENGTH.toInt()) { + throw PcapNgException( + "Stream position is not at the end of the block, expected ${startingPosition + HEADER_BLOCK_LENGTH.toInt()} got ${stream.position()}", + ) + } + return PcapNgInterfaceDescriptionBlock(PcapNgLinkType.fromValue(linkType.toUShort())) + } + } + + override fun size(): UInt = HEADER_BLOCK_LENGTH + + override fun toBytes(): ByteArray { + val buffer = ByteBuffer.allocate(HEADER_BLOCK_LENGTH.toInt()) + buffer.order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(PcapNgBlockType.INTERFACE_DESCRIPTION_BLOCK.value.toInt()) + buffer.putInt(HEADER_BLOCK_LENGTH.toInt()) + + buffer.putShort(linkType.value.toShort()) + buffer.putShort(0) // reserved + buffer.putInt(0) // snap length + + // no options + + buffer.putInt(HEADER_BLOCK_LENGTH.toInt()) + return buffer.array() + } +} diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgLinkType.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgLinkType.kt new file mode 100644 index 0000000..d907773 --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgLinkType.kt @@ -0,0 +1,43 @@ +package com.jasonernst.packetdumper.pcapng + +// https://www.tcpdump.org/linktypes.html +enum class PcapNgLinkType( + val value: UShort, +) { + NULL(0U), + ETHERNET(1U), + AX25(3U), + IEEE802_5(6U), + ARCNET_BSD(7U), + SLIP(8U), + PPP(9U), + FDDI(10U), + PPP_HDLC(50U), + PPP_ETHER(51U), + ATM_RFC1483(100U), + RAW(101U), + C_HDLC(104U), + IEEE802_11(105U), + FRELAY(107U), + LOOP(108U), + LINUX_SLL(113U), + LTALK(114U), + PFLOG(117U), + IEEE802_11_PRISM(119U), + IP_OVER_FC(122U), + SUNATM(123U), + IEEE802_11_RADIOTAP(127U), + ARCNET_LINUX(129U), + APPLE_IP_OVER_IEEE1394(138U), + MTP2_WITH_PHDR(139U), + MTP2(140U), + MTP3(141U), + SCCP(142U), + DOCSIS(143U), + LINUX_IRDA(144U), + ; + + companion object { + fun fromValue(value: UShort) = entries.first { it.value == value } + } +} diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgSectionHeaderBlockLive.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgSectionHeaderBlockLive.kt new file mode 100644 index 0000000..cb01bab --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgSectionHeaderBlockLive.kt @@ -0,0 +1,74 @@ +package com.jasonernst.packetdumper.pcapng + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +object PcapNgSectionHeaderBlockLive : PcapNgBlock { + private const val ENDIAN_MAGIC = 0x1A2B3C4D + private const val MAJOR_VERSION: UShort = 1U + private const val MINOR_VERSION: UShort = 0U + private const val SECTION_LENGTH = -1L // since we are doing a live capture, we don't know the length + + // block type (4) + 2x block len (4) + magic (4) + major (2) + minor (2) + section len (8) + private const val HEADER_BLOCK_LENGTH = 28u + + override fun size(): UInt = HEADER_BLOCK_LENGTH + + override fun toBytes(): ByteArray { + val buffer = ByteBuffer.allocate(HEADER_BLOCK_LENGTH.toInt()) + buffer.order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(PcapNgBlockType.SECTION_HEADER_BLOCK.value.toInt()) + buffer.putInt(HEADER_BLOCK_LENGTH.toInt()) + buffer.putInt(ENDIAN_MAGIC) + buffer.putShort(MAJOR_VERSION.toShort()) + buffer.putShort(MINOR_VERSION.toShort()) + buffer.putLong(SECTION_LENGTH) + buffer.putInt(HEADER_BLOCK_LENGTH.toInt()) + return buffer.array() + } + + /** + * Reads a section header block from a stream, throws a PcapNgException if the block is not a + * section header live block. + */ + fun fromStream(stream: ByteBuffer): PcapNgSectionHeaderBlockLive { + val startPosition = stream.position() + stream.order(ByteOrder.LITTLE_ENDIAN) + val blockType = stream.int + if (blockType != PcapNgBlockType.SECTION_HEADER_BLOCK.value.toInt()) { + throw PcapNgException( + "Block type is not a section header block, expected ${PcapNgBlockType.SECTION_HEADER_BLOCK.value} got $blockType", + ) + } + val blockLength = stream.int + if (blockLength != HEADER_BLOCK_LENGTH.toInt()) { + throw PcapNgException("Block length is not the expected length") + } + val endianMagic = stream.int + if (endianMagic != ENDIAN_MAGIC) { + throw PcapNgException("Endian magic is not the expected value") + } + val majorVersion = stream.short.toUShort() + if (majorVersion != MAJOR_VERSION) { + throw PcapNgException("Major version is not the expected value") + } + val minorVersion = stream.short.toUShort() + if (minorVersion != MINOR_VERSION) { + throw PcapNgException("Minor version is not the expected value") + } + val sectionLength = stream.long + if (sectionLength != SECTION_LENGTH) { + throw PcapNgException("Section length is not the expected value") + } + val blockTotalLength = stream.int + if (blockTotalLength != HEADER_BLOCK_LENGTH.toInt()) { + throw PcapNgException("Block total length is not the expected value") + } + if (stream.position() - startPosition != HEADER_BLOCK_LENGTH.toInt()) { + throw PcapNgException( + "Stream position is not at the end of the block, expected ${startPosition + HEADER_BLOCK_LENGTH.toInt()} got ${stream.position()}", + ) + } + return PcapNgSectionHeaderBlockLive + } +} diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgSimplePacketBlock.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgSimplePacketBlock.kt new file mode 100644 index 0000000..1c393c6 --- /dev/null +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/pcapng/PcapNgSimplePacketBlock.kt @@ -0,0 +1,82 @@ +package com.jasonernst.packetdumper.pcapng + +import com.jasonernst.packetdumper.stringdumper.StringPacketDumper +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.abs +import kotlin.math.ceil + +/** + * Implements: + * https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-simple-packet-block + */ +class PcapNgSimplePacketBlock( + val packetData: ByteArray, +) : PcapNgBlock { + companion object { + private val logger = LoggerFactory.getLogger(this::class.java) + + // block type (4) + 2x block len (4) + orig length (4) + const val HEADER_BLOCK_LENGTH_WITHOUT_PACKET = 16u + const val PRE_HEADER = 8u + const val TRAILER = 4u + + /** + * Reads a simple packet block from a stream, throws a PcapNgException if the block is not + * a simple packet block. The stream should be positioned at the start of the block. By + * the end of the function, the stream will be positioned at the start of the next block. + */ + fun fromStream(stream: ByteBuffer): PcapNgSimplePacketBlock { + val stringPacketDumper = StringPacketDumper(logger) + stringPacketDumper.dumpBuffer(stream, stream.position(), stream.remaining()) + stream.order(ByteOrder.LITTLE_ENDIAN) + val blockType = stream.int + if (blockType != PcapNgBlockType.SIMPLE_PACKET_BLOCK.value.toInt()) { + throw PcapNgException( + "Block type is not a simple packet block, expected ${PcapNgBlockType.SIMPLE_PACKET_BLOCK.value} got $blockType", + ) + } + val blockLength = stream.int + val packetLength = stream.int + val packetData = ByteArray(packetLength) + stream.get(packetData) + val zeroPad = blockLength - HEADER_BLOCK_LENGTH_WITHOUT_PACKET.toInt() - packetLength + stream.position(stream.position() + zeroPad) + val trailer = stream.int + if (trailer != blockLength) { + throw PcapNgException("Trailer is not the expected value") + } + return PcapNgSimplePacketBlock(packetData) + } + } + + /** + * The size of the block is the size of the packet data rounded up to the nearest 4 bytes, plus + * the header and trailer + */ + override fun size(): UInt { + val nearest4 = 4u * (ceil(abs(packetData.size / 4.0))).toUInt() + return HEADER_BLOCK_LENGTH_WITHOUT_PACKET + nearest4 + } + + private fun zeroPadSize(): Int { + val nearest4 = 4 * (ceil(abs(packetData.size / 4.0))).toInt() + return nearest4 - packetData.size + } + + override fun toBytes(): ByteArray { + val buffer = ByteBuffer.allocate(size().toInt()) + buffer.order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(PcapNgBlockType.SIMPLE_PACKET_BLOCK.value.toInt()) + buffer.putInt(size().toInt()) + buffer.putInt(packetData.size) + buffer.put(packetData) + // zero pad + for (i in 0 until zeroPadSize()) { + buffer.put(0) + } + buffer.putInt(size().toInt()) + return buffer.array() + } +} diff --git a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/stringdumper/StringPacketDumper.kt b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/stringdumper/StringPacketDumper.kt index d880add..e0ef05f 100644 --- a/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/stringdumper/StringPacketDumper.kt +++ b/packetdumper/src/main/kotlin/com/jasonernst/packetdumper/stringdumper/StringPacketDumper.kt @@ -102,7 +102,7 @@ class StringPacketDumper( output.append(" ") } } - conversionBuffer.position(startingPosition) + buffer.position(startingPosition) return output.toString() } } diff --git a/packetdumper/src/test/kotlin/com/jasonernst/packetdumper/filedumper/TestPcapNgFilePacketDumper.kt b/packetdumper/src/test/kotlin/com/jasonernst/packetdumper/filedumper/TestPcapNgFilePacketDumper.kt new file mode 100644 index 0000000..068bc6c --- /dev/null +++ b/packetdumper/src/test/kotlin/com/jasonernst/packetdumper/filedumper/TestPcapNgFilePacketDumper.kt @@ -0,0 +1,102 @@ +package com.jasonernst.packetdumper.filedumper + +import com.jasonernst.packetdumper.ethernet.EtherType +import com.jasonernst.packetdumper.ethernet.EthernetHeader +import com.jasonernst.packetdumper.pcapng.PcapNgBlock +import com.jasonernst.packetdumper.pcapng.PcapNgInterfaceDescriptionBlock +import com.jasonernst.packetdumper.pcapng.PcapNgSectionHeaderBlockLive +import com.jasonernst.packetdumper.pcapng.PcapNgSimplePacketBlock +import com.jasonernst.packetdumper.stringdumper.StringPacketDumper +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import java.io.BufferedInputStream +import java.io.FileInputStream +import java.nio.ByteBuffer + +class TestPcapNgFilePacketDumper { + private val logger = LoggerFactory.getLogger(javaClass) + private val dumper = PcapNgFilePacketDumper("/tmp", "test") + + @AfterEach + fun tearDown() { + // just in case a test fails, we want to make sure the file is closed + dumper.close() + + // delete the file created for the test to cleanup + try { + // File(dumper.filename).delete() + logger.debug("Deleted file ${dumper.filename}") + } catch (e: Exception) { + // ignore + } + } + + /** + * Verify that the file has the correct headers, advances the readBuffer beyond these headers + * and returns a list of the blocks. + */ + private fun verifyHeaders(readBuffer: ByteBuffer): List { + val pcapBlocks = mutableListOf() + + // we expect the file to start with a section header block + pcapBlocks.add(PcapNgSectionHeaderBlockLive.fromStream(readBuffer)) + + // we expect the file to have an interface description block + pcapBlocks.add(PcapNgInterfaceDescriptionBlock.fromStream(readBuffer)) + + return pcapBlocks + } + + private fun readFile(): ByteBuffer { + val readBuffer = ByteBuffer.wrap(BufferedInputStream(FileInputStream(dumper.filename)).readAllBytes()) + val stringPacketDumper = StringPacketDumper(logger) + stringPacketDumper.dumpBuffer(readBuffer, 0, readBuffer.limit(), false, null) + return readBuffer + } + + /** + * Test that the file is created and the correct blocks are written at the start. + */ + @Test fun testOpenClose() { + dumper.open() + dumper.close() + val readBuffer = readFile() + verifyHeaders(readBuffer) + } + + @Test + fun testDumpSimplePacketBlock() { + dumper.open() + val buffer = ByteBuffer.wrap(byteArrayOf(0x01, 0x02, 0x03, 0x04)) + dumper.dumpBuffer(buffer, 0, buffer.limit(), false, null) + dumper.close() + val readBuffer = readFile() + verifyHeaders(readBuffer) + val simplePacketBlock = PcapNgSimplePacketBlock.fromStream(readBuffer) + assertEquals(buffer, ByteBuffer.wrap(simplePacketBlock.packetData)) + } + + @Test + fun testDumpSinglePacketBlockWithDummyEth() { + dumper.open() + val buffer = ByteBuffer.wrap(byteArrayOf(0x01, 0x02, 0x03, 0x04)) + dumper.dumpBuffer(buffer, 0, buffer.limit(), false, EtherType.IPv4) + dumper.close() + val readBuffer = readFile() + verifyHeaders(readBuffer) + val simplePacketBlock = PcapNgSimplePacketBlock.fromStream(readBuffer) + + // parse out the ethernet dummy header + val packetData = ByteBuffer.wrap(simplePacketBlock.packetData) + EthernetHeader.fromStream(packetData) + + // remaining data in the stream should now match the original buffer + val recvData = ByteBuffer.allocate(packetData.remaining()) + recvData.put(packetData) + recvData.rewind() + + assertEquals(buffer, recvData) + } +}