1----------------------------------------
2--
3-- author: Hadriel Kaplan <hadriel@128technology.com>
4-- Copyright (c) 2015, Hadriel Kaplan
5-- This code is in the Public Domain, or the BSD (3 clause) license
6-- if Public Domain does not apply in your country.
7--
8-- Version: 1.0
9--
10------------------------------------------
11--[[
12    This code is a plugin for Wireshark, to dissect Quagga FPM Netlink
13    protocol messages over TCP.
14
15    This script is used for testing, so it does some odd things:
16    * it dissects the FPM in two ways, controlled by a pref setting:
17        1) using the desegment_offset/desegment_len method
18        2) using the dissect_tcp_pdus() method
19    * it removes any existing FPM dissector; there isn't one right now
20      but there likely will be in the future.
21
22    Wireshark has a "Netlink" protocol dissector, but it currently expects
23    to be running on a Linux cooked-mode SLL header and link type. That's
24    because Netlink has traditionally been used between the Linux kernel
25    and user-space apps. But the open-source Quagga, zebra, and the
26    commercial ZebOS routing products also send Netlink messages over TCP
27    to other processes or even outside the box, to a "Forwarding Plane Manager"
28    (FPM) that controls forwarding-plane devices (typically hardware).
29
30    The Netlink message is encapsulated within an FPM header, which identifies
31    an FPM message version (currently 1), the type of message it contains
32    (namely a Netlink message), and its length.
33
34    So we have:
35    struct fpm_msg_hdr_t
36    {
37        uint8_t  version;
38        uint8_t  msg_type;
39        uint16_t msg_len;
40    }
41    followed by a Netlink message.
42]]----------------------------------------
43
44
45----------------------------------------
46-- do not modify this table
47local debug_level = {
48    DISABLED = 0,
49    LEVEL_1  = 1,
50    LEVEL_2  = 2
51}
52
53-- set this DEBUG to debug_level.LEVEL_1 to enable printing debug_level info
54-- set it to debug_level.LEVEL_2 to enable really verbose printing
55-- note: this will be overridden by user's preference settings
56local DEBUG = debug_level.LEVEL_1
57
58local default_settings =
59{
60    debug_level  = DEBUG,
61    enabled      = true, -- whether this dissector is enabled or not
62    port         = 2620,
63    max_msg_len  = 4096,
64    desegment    = true, -- whether to TCP desegement or not
65    dissect_tcp  = false, -- whether to use the dissect_tcp_pdus method or not
66    subdissect   = true, -- whether to call sub-dissector or not
67    subdiss_type = wtap.NETLINK, -- the encap we get the subdissector for
68}
69
70local dprint = function() end
71local dprint2 = function() end
72local function reset_debug_level()
73    if default_settings.debug_level > debug_level.DISABLED then
74        dprint = function(...)
75            print(table.concat({"Lua:", ...}," "))
76        end
77
78        if default_settings.debug_level > debug_level.LEVEL_1 then
79            dprint2 = dprint
80        end
81    end
82end
83-- call it now
84reset_debug_level()
85
86
87----------------------------------------
88-- creates a Proto object, but doesn't register it yet
89local fpmProto = Proto("fpm", "FPM Header")
90
91
92----------------------------------------
93-- a function to convert tables of enumerated types to valstring tables
94-- i.e., from { "name" = number } to { number = "name" }
95local function makeValString(enumTable)
96    local t = {}
97    for name,num in pairs(enumTable) do
98        t[num] = name
99    end
100    return t
101end
102
103local MsgType = {
104    NONE     = 0,
105    NETLINK  = 1,
106}
107local msgtype_valstr = makeValString(MsgType)
108
109
110----------------------------------------
111-- a table of all of our Protocol's fields
112local hdr_fields =
113{
114    version   = ProtoField.uint8 ("fpm.version", "Version", base.DEC),
115    msg_type  = ProtoField.uint8 ("fpm.type", "Type", base.DEC, msgtype_valstr),
116    msg_len   = ProtoField.uint16("fpm.length", "Length", base.DEC),
117}
118
119-- create a flat array table of the above that can be registered
120local pfields = {}
121
122-- recursive function to flatten the table into pfields
123local function flattenTable(tbl)
124    for k,v in pairs(tbl) do
125        if type(v) == 'table' then
126            flattenTable(v)
127        else
128            pfields[#pfields+1] = v
129        end
130    end
131end
132-- call it
133flattenTable(hdr_fields)
134
135-- register them
136fpmProto.fields = pfields
137
138dprint2("fpmProto ProtoFields registered")
139
140
141----------------------------------------
142-- some forward "declarations" of helper functions we use in the dissector
143local createSLL
144
145-- due to a bug in wireshark, we need to keep newly created tvb's for longer
146-- than the duration of the dissect function
147local tvbs = {}
148
149function fpmProto.init()
150    tvbs = {}
151end
152
153
154local FPM_MSG_HDR_LEN = 4
155
156----------------------------------------
157-- the following function is used for the new dissect_tcp_pdus method
158-- this one returns the length of the full message
159local function get_fpm_length(tvbuf, pktinfo, offset)
160    dprint2("FPM get_fpm_length function called")
161    local lengthVal = tvbuf:range(offset + 2, 2):uint()
162
163    if lengthVal > default_settings.max_msg_len then
164        -- too many bytes, invalid message
165        dprint("FPM message length is too long: ", lengthVal)
166        lengthVal = tvbuf:len()
167    end
168
169    return lengthVal
170end
171
172-- the following is the dissection function called for
173-- the new dissect_tcp_pdus method
174local function dissect_fpm_pdu(tvbuf, pktinfo, root)
175    dprint2("FPM dissect_fpm_pdu function called")
176
177    local lengthTvbr = tvbuf:range(2, 2)
178    local lengthVal  = lengthTvbr:uint()
179
180    -- set the protocol column to show our protocol name
181    pktinfo.cols.protocol:set("FPM")
182
183    -- We start by adding our protocol to the dissection display tree.
184    local tree = root:add(fpmProto, tvbuf:range(offset, lengthVal))
185
186    local versionTvbr = tvbuf:range(0, 1)
187    local versionVal  = versionTvbr:uint()
188    tree:add(hdr_fields.version, versionTvbr)
189
190    local msgTypeTvbr = tvbuf:range(1, 1)
191    local msgTypeVal  = msgTypeTvbr:uint()
192    tree:add(hdr_fields.msg_type, msgTypeTvbr)
193
194    tree:add(hdr_fields.msg_len, lengthTvbr)
195
196    local result
197    if (versionVal == 1) and (msgTypeVal == MsgType.NETLINK) then
198        -- it carries a Netlink message, so we're going to create
199        -- a fake Linux SLL header for the built-in Netlink dissector
200        local payload = tvbuf:raw(FPM_MSG_HDR_LEN, lengthVal - FPM_MSG_HDR_LEN)
201        result = createSLL(payload)
202    end
203
204    -- looks good, go dissect it
205    if result then
206        -- ok now the hard part - try calling a sub-dissector?
207        -- only if settings/prefs told us to of course...
208        if default_settings.subdissect then
209            dprint2("FPM trying sub-dissector for wtap encap type:", default_settings.subdiss_type)
210
211            -- due to a bug in wireshark, we need to keep newly created tvb's for longer
212            -- than the duration of the dissect function
213            tvbs[#tvbs+1] = ByteArray.new(result, true):tvb("Netlink Message")
214            DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvbs[#tvbs], pktinfo, root)
215
216            -- local tvb = ByteArray.new(result, true):tvb("Netlink Message")
217            -- DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvb, pktinfo, root)
218            dprint2("FPM returning from sub-dissector")
219        end
220    else
221        dprint("FPM header not correctly dissected")
222    end
223
224    return lengthVal, 0
225end
226
227
228----------------------------------------
229-- the following function is used for dissecting using the
230-- old desegment_offset/desegment_len method
231-- it's a separate function because we run over TCP and thus might
232-- need to parse multiple messages in a single segment
233local function dissect(tvbuf, pktinfo, root, offset, origlen)
234    dprint2("FPM dissect function called")
235
236    local pktlen = origlen - offset
237
238    if pktlen < FPM_MSG_HDR_LEN then
239        -- we need more bytes
240        pktinfo.desegment_offset = offset
241        pktinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT
242        return 0, DESEGMENT_ONE_MORE_SEGMENT
243    end
244
245    local lengthTvbr = tvbuf:range(offset + 2, 2)
246    local lengthVal  = lengthTvbr:uint()
247
248    if lengthVal > default_settings.max_msg_len then
249        -- too many bytes, invalid message
250        dprint("FPM message length is too long: ", lengthVal)
251        return pktlen, 0
252    end
253
254    if pktlen < lengthVal then
255        dprint2("Need more bytes to desegment FPM")
256        pktinfo.desegment_offset = offset
257        pktinfo.desegment_len = (lengthVal - pktlen)
258        return 0, -(lengthVal - pktlen)
259    end
260
261    -- set the protocol column to show our protocol name
262    pktinfo.cols.protocol:set("FPM")
263
264    -- We start by adding our protocol to the dissection display tree.
265    local tree = root:add(fpmProto, tvbuf:range(offset, lengthVal))
266
267    local versionTvbr = tvbuf:range(offset, 1)
268    local versionVal  = versionTvbr:uint()
269    tree:add(hdr_fields.version, versionTvbr)
270
271    local msgTypeTvbr = tvbuf:range(offset + 1, 1)
272    local msgTypeVal  = msgTypeTvbr:uint()
273    tree:add(hdr_fields.msg_type, msgTypeTvbr)
274
275    tree:add(hdr_fields.msg_len, lengthTvbr)
276
277    local result
278    if (versionVal == 1) and (msgTypeVal == MsgType.NETLINK) then
279        -- it carries a Netlink message, so we're going to create
280        -- a fake Linux SLL header for the built-in Netlink dissector
281        local payload = tvbuf:raw(offset + FPM_MSG_HDR_LEN, lengthVal - FPM_MSG_HDR_LEN)
282        result = createSLL(payload)
283    end
284
285    -- looks good, go dissect it
286    if result then
287        -- ok now the hard part - try calling a sub-dissector?
288        -- only if settings/prefs told us to of course...
289        if default_settings.subdissect then
290            dprint2("FPM trying sub-dissector for wtap encap type:", default_settings.subdiss_type)
291
292            -- due to a bug in wireshark, we need to keep newly created tvb's for longer
293            -- than the duration of the dissect function
294            tvbs[#tvbs+1] = ByteArray.new(result, true):tvb("Netlink Message")
295            DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvbs[#tvbs], pktinfo, root)
296
297            -- local tvb = ByteArray.new(result, true):tvb("Netlink Message")
298            -- DissectorTable.get("wtap_encap"):try(default_settings.subdiss_type, tvb, pktinfo, root)
299            dprint2("FPM returning from sub-dissector")
300        end
301    else
302        dprint("FPM header not correctly dissected")
303    end
304
305    return lengthVal, 0
306end
307
308
309----------------------------------------
310-- The following creates the callback function for the dissector.
311-- It's the same as doing "appProto.dissector = function (tvbuf,pkt,root)"
312-- The 'tvbuf' is a Tvb object, 'pktinfo' is a Pinfo object, and 'root' is a TreeItem object.
313-- Whenever Wireshark dissects a packet that our Proto is hooked into, it will call
314-- this function and pass it these arguments for the packet it's dissecting.
315function fpmProto.dissector(tvbuf, pktinfo, root)
316    dprint2("fpmProto.dissector called")
317
318    local bytes_consumed = 0
319
320    if default_settings.dissect_tcp then
321        dprint2("using new dissect_tcp_pdus method")
322        dissect_tcp_pdus(tvbuf, root, FPM_MSG_HDR_LEN, get_fpm_length, dissect_fpm_pdu, default_settings.desegment)
323        bytes_consumed = tvbuf:len()
324    else
325        dprint2("using old desegment_offset/desegment_len method")
326        -- get the length of the packet buffer (Tvb).
327        local pktlen = tvbuf:len()
328        local offset, bytes_needed = 0, 0
329
330        tvbs = {}
331        while bytes_consumed < pktlen do
332            offset, bytes_needed = dissect(tvbuf, pktinfo, root, bytes_consumed, pktlen)
333            if offset == 0 then
334                if bytes_consumed > 0 then
335                    return bytes_consumed
336                else
337                    return bytes_needed
338                end
339            end
340            bytes_consumed = bytes_consumed + offset
341        end
342    end
343
344    return bytes_consumed
345end
346
347
348----------------------------------------
349-- we want to have our protocol dissection invoked for a specific TCP port,
350-- so get the TCP dissector table and add our protocol to it
351-- first remove any existing dissector for that port, if there is one
352local old_dissector = DissectorTable.get("tcp.port"):get_dissector(default_settings.port)
353if old_dissector then
354    dprint("Retrieved existing dissector")
355end
356
357local function enableDissector()
358    DissectorTable.get("tcp.port"):set(default_settings.port, fpmProto)
359end
360-- call it now
361enableDissector()
362
363local function disableDissector()
364    if old_dissector then
365        DissectorTable.get("tcp.port"):set(default_settings.port, old_dissector)
366    end
367end
368
369
370--------------------------------------------------------------------------------
371-- preferences handling stuff
372--------------------------------------------------------------------------------
373
374local debug_pref_enum = {
375    { 1,  "Disabled", debug_level.DISABLED },
376    { 2,  "Level 1",  debug_level.LEVEL_1  },
377    { 3,  "Level 2",  debug_level.LEVEL_2  },
378}
379
380----------------------------------------
381-- register our preferences
382fpmProto.prefs.enabled     = Pref.bool("Dissector enabled", default_settings.enabled,
383                                       "Whether the FPM dissector is enabled or not")
384
385
386fpmProto.prefs.desegment   = Pref.bool("Reassemble FPM messages spanning multiple TCP segments",
387                                       default_settings.desegment,
388                                       "Whether the FPM dissector should reassemble"..
389                                       " messages spanning multiple TCP segments."..
390                                       " To use this option, you must also enable"..
391                                       " \"Allow subdissectors to reassemble TCP"..
392                                       " streams\" in the TCP protocol settings.")
393
394fpmProto.prefs.dissect_tcp = Pref.bool("Use dissect_tcp_pdus", default_settings.dissect_tcp,
395                                       "Whether the FPM dissector should use the new" ..
396                                       " dissect_tcp_pdus model or not")
397
398fpmProto.prefs.subdissect  = Pref.bool("Enable sub-dissectors", default_settings.subdissect,
399                                       "Whether the FPM packet's content" ..
400                                       " should be dissected or not")
401
402fpmProto.prefs.debug       = Pref.enum("Debug", default_settings.debug_level,
403                                       "The debug printing level", debug_pref_enum)
404
405----------------------------------------
406-- a function for handling prefs being changed
407function fpmProto.prefs_changed()
408    dprint2("prefs_changed called")
409
410    default_settings.dissect_tcp = fpmProto.prefs.dissect_tcp
411
412    default_settings.subdissect  = fpmProto.prefs.subdissect
413
414    default_settings.debug_level = fpmProto.prefs.debug
415    reset_debug_level()
416
417    if default_settings.enabled ~= fpmProto.prefs.enabled then
418        default_settings.enabled = fpmProto.prefs.enabled
419        if default_settings.enabled then
420            enableDissector()
421        else
422            disableDissector()
423        end
424        -- have to reload the capture file for this type of change
425        reload()
426    end
427
428end
429
430dprint2("pcapfile Prefs registered")
431
432
433----------------------------------------
434-- the hatype field of the SLL must be 824 decimal, in big-endian encoding (0x0338)
435local ARPHRD_NETLINK = "\003\056"
436local WS_NETLINK_ROUTE = "\000\000"
437local function emptyBytes(num)
438    return string.rep("\000", num)
439end
440
441createSLL = function (payload)
442    dprint2("FPM createSLL function called")
443    local sllmsg =
444    {
445        emptyBytes(2),      -- Unused 2B
446        ARPHRD_NETLINK,     -- netlink type
447        emptyBytes(10),     -- Unused 10B
448        WS_NETLINK_ROUTE,   -- Route type
449        payload             -- the Netlink message
450    }
451    return table.concat(sllmsg)
452end
453