Add Wireshark packet dissector plugin and associated tools

This commit is contained in:
Allen Hill
2024-08-23 11:02:40 -07:00
parent fadd2c4259
commit e857bbb09f
26 changed files with 1475 additions and 0 deletions
@@ -0,0 +1,19 @@
Copyright (c) 2020 Christian Rorvik <christian.rorvik@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,21 @@
name = "PcapTools"
uuid = "222fe7e8-3f39-464a-bf97-d9bbb753f246"
authors = ["Christian Rorvik <christian.rorvik@gmail.com>"]
version = "1.1.1"
[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
UnixTimes = "ab1a18e7-b408-4913-896c-624bb82ed7f4"
UnsafeArrays = "c4a57d5a-5b31-53a6-b365-19f8c011fbd6"
[compat]
UnixTimes = "1"
UnsafeArrays = "1"
julia = "1.4"
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
@@ -0,0 +1,5 @@
# PcapTools
[![Build Status](https://travis-ci.com/ancapdev/PcapTools.jl.svg?branch=master)](https://travis-ci.com/ancapdev/PcapTools.jl)
[![Coverage Status](https://coveralls.io/repos/github/ancapdev/PcapTools.jl/badge.svg)](https://coveralls.io/github/ancapdev/PcapTools.jl)
@@ -0,0 +1,26 @@
module PcapTools
using Dates
using Mmap
using UnixTimes
using UnsafeArrays
export PcapHeader, RecordHeader
export PcapRecord
export PcapReader, PcapStreamReader, PcapBufferReader
export PcapWriter, PcapStreamWriter
export LINKTYPE_NULL, LINKTYPE_ETHERNET
export splitcap
abstract type PcapReader end
abstract type PcapWriter end
include("pcap_header.jl")
include("record_header.jl")
include("record.jl")
include("buffer_reader.jl")
include("stream_reader.jl")
include("stream_writer.jl")
include("splitcap.jl")
end
@@ -0,0 +1,96 @@
"""
Reads pcap data from an array of bytes.
"""
mutable struct PcapBufferReader <: PcapReader
data::Vector{UInt8}
raw_header::Vector{UInt8}
header::PcapHeader
offset::Int64
mark::Int64
usec_mul::Int64
bswapped::Bool
@doc """
PcapBufferReader(data::Vector{UInt8})
Create reader over `data`. Will read and process pcap header,
and yield records through `read(::PcapBufferReader)`.
"""
function PcapBufferReader(data::Vector{UInt8})
length(data) < sizeof(PcapHeader) && throw(EOFError())
rh = data[1:sizeof(PcapHeader)]
h = unsafe_load(Ptr{PcapHeader}(pointer(data)))
h, bswapped, nanotime = process_header(h)
new(data, rh, h, sizeof(h), -1, nanotime ? 1 : 1000, bswapped)
end
end
"""
PcapBufferReader(path::AbstractString)
Memory map file in `path` and create PcapBufferReader over its content.
"""
function PcapBufferReader(path::AbstractString)
io = open(path)
data = Mmap.mmap(io)
PcapBufferReader(data)
end
function Base.close(x::PcapBufferReader)
x.data = UInt8[]
x.offset = 0
nothing
end
Base.length(x::PcapBufferReader) = length(x.data)
Base.position(x::PcapBufferReader) = x.offset; nothing
Base.seek(x::PcapBufferReader, pos) = x.offset = pos; nothing
function Base.mark(x::PcapBufferReader)
x.mark = x.offset
x.mark
end
function Base.unmark(x::PcapBufferReader)
if x.mark >= 0
x.mark = -1
true
else
false
end
end
Base.ismarked(x::PcapBufferReader) = x.mark >= 0
function Base.reset(x::PcapBufferReader)
!ismarked(x) && error("PcapBufferReader not marked")
x.offset = x.mark
x.mark = -1
x.offset
end
Base.eof(x::PcapBufferReader) = (length(x) - x.offset) < sizeof(RecordHeader)
"""
read(x::PcapBufferReader) -> PcapRecord
Read one record from pcap data.
Throws `EOFError` if no more data available.
"""
@inline function Base.read(x::PcapBufferReader)
eof(x) && throw(EOFError())
record_offset = x.offset
p = pointer(x.data) + record_offset
GC.@preserve x begin
h = unsafe_load(Ptr{RecordHeader}(p))
end
if x.bswapped
h = bswap(h)
end
t1 = (h.ts_sec + x.header.thiszone) * 1_000_000_000
t2 = Int64(h.ts_usec) * x.usec_mul
t = UnixTime(Dates.UTInstant(Nanosecond(t1 + t2)))
x.offset += sizeof(RecordHeader) + h.incl_len
x.offset > length(x) && error("Insufficient data in pcap record")
PcapRecord(h, t, x.data, record_offset)
end
@@ -0,0 +1,46 @@
# NOTE: Not using @enum because it craps out when displaying unknown values
const LINKTYPE_NULL = UInt32(0)
const LINKTYPE_ETHERNET = UInt32(1)
struct PcapHeader
magic::UInt32
version_major::UInt16
version_minor::UInt16
thiszone::Int32
sigfigs::UInt32
snaplen::UInt32
linktype::UInt32
end
function Base.bswap(x::PcapHeader)
PcapHeader(
bswap(x.magic),
bswap(x.version_major),
bswap(x.version_minor),
bswap(x.thiszone),
bswap(x.sigfigs),
bswap(x.snaplen),
bswap(x.linktype))
end
function process_header(x::PcapHeader)
if x.magic == 0xa1b2c3d4
bswapped = false
nanotime = false
elseif x.magic == 0xd4c3b2a1
bswapped = true
nanotime = false
elseif x.magic == 0xa1b23c4d
bswapped = false
nanotime = true
elseif x.magic == 0x4d3cb2a1
bswapped = true
nanotime = true
else
throw(ArgumentError("Invalid pcap header"))
end
if bswapped
x = bswap(x)
end
x, bswapped, nanotime
end
@@ -0,0 +1,25 @@
"""
Record of pcap data
"""
struct PcapRecord
header::RecordHeader
timestamp::UnixTime
underlying_data::Vector{UInt8}
record_offset::Int
end
@inline function record_field_(x::PcapRecord, ::Val{:data})
offset = getfield(x, :record_offset) + sizeof(RecordHeader)
len = Int(getfield(x, :header).incl_len)
UnsafeArray{UInt8, 1}(pointer(getfield(x, :underlying_data)) + offset, (len,))
end
@inline function record_field_(x::PcapRecord, ::Val{:raw})
offset = getfield(x, :record_offset)
len = sizeof(RecordHeader) + getfield(x, :header).incl_len
UnsafeArray{UInt8, 1}(pointer(getfield(x, :underlying_data)) + offset, (len,))
end
@inline record_field_(x::PcapRecord, ::Val{f}) where {f} = getfield(x, f)
@inline Base.getproperty(x::PcapRecord, f::Symbol) = record_field_(x, Val(f))
@@ -0,0 +1,14 @@
struct RecordHeader
ts_sec::UInt32
ts_usec::UInt32
incl_len::UInt32
orig_len::UInt32
end
function Base.bswap(x::RecordHeader)
RecordHeader(
bswap(x.ts_sec),
bswap(x.ts_usec),
bswap(x.incl_len),
bswap(x.orig_len))
end
@@ -0,0 +1,102 @@
strip_nothing_(::Type{Union{Nothing, T}}) where T = T
strip_nothing_(::Type{T}) where T = T
progress_noop_(n) = nothing
mutable struct SplitCapOutput{S}
work_buffer::Vector{UInt8}
complete_buffers::Channel{Vector{UInt8}}
stream::S
end
# Since Julia (as of 1.5) doesn't support task migration,
# continue with new task after each buffer write, to rebalance across threads
function write_one_and_continue_(output::SplitCapOutput, free_buffers::Channel, pending::Threads.Atomic{Int})
i = iterate(output.complete_buffers)
if i === nothing
Threads.atomic_sub!(pending, 1)
return nothing
end
b, _ = i
write(output.stream, b)
empty!(b)
put!(free_buffers, b)
Threads.@spawn write_one_and_continue_($output, $free_buffers, $pending)
end
function splitcap(
::Type{KeyType},
::Type{StreamType},
reader::PcapReader,
record2key,
key2stream,
progress_callback = progress_noop_;
own_streams::Bool = true
) where {KeyType, StreamType}
buffer_size = 1024 * 1024 * 2
max_pending_buffers = 4
outputs = Dict{KeyType, SplitCapOutput{StreamType}}()
free_buffers = Channel{Vector{UInt8}}(Inf)
n = 0
pending = Threads.Atomic{Int}(0)
try
while !eof(reader)
record = read(reader)
dst = record2key(record)
if dst isa KeyType
output = get!(outputs, dst) do
stream = key2stream(dst)
buffer = sizehint!(UInt8[], buffer_size + 1500)
own_streams && append!(buffer, reader.raw_header)
output = SplitCapOutput{StreamType}(
buffer,
Channel{Vector{UInt8}}(max_pending_buffers),
stream)
Threads.atomic_add!(pending, 1)
Threads.@spawn write_one_and_continue_($output, $free_buffers, $pending)
output
end
append!(output.work_buffer, record.raw)
if length(output.work_buffer) >= buffer_size
put!(output.complete_buffers, output.work_buffer)
if isready(free_buffers)
output.work_buffer = take!(free_buffers)
else
output.work_buffer = sizehint!(UInt8[], buffer_size + 1500)
end
end
end
n += 1
progress_callback(n)
GC.safepoint()
end
for output in values(outputs)
if !isempty(output.work_buffer)
put!(output.complete_buffers, output.work_buffer)
end
close(output.complete_buffers)
end
while pending[] != 0
sleep(0.1)
end
finally
if own_streams
for output in values(outputs)
close(output.stream)
end
end
end
nothing
end
function splitcap(
reader::PcapReader,
record2key,
key2stream,
progress_callback = progress_noop_;
kwargs...
)
KeyType = strip_nothing_(Core.Compiler.return_type(record2key, Tuple{PcapRecord}))
StreamType = Core.Compiler.return_type(key2stream, Tuple{KeyType})
splitcap(KeyType, StreamType, reader, record2key, key2stream, progress_callback; kwargs...)
end
@@ -0,0 +1,63 @@
"""
Reads pcap data from a stream.
"""
mutable struct PcapStreamReader{Src <: IO} <: PcapReader
src::Src
raw_header::Vector{UInt8}
header::PcapHeader
usec_mul::Int64
bswapped::Bool
record_buffer::Vector{UInt8}
@doc """
PcapStreamReader(src::IO)
Create reader over `src`. Will read and process pcap header,
and yield records through `read(::PcapStreamReader)`.
"""
function PcapStreamReader(src::Src) where {Src <: IO}
raw_header = read(src, sizeof(PcapHeader))
length(raw_header) != sizeof(PcapHeader) && throw(EOFError())
h = GC.@preserve raw_header unsafe_load(Ptr{PcapHeader}(pointer(raw_header)))
header, bswapped, nanotime = process_header(h)
new{Src}(src, raw_header, header, nanotime ? 1 : 1000, bswapped, zeros(UInt8, 9000 + sizeof(RecordHeader)))
end
end
"""
PcapStreamReader(path)
Open file at `path` and create PcapStreamReader over its content.
"""
PcapStreamReader(path::AbstractString) = PcapStreamReader(open(path))
Base.close(x::PcapStreamReader) = close(x.src)
Base.position(x::PcapStreamReader) = position(x.src)
Base.seek(x::PcapStreamReader, pos) = seek(x.src, pos)
Base.mark(x::PcapStreamReader) = mark(x.src)
Base.unmark(x::PcapStreamReader) = unmark(x.src)
Base.ismarked(x::PcapStreamReader) = ismarked(x.src)
Base.reset(x::PcapStreamReader) = reset(x.src)
Base.eof(x::PcapStreamReader) = eof(x.src)
"""
read(x::PcapStreamReader) -> PcapRecord
Read one record from pcap data. Record is valid until next read().
Throws `EOFError` if no more data available.
"""
function Base.read(x::PcapStreamReader)
p = pointer(x.record_buffer)
GC.@preserve x begin
unsafe_read(x.src, p, sizeof(RecordHeader))
h = unsafe_load(Ptr{RecordHeader}(p))
if x.bswapped
h = bswap(h)
end
unsafe_read(x.src, p + sizeof(RecordHeader), h.incl_len)
end
t1 = (h.ts_sec + x.header.thiszone) * 1_000_000_000
t2 = Int64(h.ts_usec) * x.usec_mul
t = UnixTime(Dates.UTInstant(Nanosecond(t1 + t2)))
PcapRecord(h, t, x.record_buffer, 0)
end
@@ -0,0 +1,30 @@
struct PcapStreamWriter{Dst <: IO} <: PcapWriter
dst::Dst
function PcapStreamWriter{Dst}(dst::Dst; thiszone = 0, snaplen = 65535, linktype = LINKTYPE_ETHERNET) where {Dst <: IO}
h = PcapHeader(
0xa1b23c4d,
0x0002,
0x0004,
thiszone,
0,
snaplen,
linktype)
write(dst, reinterpret(UInt8, [h]))
new(dst)
end
end
PcapStreamWriter(io::IO; kwargs...) = PcapStreamWriter{typeof(io)}(io; kwargs...)
PcapStreamWriter(path::AbstractString; kwargs...) = PcapStreamWriter(open(path, "w"); kwargs...)
Base.close(x::PcapStreamWriter) = close(x.dst)
function Base.write(x::PcapStreamWriter, timestamp::UnixTime, data)
sec, nsec = fldmod(Dates.value(timestamp), 1_000_000_000)
data_length = length(data)
h = RecordHeader(sec, nsec, data_length, data_length)
write(x.dst, reinterpret(UInt8, [h]))
write(x.dst, collect(data))
nothing
end