aboutsummaryrefslogtreecommitdiffstats
path: root/test/lua/dissectFPM.lua
blob: da52d74a5ce59a0b0a69ed3410d086872b6bc963 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
----------------------------------------
--
-- 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