forked from osmocom/wireshark
![Hadriel Kaplan](/assets/img/avatar_default.png)
Provide a way for Lua-based dissectors to invoke tcp_dissect_pdus() to make TCP-based dissection easier. Bug: 9851 Change-Id: I91630ebf1f1fc1964118b6750cc34238e18a8ad3 Reviewed-on: https://code.wireshark.org/review/6778 Reviewed-by: Alexis La Goutte <alexis.lagoutte@gmail.com> Reviewed-by: Hadriel Kaplan <hadrielk@yahoo.com> Tested-by: Hadriel Kaplan <hadrielk@yahoo.com>
453 lines
16 KiB
Lua
453 lines
16 KiB
Lua
----------------------------------------
|
|
--
|
|
-- author: Hadriel Kaplan <hadriel@128technology.com>
|
|
-- Copyright (c) 2015, Hadriel Kaplan
|
|
-- This code is in the Public Domain, or the BSD (3 clause) license
|
|
-- if Public Domain does not apply in your country.
|
|
--
|
|
-- Version: 1.0
|
|
--
|
|
------------------------------------------
|
|
--[[
|
|
This code is a plugin for Wireshark, to dissect Quagga FPM Netlink
|
|
protocol messages over TCP.
|
|
|
|
This script is used for testing, so it does some odd things:
|
|
* it dissects the FPM in two ways, controlled by a pref setting:
|
|
1) using the desegment_offset/desegment_len method
|
|
2) using the dissect_tcp_pdus() method
|
|
* it removes any existing FPM dissector; there isn't one right now
|
|
but there likely will be in the future.
|
|
|
|
Wireshark has a "Netlink" protocol dissector, but it currently expects
|
|
to be running on a Linux cooked-mode SLL header and link type. That's
|
|
because Netlink has traditionally been used between the Linux kernel
|
|
and user-space apps. But the open-source Quagga, zebra, and the
|
|
commercial ZebOS routing products also send Netlink messages over TCP
|
|
to other processes or even outside the box, to a "Forwarding Plane Manager"
|
|
(FPM) that controls forwarding-plane devices (typically hardware).
|
|
|
|
The Netlink message is encapsulated within an FPM header, which identifies
|
|
an FPM message version (currently 1), the type of message it contains
|
|
(namely a Netlink message), and its length.
|
|
|
|
So we have:
|
|
struct fpm_msg_hdr_t
|
|
{
|
|
uint8_t version;
|
|
uint8_t msg_type;
|
|
uint16_t msg_len;
|
|
}
|
|
followed by a Netlink message.
|
|
]]----------------------------------------
|
|
|
|
|
|
----------------------------------------
|
|
-- do not modify this table
|
|
local debug_level = {
|
|
DISABLED = 0,
|
|
LEVEL_1 = 1,
|
|
LEVEL_2 = 2
|
|
}
|
|
|
|
-- set this DEBUG to debug_level.LEVEL_1 to enable printing debug_level info
|
|
-- set it to debug_level.LEVEL_2 to enable really verbose printing
|
|
-- note: this will be overridden by user's preference settings
|
|
local DEBUG = debug_level.LEVEL_1
|
|
|
|
local default_settings =
|
|
{
|
|
debug_level = DEBUG,
|
|
enabled = true, -- whether this dissector is enabled or not
|
|
port = 2620,
|
|
max_msg_len = 4096,
|
|
desegment = true, -- whether to TCP desegement or not
|
|
dissect_tcp = false, -- whether to use the dissect_tcp_pdus method or not
|
|
subdissect = true, -- whether to call sub-dissector or not
|
|
subdiss_type = wtap.NETLINK, -- the encap we get the subdissector for
|
|
}
|
|
|
|
local dprint = function() end
|
|
local dprint2 = function() end
|
|
local function reset_debug_level()
|
|
if default_settings.debug_level > debug_level.DISABLED then
|
|
dprint = function(...)
|
|
print(table.concat({"Lua:", ...}," "))
|
|
end
|
|
|
|
if default_settings.debug_level > debug_level.LEVEL_1 then
|
|
dprint2 = dprint
|
|
end
|
|
end
|
|
end
|
|
-- call it now
|
|
reset_debug_level()
|
|
|
|
|
|
----------------------------------------
|
|
-- creates a Proto object, but doesn't register it yet
|
|
local fpmProto = Proto("fpm", "FPM Header")
|
|
|
|
|
|
----------------------------------------
|
|
-- a function to convert tables of enumerated types to valstring tables
|
|
-- i.e., from { "name" = number } to { number = "name" }
|
|
local function makeValString(enumTable)
|
|
local t = {}
|
|
for name,num in pairs(enumTable) do
|
|
t[num] = name
|
|
end
|
|
return t
|
|
end
|
|
|
|
local MsgType = {
|
|
NONE = 0,
|
|
NETLINK = 1,
|
|
}
|
|
local msgtype_valstr = makeValString(MsgType)
|
|
|
|
|
|
----------------------------------------
|
|
-- a table of all of our Protocol's fields
|
|
local hdr_fields =
|
|
{
|
|
version = ProtoField.uint8 ("fpm.version", "Version", base.DEC),
|
|
msg_type = ProtoField.uint8 ("fpm.type", "Type", base.DEC, msgtype_valstr),
|
|
msg_len = ProtoField.uint16("fpm.length", "Length", base.DEC),
|
|
}
|
|
|
|
-- create a flat array table of the above that can be registered
|
|
local pfields = {}
|
|
|
|
-- recursive function to flatten the table into pfields
|
|
local function flattenTable(tbl)
|
|
for k,v in pairs(tbl) do
|
|
if type(v) == 'table' then
|
|
flattenTable(v)
|
|
else
|
|
pfields[#pfields+1] = v
|
|
end
|
|
end
|
|
end
|
|
-- call it
|
|
flattenTable(hdr_fields)
|
|
|
|
-- register them
|
|
fpmProto.fields = pfields
|
|
|
|
dprint2("fpmProto ProtoFields registered")
|
|
|
|
|
|
----------------------------------------
|
|
-- some forward "declarations" of helper functions we use in the dissector
|
|
local createSLL
|
|
|
|
-- due to a bug in wireshark, we need to keep newly created tvb's for longer
|
|
-- than the duration of the dissect function
|
|
local tvbs = {}
|
|
|
|
function fpmProto.init()
|
|
tvbs = {}
|
|
end
|
|
|
|
|
|
local FPM_MSG_HDR_LEN = 4
|
|
|
|
----------------------------------------
|
|
-- the following function is used for the new dissect_tcp_pdus method
|
|
-- this one returns the length of the full message
|
|
local function get_fpm_length(tvbuf, pktinfo, offset)
|
|
dprint2("FPM get_fpm_length function called")
|
|
local lengthVal = tvbuf:range(offset + 2, 2):uint()
|
|
|
|
if lengthVal > default_settings.max_msg_len then
|
|
-- too many bytes, invalid message
|
|
dprint("FPM message length is too long: ", lengthVal)
|
|
lengthVal = tvbuf:len()
|
|
end
|
|
|
|
return lengthVal
|
|
end
|
|
|
|
-- the following is the dissection function called for
|
|
-- the new dissect_tcp_pdus method
|
|
local function dissect_fpm_pdu(tvbuf, pktinfo, root)
|
|
dprint2("FPM dissect_fpm_pdu function called")
|
|
|
|
local lengthTvbr = tvbuf:range(2, 2)
|
|
local lengthVal = lengthTvbr:uint()
|
|
|
|
-- set the protocol column to show our protocol name
|
|
pktinfo.cols.protocol:set("FPM")
|
|
|
|
-- We start by adding our protocol to the dissection display tree.
|
|
local tree = root:add(fpmProto, tvbuf:range(offset, lengthVal))
|
|
|
|
local versionTvbr = tvbuf:range(0, 1)
|
|
local versionVal = versionTvbr:uint()
|
|
tree:add(hdr_fields.version, versionTvbr)
|
|
|
|
local msgTypeTvbr = tvbuf:range(1, 1)
|
|
local msgTypeVal = msgTypeTvbr:uint()
|
|
tree:add(hdr_fields.msg_type, msgTypeTvbr)
|
|
|
|
tree:add(hdr_fields.msg_len, lengthTvbr)
|
|
|
|
local result
|
|
if (versionVal == 1) and (msgTypeVal == MsgType.NETLINK) then
|
|
-- it carries a Netlink message, so we're going to create
|
|
-- a fake Linux SLL header for the built-in Netlink dissector
|
|
local payload = tvbuf:raw(FPM_MSG_HDR_LEN, lengthVal - FPM_MSG_HDR_LEN)
|
|
result = createSLL(payload)
|
|
end
|
|
|
|
-- looks good, go dissect it
|
|
if result then
|
|
-- ok now the hard part - try calling a sub-dissector?
|
|
-- only if settings/prefs told us to of course...
|
|
if default_settings.subdissect then
|
|
dprint2("FPM trying sub-dissector for wtap encap type:", default_settings.subdiss_type)
|
|
|
|
-- due to a bug in wireshark, we need to keep newly created tvb's for longer
|
|
-- than the duration of the dissect function
|
|
tvbs[#tvbs+1] = ByteArray.new(result, true):tvb("Netlink Message")
|
|
DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvbs[#tvbs], pktinfo, root)
|
|
|
|
-- local tvb = ByteArray.new(result, true):tvb("Netlink Message")
|
|
-- DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvb, pktinfo, root)
|
|
dprint2("FPM returning from sub-dissector")
|
|
end
|
|
else
|
|
dprint("FPM header not correctly dissected")
|
|
end
|
|
|
|
return lengthVal, 0
|
|
end
|
|
|
|
|
|
----------------------------------------
|
|
-- the following function is used for dissecting using the
|
|
-- old desegment_offset/desegment_len method
|
|
-- it's a separate function because we run over TCP and thus might
|
|
-- need to parse multiple messages in a single segment
|
|
local function dissect(tvbuf, pktinfo, root, offset, origlen)
|
|
dprint2("FPM dissect function called")
|
|
|
|
local pktlen = origlen - offset
|
|
|
|
if pktlen < FPM_MSG_HDR_LEN then
|
|
-- we need more bytes
|
|
pktinfo.desegment_offset = offset
|
|
pktinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT
|
|
return 0, DESEGMENT_ONE_MORE_SEGMENT
|
|
end
|
|
|
|
local lengthTvbr = tvbuf:range(offset + 2, 2)
|
|
local lengthVal = lengthTvbr:uint()
|
|
|
|
if lengthVal > default_settings.max_msg_len then
|
|
-- too many bytes, invalid message
|
|
dprint("FPM message length is too long: ", lengthVal)
|
|
return pktlen, 0
|
|
end
|
|
|
|
if pktlen < lengthVal then
|
|
dprint2("Need more bytes to desegment FPM")
|
|
pktinfo.desegment_offset = offset
|
|
pktinfo.desegment_len = (lengthVal - pktlen)
|
|
return 0, -(lengthVal - pktlen)
|
|
end
|
|
|
|
-- set the protocol column to show our protocol name
|
|
pktinfo.cols.protocol:set("FPM")
|
|
|
|
-- We start by adding our protocol to the dissection display tree.
|
|
local tree = root:add(fpmProto, tvbuf:range(offset, lengthVal))
|
|
|
|
local versionTvbr = tvbuf:range(offset, 1)
|
|
local versionVal = versionTvbr:uint()
|
|
tree:add(hdr_fields.version, versionTvbr)
|
|
|
|
local msgTypeTvbr = tvbuf:range(offset + 1, 1)
|
|
local msgTypeVal = msgTypeTvbr:uint()
|
|
tree:add(hdr_fields.msg_type, msgTypeTvbr)
|
|
|
|
tree:add(hdr_fields.msg_len, lengthTvbr)
|
|
|
|
local result
|
|
if (versionVal == 1) and (msgTypeVal == MsgType.NETLINK) then
|
|
-- it carries a Netlink message, so we're going to create
|
|
-- a fake Linux SLL header for the built-in Netlink dissector
|
|
local payload = tvbuf:raw(offset + FPM_MSG_HDR_LEN, lengthVal - FPM_MSG_HDR_LEN)
|
|
result = createSLL(payload)
|
|
end
|
|
|
|
-- looks good, go dissect it
|
|
if result then
|
|
-- ok now the hard part - try calling a sub-dissector?
|
|
-- only if settings/prefs told us to of course...
|
|
if default_settings.subdissect then
|
|
dprint2("FPM trying sub-dissector for wtap encap type:", default_settings.subdiss_type)
|
|
|
|
-- due to a bug in wireshark, we need to keep newly created tvb's for longer
|
|
-- than the duration of the dissect function
|
|
tvbs[#tvbs+1] = ByteArray.new(result, true):tvb("Netlink Message")
|
|
DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvbs[#tvbs], pktinfo, root)
|
|
|
|
-- local tvb = ByteArray.new(result, true):tvb("Netlink Message")
|
|
-- DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvb, pktinfo, root)
|
|
dprint2("FPM returning from sub-dissector")
|
|
end
|
|
else
|
|
dprint("FPM header not correctly dissected")
|
|
end
|
|
|
|
return lengthVal, 0
|
|
end
|
|
|
|
|
|
----------------------------------------
|
|
-- The following creates the callback function for the dissector.
|
|
-- It's the same as doing "appProto.dissector = function (tvbuf,pkt,root)"
|
|
-- The 'tvbuf' is a Tvb object, 'pktinfo' is a Pinfo object, and 'root' is a TreeItem object.
|
|
-- Whenever Wireshark dissects a packet that our Proto is hooked into, it will call
|
|
-- this function and pass it these arguments for the packet it's dissecting.
|
|
function fpmProto.dissector(tvbuf, pktinfo, root)
|
|
dprint2("fpmProto.dissector called")
|
|
|
|
local bytes_consumed = 0
|
|
|
|
if default_settings.dissect_tcp then
|
|
dprint2("using new dissect_tcp_pdus method")
|
|
dissect_tcp_pdus(tvbuf, root, FPM_MSG_HDR_LEN, get_fpm_length, dissect_fpm_pdu, default_settings.desegment)
|
|
bytes_consumed = tvbuf:len()
|
|
else
|
|
dprint2("using old desegment_offset/desegment_len method")
|
|
-- get the length of the packet buffer (Tvb).
|
|
local pktlen = tvbuf:len()
|
|
local offset, bytes_needed = 0, 0
|
|
|
|
tvbs = {}
|
|
while bytes_consumed < pktlen do
|
|
offset, bytes_needed = dissect(tvbuf, pktinfo, root, bytes_consumed, pktlen)
|
|
if offset == 0 then
|
|
if bytes_consumed > 0 then
|
|
return bytes_consumed
|
|
else
|
|
return bytes_needed
|
|
end
|
|
end
|
|
bytes_consumed = bytes_consumed + offset
|
|
end
|
|
end
|
|
|
|
return bytes_consumed
|
|
end
|
|
|
|
|
|
----------------------------------------
|
|
-- we want to have our protocol dissection invoked for a specific TCP port,
|
|
-- so get the TCP dissector table and add our protocol to it
|
|
-- first remove any existing dissector for that port, if there is one
|
|
local old_dissector = DissectorTable.get("tcp.port"):get_dissector(default_settings.port)
|
|
if old_dissector then
|
|
dprint("Retrieved existing dissector")
|
|
end
|
|
|
|
local function enableDissector()
|
|
DissectorTable.get("tcp.port"):set(default_settings.port, fpmProto)
|
|
end
|
|
-- call it now
|
|
enableDissector()
|
|
|
|
local function disableDissector()
|
|
if old_dissector then
|
|
DissectorTable.get("tcp.port"):set(default_settings.port, old_dissector)
|
|
end
|
|
end
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- preferences handling stuff
|
|
--------------------------------------------------------------------------------
|
|
|
|
local debug_pref_enum = {
|
|
{ 1, "Disabled", debug_level.DISABLED },
|
|
{ 2, "Level 1", debug_level.LEVEL_1 },
|
|
{ 3, "Level 2", debug_level.LEVEL_2 },
|
|
}
|
|
|
|
----------------------------------------
|
|
-- register our preferences
|
|
fpmProto.prefs.enabled = Pref.bool("Dissector enabled", default_settings.enabled,
|
|
"Whether the FPM dissector is enabled or not")
|
|
|
|
|
|
fpmProto.prefs.desegment = Pref.bool("Reassemble FPM messages spanning multiple TCP segments",
|
|
default_settings.desegment,
|
|
"Whether the FPM dissector should reassemble"..
|
|
" messages spanning multiple TCP segments."..
|
|
" To use this option, you must also enable"..
|
|
" \"Allow subdissectors to reassemble TCP"..
|
|
" streams\" in the TCP protocol settings.")
|
|
|
|
fpmProto.prefs.dissect_tcp = Pref.bool("Use dissect_tcp_pdus", default_settings.dissect_tcp,
|
|
"Whether the FPM dissector should use the new" ..
|
|
" dissect_tcp_pdus model or not")
|
|
|
|
fpmProto.prefs.subdissect = Pref.bool("Enable sub-dissectors", default_settings.subdissect,
|
|
"Whether the FPM packet's content" ..
|
|
" should be dissected or not")
|
|
|
|
fpmProto.prefs.debug = Pref.enum("Debug", default_settings.debug_level,
|
|
"The debug printing level", debug_pref_enum)
|
|
|
|
----------------------------------------
|
|
-- a function for handling prefs being changed
|
|
function fpmProto.prefs_changed()
|
|
dprint2("prefs_changed called")
|
|
|
|
default_settings.dissect_tcp = fpmProto.prefs.dissect_tcp
|
|
|
|
default_settings.subdissect = fpmProto.prefs.subdissect
|
|
|
|
default_settings.debug_level = fpmProto.prefs.debug
|
|
reset_debug_level()
|
|
|
|
if default_settings.enabled ~= fpmProto.prefs.enabled then
|
|
default_settings.enabled = fpmProto.prefs.enabled
|
|
if default_settings.enabled then
|
|
enableDissector()
|
|
else
|
|
disableDissector()
|
|
end
|
|
-- have to reload the capture file for this type of change
|
|
reload()
|
|
end
|
|
|
|
end
|
|
|
|
dprint2("pcapfile Prefs registered")
|
|
|
|
|
|
----------------------------------------
|
|
-- the hatype field of the SLL must be 824 decimal, in big-endian encoding (0x0338)
|
|
local ARPHRD_NETLINK = "\003\056"
|
|
local WS_NETLINK_ROUTE = "\000\000"
|
|
local function emptyBytes(num)
|
|
return string.rep("\000", num)
|
|
end
|
|
|
|
createSLL = function (payload)
|
|
dprint2("FPM createSLL function called")
|
|
local sllmsg =
|
|
{
|
|
emptyBytes(2), -- Unused 2B
|
|
ARPHRD_NETLINK, -- netlink type
|
|
emptyBytes(10), -- Unused 10B
|
|
WS_NETLINK_ROUTE, -- Route type
|
|
payload -- the Netlink message
|
|
}
|
|
return table.concat(sllmsg)
|
|
end
|