-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfpm.lua
More file actions
executable file
·621 lines (500 loc) · 24.5 KB
/
fpm.lua
File metadata and controls
executable file
·621 lines (500 loc) · 24.5 KB
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
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
-------------------------------------------------------------------------------
--
-- 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.
The purpose of this script is two-fold:
1) To decode a protocol Wireshark does not (currently) decode natively.
2) To provide a tutorial for TCP-based Lua dissection.
Because of the second goal (a tutorial), this script has a lot more comments
than one would normally expect or want.
----------------------------------------
OVERVIEW:
This Lua plugin script dissects Quagga/zebra-style FPM messages carrying
Netlink messages, over TCP connections.
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.
Note that there is no Linux cooked-mode SLL header in this case.
Therefore, to be able to re-use Wireshark's built-in Netlink dissector,
this Lua script creates a fake SLL header, and invokes the built-in
Netlink dissector using that.
----------------------------------------
HOW TO RUN THIS SCRIPT:
Wireshark and Tshark support multiple ways of loading Lua scripts: through
a dofile() call in init.lua, through the file being in either the global
or personal plugins directories, or via the command line.
See the Wireshark Developer's Guide chapter on Lua
(https://www.wireshark.org/docs/wsdg_html_chunked/wsluarm_modules.html).
Once this script is loaded, it creates a new protocol named "FPM", as
described in the "Background" section. If you have a capture file with FPM
messages in it, simply select one in the Packet List pane, right-click on
it, and select "Decode As ...", and then in the dialog box that shows up
scroll down the list of protocols to one called "FPM", select that and
click the "ok" or "apply" button. Voila`, you're now decoding FPM packets
using the dissector in this script. Another way is to download the capture
file made for this script (called "segmented_fpm.pcap", and open that -
since the FPM packets in it use TCP port 2620, and since the FPM protocol
in this script has been set to automatically decode TCP port 2620, it will
automagically do it without doing "Decode As ...".
----------------------------------------
Writing Lua Dissectors for TCP-based Protocols:
A Lua-based protocol dissector for TCP works much the same as one for UDP,
which is described in the 'dissector.lua' tutorial script available on
https://wiki.wireshark.org/Lua/Examples. The main differences are
interfacing with Wireshark's Lua API for reassembly, and handling the
various conditions that can arise due to running on TCP.
In particular, your dissector function needs to handle the following
conditions which can occur for TCP-based packet captures:
1) The TCP packet segment might only have a portion of your message.
2) The TCP packet segment might contain multiple of your messages.
3) The TCP packet might be in the middle of your message, because
a previous segment was not captured.
4) The TCP packet might be cut-off, because the user set Wireshark to
limit the size of the packets being captured.
5) Any combination of the above.
For case (4), the simplest thing to do is just not dissect packets that
are cut-off. Check the Tvb's len() vs. reported_len(), and if they're
different that means the packet was cut-off.
For case (3), your dissector should try to perform some sanity checking of
an early field if possible. If the sanity check fails, then ignore this
packet and wait for the next one. "Ignoring" the packet means returning
the number 0 from your dissector.
For case (2), currently this requires you to write your own while-loop,
dissecting your message within the while-loop, such that you can dissect
multiple of your messages each time Wireshark invokes your Proto's
dissector() function.
For case (1), you have to dissect your message enough to figure out what
the full length will be - if you can figure that out, then set the Pinfo's
desegment_len to how many more bytes than are currently in the Tvb that
you need in order to decode the full message; or if you don't know exactly
how many more bytes you need, then set the Pinfo desegment_len to the pre-
defined value of "DESEGMENT_ONE_MORE_SEGMENT". You also need to set the
Pinfo's desegment_offset to the offset in the tvbuff at which you want the
dissector to continue processing when next invoked by Wireshark. The next
time a TCP packet segment is received by Wireshark, it will invoke your
Proto's dissector function with a Tvb buffer composed of the data bytes
starting at the desegment_offset of the previous Tvb buffer together with
desegment_len more bytes.
For the return value of your Proto's dissector() function, you should
return one of the following:
1) If the packet does not belong to your dissector, return 0. You must
*not* set the Pinfo.desegment_len nor the desegment_offset if you
return 0.
2) If you need more bytes, set the Pinfo's
desegment_len/desegment_offset as described earlier, and return
either nothing, or return the length of the Tvb. Either way is fine.
3) If you don't need more bytes, return either nothing, or return the
length of the Tvb. Either way is fine.
See the code in this script for an example of the above.
]]----------------------------------------
----------------------------------------
-- 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
-- set it to debug_level.DISABLED to disable debug printing
-- note: this will be overridden by user's preference settings
local DEBUG = debug_level.LEVEL_1
-- a table of our default settings - these can be changed by changing
-- the preferences through the GUI or command-line; the Lua-side of that
-- preference handling is at the end of this script file
local default_settings =
{
debug_level = DEBUG,
enabled = true, -- whether this dissector is enabled or not
port = 2620, -- default TCP port number for FPM
max_msg_len = 4096, -- max length of FPM message
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 resetDebugLevel()
if default_settings.debug_level > debug_level.DISABLED then
dprint = function(...)
info(table.concat({"Lua: ", ...}," "))
end
if default_settings.debug_level > debug_level.LEVEL_1 then
dprint2 = dprint
end
else
dprint = function() end
dprint2 = dprint
end
end
-- call it now
resetDebugLevel()
--------------------------------------------------------------------------------
-- creates a Proto object, but doesn't register it yet
local fpm_proto = Proto("fpm", "FPM Header")
----------------------------------------
-- a function to convert tables of enumerated types to value-string 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),
}
-- register the ProtoFields
fpm_proto.fields = hdr_fields
dprint2("fpm_proto ProtoFields registered")
-- due to a bug in older (prior to 1.12) wireshark versions, we need to keep newly created
-- Tvb's for longer than the duration of the dissect function (see bug 10888)
-- this bug only affects dissectors that create new Tvb's, which is not that common
-- but this FPM dissector happens to do it in order to create the fake SLL header
-- to pass on to the Netlink dissector
local tvbs = {}
---------------------------------------
-- This function will be invoked by Wireshark during initialization, such as
-- at program start and loading a new file
function fpm_proto.init()
-- reset the save Tvbs
tvbs = {}
end
-- this is the size of the FPM message header (4 bytes) and the minimum FPM
-- message size we need to figure out how much the rest of the Netlink message
-- will be
local FPM_MSG_HDR_LEN = 4
-- some forward "declarations" of helper functions we use in the dissector
local createSllTvb, dissectFPM, checkFpmLength
-- this holds the Dissector object for Netlink, which we invoke in
-- our FPM dissector to dissect the encapsulated Netlink protocol
local netlink = DissectorTable.get("wtap_encap"):get_dissector(default_settings.subdiss_type)
-- this holds the plain "data" Dissector, in case we can't dissect it as Netlink
local data = Dissector.get("data")
--------------------------------------------------------------------------------
-- The following creates the callback function for the dissector.
-- It's the same as doing "fpm_proto.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 fpm_proto.dissector(tvbuf, pktinfo, root)
dprint2("fpm_proto.dissector called")
-- reset the save Tvbs
tvbs = {}
-- get the length of the packet buffer (Tvb).
local pktlen = tvbuf:len()
local bytes_consumed = 0
-- we do this in a while loop, because there could be multiple FPM messages
-- inside a single TCP segment, and thus in the same tvbuf - but our
-- fpm_proto.dissector() will only be called once per TCP segment, so we
-- need to do this loop to dissect each FPM message in it
while bytes_consumed < pktlen do
-- We're going to call our "dissect()" function, which is defined
-- later in this script file. The dissect() function returns the
-- length of the FPM message it dissected as a positive number, or if
-- it's a negative number then it's the number of additional bytes it
-- needs if the Tvb doesn't have them all. If it returns a 0, it's a
-- dissection error.
local result = dissectFPM(tvbuf, pktinfo, root, bytes_consumed)
if result > 0 then
-- we successfully processed an FPM message, of 'result' length
bytes_consumed = bytes_consumed + result
-- go again on another while loop
elseif result == 0 then
-- If the result is 0, then it means we hit an error of some kind,
-- so return 0. Returning 0 tells Wireshark this packet is not for
-- us, and it will try heuristic dissectors or the plain "data"
-- one, which is what should happen in this case.
return 0
else
-- we need more bytes, so set the desegment_offset to what we
-- already consumed, and the desegment_len to how many more
-- are needed
pktinfo.desegment_offset = bytes_consumed
-- invert the negative result so it's a positive number
result = -result
pktinfo.desegment_len = result
-- even though we need more bytes, this packet is for us, so we
-- tell wireshark all of its bytes are for us by returning the
-- number of Tvb bytes we "successfully processed", namely the
-- length of the Tvb
return pktlen
end
end
-- In a TCP dissector, you can either return nothing, or return the number of
-- bytes of the tvbuf that belong to this protocol, which is what we do here.
-- Do NOT return the number 0, or else Wireshark will interpret that to mean
-- this packet did not belong to your protocol, and will try to dissect it
-- with other protocol dissectors (such as heuristic ones)
return bytes_consumed
end
----------------------------------------
-- The following is a local function used for dissecting our FPM messages
-- inside the TCP segment using the 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/packet. So we invoke this
-- function only dissects one FPM message and we invoke it in a while loop
-- from the Proto's main disector function.
--
-- This function is passed in the original Tvb, Pinfo, and TreeItem from the Proto's
-- dissector function, as well as the offset in the Tvb that this function should
-- start dissecting from.
--
-- This function returns the length of the FPM message it dissected as a
-- positive number, or as a negative number the number of additional bytes it
-- needs if the Tvb doesn't have them all, or a 0 for error.
--
dissectFPM = function (tvbuf, pktinfo, root, offset)
dprint2("FPM dissect function called")
local length_val, length_tvbr = checkFpmLength(tvbuf, offset)
if length_val <= 0 then
return length_val
end
-- if we got here, then we have a whole message in the Tvb buffer
-- so let's finish dissecting it...
-- set the protocol column to show our protocol name
pktinfo.cols.protocol:set("FPM")
-- set the INFO column too, but only if we haven't already set it before
-- for this frame/packet, because this function can be called multiple
-- times per packet/Tvb
if string.find(tostring(pktinfo.cols.info), "^FPM") == nil then
pktinfo.cols.info:set("FPM")
end
-- We start by adding our protocol to the dissection display tree.
local tree = root:add(fpm_proto, tvbuf:range(offset, length_val))
-- dissect the version field
local version_tvbr = tvbuf:range(offset, 1)
local version_val = version_tvbr:uint()
tree:add(hdr_fields.version, version_tvbr)
-- dissect the type field
local msgtype_tvbr = tvbuf:range(offset + 1, 1)
local msgtype_val = msgtype_tvbr:uint()
tree:add(hdr_fields.msg_type, msgtype_tvbr)
-- dissect the length field
tree:add(hdr_fields.msg_len, length_tvbr)
-- ok now the hard part - try calling a sub-dissector?
-- only if settings/prefs told us to of course...
if default_settings.subdissect and (version_val == 1) and (msgtype_val == msgtype.NETLINK) then
-- append the INFO column - this will be overwritten/replaced by the
-- Netlink dissector, which sadly appears to clear it but not set
-- anything, so doing this is kind of silly/pointless, but since this
-- is a tutorial script, this showswhat you might want to do for your
-- protocol
if string.find(tostring(pktinfo.cols.info), "^FPM:") == nil then
pktinfo.cols.info:append(": Netlink")
else
pktinfo.cols.info:append(", Netlink")
end
-- it carries a Netlink message, so we're going to create a new Tvb
-- with a a fake Linux SLL header for the built-in Netlink dissector
-- to use
local tvb = createSllTvb(tvbuf, offset + FPM_MSG_HDR_LEN, length_val - FPM_MSG_HDR_LEN)
dprint2("FPM trying sub-dissector for wtap encap type:", default_settings.subdiss_type)
-- invoke the Netlink dissector (we got the Dissector object earlier,
-- as variable 'netlink')
netlink:call(tvb, pktinfo, root)
dprint2("FPM finished with sub-dissector")
else
dprint2("Netlink sub-dissection disabled or not Netlink type, invoking 'data' dissector")
-- append the INFO column
if string.find(tostring(pktinfo.cols.info), "^FPM:") == nil then
pktinfo.cols.info:append(": Unknown")
else
pktinfo.cols.info:append(", Unknown")
end
tvbs[#tvbs+1] = tvbuf(offset + FPM_MSG_HDR_LEN, length_val - FPM_MSG_HDR_LEN):tvb()
data:call(tvbs[#tvbs], pktinfo, root)
end
return length_val
end
----------------------------------------
-- The function to check the length field.
--
-- This returns two things: (1) the length, and (2) the TvbRange object, which
-- might be nil if length <= 0.
checkFpmLength = function (tvbuf, offset)
-- "msglen" is the number of bytes remaining in the Tvb buffer which we
-- have available to dissect in this run
local msglen = tvbuf:len() - offset
-- check if capture was only capturing partial packet size
if msglen ~= tvbuf:reported_length_remaining(offset) then
-- captured packets are being sliced/cut-off, so don't try to desegment/reassemble
dprint2("Captured packet was shorter than original, can't reassemble")
return 0
end
if msglen < FPM_MSG_HDR_LEN then
-- we need more bytes, so tell the main dissector function that we
-- didn't dissect anything, and we need an unknown number of more
-- bytes (which is what "DESEGMENT_ONE_MORE_SEGMENT" is used for)
dprint2("Need more bytes to figure out FPM length field")
-- return as a negative number
return -DESEGMENT_ONE_MORE_SEGMENT
end
-- if we got here, then we know we have enough bytes in the Tvb buffer
-- to at least figure out the full length of this FPM messsage (the length
-- is the 16-bit integer in third and fourth bytes)
-- get the TvbRange of bytes 3+4
local length_tvbr = tvbuf:range(offset + 2, 2)
-- get the length as an unsigned integer, in network-order (big endian)
local length_val = length_tvbr:uint()
if length_val > default_settings.max_msg_len then
-- too many bytes, invalid message
dprint("FPM message length is too long: ", length_val)
return 0
end
if msglen < length_val then
-- we need more bytes to get the whole FPM message
dprint2("Need more bytes to desegment full FPM")
return -(length_val - msglen)
end
return length_val, length_tvbr
end
----------------------------------------
-- For us to be able to use Wireshark's built-in Netlink dissector, we have to
-- create a fake SLL layer, which is what this function does.
--
local ARPHRD_NETLINK, WS_NETLINK_ROUTE, emptyBytes
-- in release 1.12+, you could call Tvb:raw() to get the raw bytes, and you
-- can call ByteArray.new() using a Lua string of binary; since that's easier
-- and more efficient, wel;l do that if the Wireshark running this script is
-- 1.12+, otherwise will do the 'else' clause the longer way
if Tvb.raw then
-- if we're here, this is Wireshark 1.12+, so we can deal with raw Lua binary strings
-- the "hatype" field of the SLL must be 824 decimal, in big-endian encoding (0x0338)
ARPHRD_NETLINK = "\003\056"
WS_NETLINK_ROUTE = "\000\000"
emptyBytes = function (num)
return string.rep("\000", num)
end
createSllTvb = function (tvbuf, begin, length)
dprint2("FPM createSllTvb function called, using 1.12+ method")
-- the SLL header and Netlink message
local sllmsg =
{
emptyBytes(2), -- Unused 2B
ARPHRD_NETLINK, -- netlink type
emptyBytes(10), -- Unused 10B
WS_NETLINK_ROUTE, -- Route type
tvbuf:raw(begin, length) -- the Netlink message
}
local payload = table.concat(sllmsg)
return ByteArray.new(payload, true):tvb("Netlink Message")
end
else
-- prior to 1.12, the only way to create a ByteArray was from hex-ascii
-- so we do things in hex-ascii
ARPHRD_NETLINK = "0338"
WS_NETLINK_ROUTE = "0000"
emptyBytes = function (num)
return string.rep("00", num)
end
createSllTvb = function (tvbuf, begin, length)
dprint2("FPM createSllTvb function called, using pre-1.12 method")
-- first get a TvbRange from the Tvb, and the TvbRange's ByteArray...
local nl_bytearray = tvbuf(begin,length):bytes()
-- then create a hex-ascii string of the SLL header portion
local sllmsg =
{
emptyBytes(2), -- Unused 2B
ARPHRD_NETLINK, -- netlink type
emptyBytes(10), -- Unused 10B
WS_NETLINK_ROUTE -- Route type
}
local hexSLL = table.concat(sllmsg)
-- then create a ByteArray from that hex-string
local sll_bytearray = ByteArray.new(hexSLL)
-- then concatenate the two ByteArrays
local full_bytearray = sll_bytearray .. nl_bytearray
-- create the new Tvb from the full ByteArray
-- and because this is pre-1.12, we need to store them longer to
-- work around bug 10888
tvbs[#tvbs+1] = full_bytearray:tvb()
-- now return the newly created Tvb
return tvbs[#tvbs]
end
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.
local function enableDissector()
-- using DissectorTable:set() removes existing dissector(s), whereas the
-- DissectorTable:add() one adds ours before any existing ones, but
-- leaves the other ones alone, which is better
DissectorTable.get("tcp.port"):add(default_settings.port, fpm_proto)
end
-- call it now, because we're enabled by default
enableDissector()
local function disableDissector()
DissectorTable.get("tcp.port"):remove(default_settings.port, fpm_proto)
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
fpm_proto.prefs.enabled = Pref.bool("Dissector enabled", default_settings.enabled,
"Whether the FPM dissector is enabled or not")
fpm_proto.prefs.subdissect = Pref.bool("Enable sub-dissectors", default_settings.subdissect,
"Whether the FPM packet's content" ..
" should be dissected or not")
fpm_proto.prefs.debug = Pref.enum("Debug", default_settings.debug_level,
"The debug printing level", debug_pref_enum)
----------------------------------------
-- the function for handling preferences being changed
function fpm_proto.prefs_changed()
dprint2("prefs_changed called")
default_settings.subdissect = fpm_proto.prefs.subdissect
default_settings.debug_level = fpm_proto.prefs.debug
resetDebugLevel()
if default_settings.enabled ~= fpm_proto.prefs.enabled then
default_settings.enabled = fpm_proto.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")