Skip to content

Commit

Permalink
Added tcp pcap dumper + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
compscidr committed Aug 8, 2024
1 parent 0487c48 commit 24ed3bd
Show file tree
Hide file tree
Showing 5 changed files with 456 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ package com.jasonernst.packetdumper

import com.jasonernst.packetdumper.ethernet.EtherType
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,125 @@
package com.jasonernst.packetdumper.serverdumper

import com.jasonernst.packetdumper.ethernet.EtherType
import com.jasonernst.packetdumper.ethernet.EthernetHeader.Companion.prependDummyHeader
import com.jasonernst.packetdumper.pcapng.PcapNgEnhancedPacketBlock
import com.jasonernst.packetdumper.pcapng.PcapNgInterfaceDescriptionBlock
import com.jasonernst.packetdumper.pcapng.PcapNgSectionHeaderBlockLive
import com.jasonernst.packetdumper.pcapng.PcapNgSimplePacketBlock
import org.slf4j.LoggerFactory
import java.io.BufferedOutputStream
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketException
import java.nio.ByteBuffer
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean

/**
* Sets up a server listening on the specified TCP port. (default is DEFAULT_PORT)
*
* Wireshark can then connect to it as follows: wireshark -k -i TCP@<ip address>:<port>
*
* Dumps packets over the network in the PCAP-NG format.
*
* @param listenPort The port to listen on.
* @param isSimple If true, the file will be written in the simple format.
* If false, it will be written in the enhanced format.
*/
class PcapNgTcpServerPacketDumper(
val listenPort: Int = DEFAULT_PORT,
private val listenPort: Int = DEFAULT_PORT,
private val isSimple: Boolean = true,
) : AbstractServerPacketDumper() {
private val logger = LoggerFactory.getLogger(javaClass)
private val isRunning = AtomicBoolean(false)

// TODO: use a more kotlin way to do this with coroutines instead of a thread
private var socket: ServerSocket? = null
private var listenerThread: Thread? = null
private val connectionQueue = Collections.synchronizedList(mutableListOf<Socket>())

companion object {
const val DEFAULT_PORT = 19000
}

override fun start() {
TODO("Not yet implemented")
if (isRunning.get()) {
logger.error("Trying to start a server that is already running")
return
}
listenerThread =
Thread({
logger.trace("WiresharkTcpDump starting")
try {
socket = ServerSocket(listenPort)
} catch (e: Exception) {
logger.error("Error starting WiresharkTcpDumper: $e")
return@Thread
}
logger.trace("WiresharkTcpDump listening on {}", socket!!.localSocketAddress)
logAllIPAddresses()
isRunning.set(true)

while (isRunning.get()) {
try {
val client = socket!!.accept()
logger.trace(
"WiresharkTcpDump accepted connection from {}",
client.remoteSocketAddress,
)

try {
val outputstream = BufferedOutputStream(client.getOutputStream())
outputstream.write(PcapNgSectionHeaderBlockLive.toBytes())
outputstream.flush()

val interfaceblock = PcapNgInterfaceDescriptionBlock().toBytes()

outputstream.write(interfaceblock)
outputstream.flush()

connectionQueue.add(client)
} catch (e: Exception) {
logger.warn(
"Error writing to wireshark client, it may have " +
"disconnected from us before we wrote the pcap header: $e",
)
continue
}
} catch (e: Exception) {
logger.warn(
"WiresharkTcpDump error accepting connection, possibly" +
" shutting down: $e",
)
continue
}
}
}, "PcapNgTcpServerPacketDumper listener")
listenerThread!!.start()

// wait for isRunning to be set to true
while (!isRunning.get()) {
Thread.sleep(100)
}
}

override fun stop() {
TODO("Not yet implemented")
if (!isRunning.get()) {
logger.error("Trying to stop a server that is already stopped")
return
}
isRunning.set(false)
socket?.close()
logger.debug("Waiting for listener thread to finish")
listenerThread?.join()
logger.debug("Closing all connections")
for (connection in connectionQueue) {
try {
connection.close()
} catch (e: Exception) {
logger.error("Error closing connection", e)
}
}
}

override fun dumpBuffer(
Expand All @@ -25,6 +129,71 @@ class PcapNgTcpServerPacketDumper(
addresses: Boolean,
etherType: EtherType?,
) {
TODO("Not yet implemented")
if (connectionQueue.isEmpty()) {
// if there are no connected clients, no reason to dump
return
}
val conversionBuffer =
if (etherType != null) {
prependDummyHeader(buffer, offset, length, etherType)
} else {
buffer
}
val packetBlock =
if (isSimple) {
PcapNgSimplePacketBlock(conversionBuffer.array())
} else {
// todo: timestamp
PcapNgEnhancedPacketBlock(conversionBuffer.array())
}

with(connectionQueue.iterator()) {
forEach {
try {
val outputstream = BufferedOutputStream(it.getOutputStream())
outputstream.write(packetBlock.toBytes())
outputstream.flush()
} catch (e: Exception) {
remove()
}
}
}
}

private fun logAllIPAddresses(
excludeInterfaces: List<String> = listOf("bump", "docker", "virbr", "veth", "tailscale", "dummy", "tun"),
) {
try {
val interfaces = java.net.NetworkInterface.getNetworkInterfaces()
if (interfaces == null) {
logger.error("No network interfaces found")
return
}
for (networkInterface in interfaces) {
var excluded = false
for (excludeInterface in excludeInterfaces) {
if (networkInterface.displayName.contains(excludeInterface)) {
excluded = true
break
}
}
if (excluded) {
continue
}
if (networkInterface.isUp.not()) {
continue
}
val addresses = networkInterface.inetAddresses.toList()
if (addresses.isEmpty()) {
continue
}
logger.trace("Network Interface: ${networkInterface.name}")
for (inetAddress in addresses) {
logger.trace(" IP Address: ${inetAddress.hostAddress}")
}
}
} catch (e: SocketException) {
logger.error("Error getting network interfaces", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.jasonernst.packetdumper

import com.jasonernst.packetdumper.filedumper.AbstractFilePacketDumper
import com.jasonernst.packetdumper.pcapng.PcapNgBlock
import com.jasonernst.packetdumper.pcapng.PcapNgInterfaceDescriptionBlock
import com.jasonernst.packetdumper.pcapng.PcapNgSectionHeaderBlockLive
import com.jasonernst.packetdumper.stringdumper.StringPacketDumper
import org.slf4j.LoggerFactory
import java.io.BufferedInputStream
import java.io.FileInputStream
import java.nio.ByteBuffer

object PcapNgTestHelper {
private val logger = LoggerFactory.getLogger(javaClass)

/**
* Verify that the file has the correct headers, advances the readBuffer beyond these headers
* and returns a list of the blocks.
*/
fun verifyHeaders(readBuffer: ByteBuffer): List<PcapNgBlock> {
val pcapBlocks = mutableListOf<PcapNgBlock>()

// 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
}

fun readFile(dumper: AbstractFilePacketDumper): ByteBuffer {
val readBuffer = ByteBuffer.wrap(BufferedInputStream(FileInputStream(dumper.filename)).readAllBytes())
val stringPacketDumper = StringPacketDumper(logger)
stringPacketDumper.dumpBuffer(readBuffer, 0, readBuffer.limit(), false, null)
return readBuffer
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package com.jasonernst.packetdumper.filedumper

import com.jasonernst.packetdumper.PcapNgTestHelper.readFile
import com.jasonernst.packetdumper.PcapNgTestHelper.verifyHeaders
import com.jasonernst.packetdumper.ethernet.EtherType
import com.jasonernst.packetdumper.ethernet.EthernetHeader
import com.jasonernst.packetdumper.pcapng.PcapNgBlock
import com.jasonernst.packetdumper.pcapng.PcapNgEnhancedPacketBlock
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.io.File
import java.nio.ByteBuffer

class TestPcapNgFilePacketDumper {
Expand All @@ -27,44 +24,21 @@ class TestPcapNgFilePacketDumper {

// delete the file created for the test to cleanup
try {
// File(dumper.filename).delete()
// logger.debug("Deleted file ${dumper.filename}")
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<PcapNgBlock> {
val pcapBlocks = mutableListOf<PcapNgBlock>()

// 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 = PcapNgFilePacketDumper("/tmp", "test")
dumper.open()
dumper.close()
val readBuffer = readFile()
val readBuffer = readFile(dumper)
verifyHeaders(readBuffer)
}

Expand All @@ -75,7 +49,7 @@ class TestPcapNgFilePacketDumper {
val buffer = ByteBuffer.wrap(byteArrayOf(0x01, 0x02, 0x03, 0x04))
dumper.dumpBuffer(buffer, 0, buffer.limit(), false, null)
dumper.close()
val readBuffer = readFile()
val readBuffer = readFile(dumper)
verifyHeaders(readBuffer)
val simplePacketBlock = PcapNgSimplePacketBlock.fromStream(readBuffer)
assertEquals(buffer, ByteBuffer.wrap(simplePacketBlock.packetData))
Expand All @@ -88,7 +62,7 @@ class TestPcapNgFilePacketDumper {
val buffer = ByteBuffer.wrap(byteArrayOf(0x01, 0x02, 0x03, 0x04))
dumper.dumpBuffer(buffer, 0, buffer.limit(), false, EtherType.IPv4)
dumper.close()
val readBuffer = readFile()
val readBuffer = readFile(dumper)
verifyHeaders(readBuffer)
val simplePacketBlock = PcapNgSimplePacketBlock.fromStream(readBuffer)

Expand All @@ -113,7 +87,7 @@ class TestPcapNgFilePacketDumper {
dumper.dumpBuffer(buffer, 0, buffer.limit(), false, null)
dumper.dumpBuffer(buffer2, 0, buffer2.limit(), false, null)
dumper.close()
val readBuffer = readFile()
val readBuffer = readFile(dumper)
verifyHeaders(readBuffer)
val simplePacketBlock = PcapNgSimplePacketBlock.fromStream(readBuffer)
val simplePacketBlock2 = PcapNgSimplePacketBlock.fromStream(readBuffer)
Expand All @@ -129,7 +103,7 @@ class TestPcapNgFilePacketDumper {
val buffer = ByteBuffer.wrap(byteArrayOf(0x01, 0x02, 0x03, 0x04))
dumper.dumpBuffer(buffer, 0, buffer.limit(), false, null)
dumper.close()
val readBuffer = readFile()
val readBuffer = readFile(dumper)
verifyHeaders(readBuffer)
val enhancedPacketBlock = PcapNgEnhancedPacketBlock.fromStream(readBuffer)
assertEquals(buffer, ByteBuffer.wrap(enhancedPacketBlock.packetData))
Expand All @@ -142,7 +116,7 @@ class TestPcapNgFilePacketDumper {
val buffer = ByteBuffer.wrap(byteArrayOf(0x01, 0x02, 0x03, 0x04))
dumper.dumpBuffer(buffer, 0, buffer.limit(), false, EtherType.IPv4)
dumper.close()
val readBuffer = readFile()
val readBuffer = readFile(dumper)
verifyHeaders(readBuffer)
val enhancedPacketBlock = PcapNgEnhancedPacketBlock.fromStream(readBuffer)

Expand All @@ -167,7 +141,7 @@ class TestPcapNgFilePacketDumper {
dumper.dumpBuffer(buffer, 0, buffer.limit(), false, null)
dumper.dumpBuffer(buffer2, 0, buffer2.limit(), false, null)
dumper.close()
val readBuffer = readFile()
val readBuffer = readFile(dumper)
verifyHeaders(readBuffer)
val enhancedPacketBlock = PcapNgEnhancedPacketBlock.fromStream(readBuffer)
val enhancedPacketBlock2 = PcapNgEnhancedPacketBlock.fromStream(readBuffer)
Expand Down
Loading

0 comments on commit 24ed3bd

Please sign in to comment.