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