decryption works, but addid doesn't because of unique pointer layers
[wireshark-sm.git] / test / lua / acme_file.lua
blob59c48dc6e473b5003e665d06fb47e4a7ca772df0
1 ------------------------------------------
2 -- acme_file_reader.lua
3 -- Author: Hadriel Kaplan (hadrielk at yahoo dot com)
4 -- version = 1.0
5 -- date = 3/3/2014
6 ------------------------------------------
7 --[[
8 This is a Wireshark Lua-based capture file reader.
9 This "capture file" reader reads message logs from Acme Packet (now Oracle) Session Border Controllers,
10 such as sipmsg.log files. There are several variants of the log file format, as well as some changes that
11 can happen based on how the log file is generated and retrieved; for example if it's generated through a
12 'tail' command, or FTP'ed by a FTP client which adds carriage-returns. This Lua file reader tries to handle
13 such conditions.
15 Note: this script wasn't written to be super-efficient, nor clever. When you've been writing Lua for a while
16 you get used to writing in a different, more elegant fashion than this script is; but other people find it
17 hard to read such Lua code, so I've tried to keep this simpler.
19 Features:
20 -handles sipmsg type logs, sipdns type logs, algd type logs
21 -handles both IPv4 and IPv6, for both UDP and TCP
22 -reads sipmsg logs from 3800, 4250, 4500, 9200, 6300 SBCs
23 -handles logs with extra carriage-returns and linefeeds, such as from certain FTP'ed cases
24 -handles logs generated/copied from a 'tail' command on the SBC ACLI
25 -handles MBCD messages in logs, and puts their decoded ascii description in comments in Wireshark
27 Issues:
28 -for very large logs (many megabytes), it takes a long time (many minutes)
29 -creates fake IP and UDP/TCP headers, which might be misleading
30 -has to guess sometimes, though it hasn't guessed wrong yet as far as I know
32 To-do:
33 - make it use Struct.tohex/fromhex now that we have the Struct library in Wireshark
34 - make it use a linux cooked-mode pseudo-header (see https://gitlab.com/wireshark/wireshark/-/wikis/SLL)
35 - make it use preferences, once I write C-code for Wireshark to do that :)
36 - rewrite some of the pattern searches to use real regex/PCRE instead?
38 Example SIP over UDP message:
39 Aug 26 19:25:10.685 On [5:0]2.1.1.1:5060 received from 2.1.2.115:5060
40 REGISTER sip:2.1.1.1:5060 SIP/2.0
41 Via: SIP/2.0/UDP 2.1.2.115:5060;branch=z9hG4bK6501441021660x81000412
42 From: <sip:public_115@2.1.1.1:5060>;tag=520052-7015560x81000412
43 To: <sip:public_115@2.1.1.1:5060>
44 Call-ID: 680192-4234150x81000412@2.1.2.115
45 CSeq: 247 REGISTER
46 Contact: <sip:public_115@2.1.2.115:5060;transport=udp>
47 Expires: 300
48 Max-Forwards: 70
49 Authorization: Digest username="public_115",realm="empirix.com",uri="sip:2.1.1.1",response="5d61837cc54dc27018a40f2532e622de",nonce="430f6ff09ecd8c3fdfc5430b6e7e437a4cf77057",algorithm=md5
50 Content-Length: 0
53 ----------------------------------------
54 Another one:
55 2007-03-06 13:38:48.037 OPENED
56 2007-03-06 13:38:48.037 OPENED
57 2007-03-06 13:38:48.037 OPENED
58 Mar 6 13:38:54.959 On [1:0]135.25.29.135:5060 received from 192.168.109.138:65471
59 OPTIONS sip:135.25.29.135 SIP/2.0
60 Accept: application/sdp
61 User-Agent: ABS GW v5.1.0
62 To: sip:135.25.29.135
63 From: sip:192.168.109.138;tag=a2a090ade36bb108da70b0c8f7ba02e9
64 Contact: sip:192.168.109.138
65 Call-ID: 8c0296da4a0d9f4d97e1389cd28d2352@172.16.6.114
66 CSeq: 347517161 OPTIONS
67 Via: SIP/2.0/UDP 192.168.109.138;branch=z9hG4bK21feac80fe9a63c1cf2988baa2af0849
68 Max-Forwards: 70
69 Content-Length: 0
72 ----------------------------------------
73 Another SIP over UDP (from 9200):
74 File opened.
75 Jun 8 14:34:22.599 UDP[3:0]10.102.131.194:5060 OPENED
76 Jun 8 14:34:22.616 UDP[6:0]10.102.130.185:5060 OPENED
77 Jun 8 14:34:49.416 On [6:0]10.102.130.185:5060 received from 10.102.130.150:5060
78 REGISTER sip:csp.noklab.net SIP/2.0
79 Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK26b7a48d
80 From: sip:34903@csp.noklab.net
81 To: sip:34903@csp.noklab.net
82 Call-ID: 003094c3-a0160002-23aa7e86-29e5808d@192.168.1.100
83 CSeq: 144 REGISTER
84 User-Agent: CSCO/7
85 Contact: <sip:34903@192.168.1.100:5060>
86 Content-Length: 0
87 Expires: 3600
90 ----------------------------------------
92 Example SIP over TCP message (note it ends in the middle of a header name):
93 Jan 12 00:03:54.700 On 172.25.96.200:8194 received from 172.25.32.28:5060
94 SIP/2.0 200 OK
95 From: Unavailable <sip:Unavailable@172.25.96.200:5060;user=phone>;tag=1200822480
96 To: 24001900011 <sip:0011@172.25.32.28:5060;user=phone>;tag=03c86c0b27df1b1254aeccbc000
97 Call-ID: 7f6b0887-1d313896-1511da31-b045@144.229.136.111
98 CSe
99 ----------------------------------------
101 Example SIP Pre and Post-NAT messages:
102 Post-NAT from private<realm=e911core> encoded:
103 SIP/2.0 302 Moved Temporarily
104 Call-ID: SD27o9f04-fcc63aa885c83e22a1be64cfc210b55e-vjvtv00
105 CSeq: 2 INVITE
106 From: <sip:7866932005@127.1.0.100:5060;user=phone;e911core=TSD5051AEPCORE-dnamt76v6nm04;CKE=BSLD-5cuduig6t52l2;e911vpn=TSD5051AEPVPN-7gdq13vt8fi59>;tag=SD27o9f04-10000000-0-1424021314
107 To: <sip:911@127.0.0.100;user=phone;CKE=BSLD-8blt7m3dhnj17>;tag=10280004-0-1239441202
108 Via: SIP/2.0/UDP 127.254.254.1:5060;branch=z9hG4bK5i4ue300dgrdras7q281.1
109 Server: DC-SIP/1.2
110 Content-Length: 0
111 Contact: <sip:1111119999@127.0.0.100:5060;e911core=TSD5051AEPCORE-5n86t36uuma01>
114 ----------------------------------------
115 Pre-NAT to private<realm=e911core> decode:
116 ACK sip:911@127.0.0.100;user=phone;CKE=BSLD-8blt7m3dhnj17 SIP/2.0
117 Via: SIP/2.0/UDP 127.254.254.1:5060;branch=z9hG4bK5i4ue300dgrdras7q281.1
118 Call-ID: SD27o9f04-fcc63aa885c83e22a1be64cfc210b55e-vjvtv00
119 CSeq: 2 ACK
120 From: <sip:7866932005@127.1.0.100:5060;user=phone;e911core=TSD5051AEPCORE-dnamt76v6nm04;CKE=BSLD-5cuduig6t52l2;e911vpn=TSD5051AEPVPN-7gdq13vt8fi59>;tag=SD27o9f04-10000000-0-1424021314
121 To: <sip:911@127.0.0.100;user=phone;CKE=BSLD-8blt7m3dhnj17>;tag=10280004-0-1239441202
122 Max-Forwards: 70
125 ----------------------------------------
127 Example DNS message:
128 Nov 1 23:03:12.811 On 10.21.232.194:1122 received from 10.21.199.204:53
129 DNS Response 3916 flags=8503 q=1 ans=0 auth=1 add=0 net-ttl=0
130 Q:NAPTR 7.6.5.4.3.2.1.0.1.2.e164
131 NS:SOA e164 ttl=0 netnumber01
132 rname=user.netnumber01
133 ser=223 ref=0 retry=0 exp=0 minttl=0
135 0000: 0f 4c 85 03 00 01 00 00 00 01 00 00 01 37 01 36 .L...........7.6
136 0010: 01 35 01 34 01 33 01 32 01 31 01 30 01 31 01 32 .5.4.3.2.1.0.1.2
137 0020: 04 65 31 36 34 00 00 23 00 01 04 65 31 36 34 00 .e164..#...e164.
138 0030: 00 06 00 01 00 00 00 00 00 33 0b 6e 65 74 6e 75 .........3.netnu
139 0040: 6d 62 65 72 30 31 00 04 75 73 65 72 0b 6e 65 74 mber01..user.net
140 0050: 6e 75 6d 62 65 72 30 31 00 00 00 00 df 00 00 00 number01........
141 0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 .............
143 ----------------------------------------
144 Example MGCP message (note the IP/UDP headers are in the hex):
145 Mar 1 14:37:26.683 On [0:803]172.16.84.141:2427 sent to 172.16.74.100:2427
146 Packet:
147 0000: 00 04 00 00 00 01 00 02 00 00 03 23 0a ad 00 c9 ...........#....
148 0010: 45 00 00 a8 23 36 00 00 3c 11 63 fd ac 10 54 8d E...#6..<.c...T.
149 0020: ac 10 4a 64 09 7b 09 7b 00 94 16 c2 32 35 30 20 ..Jd.{.{....250
151 250 55363 Connection Deleted
152 P: PS=6551, OS=1048160, PR=6517, OR=1042720, PL=0, JI=1, LA=5, PC/RPS=6466, PC/ROS=1034560, PC/RPL=0, PC/RJI=0
154 ----------------------------------------
155 Example MBCD message:
156 Mar 1 14:37:26.672 On 127.0.0.1:2946 sent to 127.0.0.1:2944
157 0000: ac 3e fd a8 01 01 77 36 9e 00 37 10 0c 34 4c bc .>....w6..7..4L.
158 0010: 00 30 23 0c 34 4c bc 00 11 33 00 0e 35 00 04 00 .0#.4L...3..5...
159 0020: 00 00 00 30 00 04 00 00 00 00 23 0c 34 4c bd 00 ...0......#.4L..
160 0030: 11 33 00 0e 35 00 04 00 00 00 00 30 00 04 00 00 .3..5......0....
161 0040: 00 00 ..
162 Transaction = 24589982 {
163 Context = 204754108 {
164 Subtract = 204754108 {
165 Audit {
166 Stats,
167 Flow
170 Subtract = 204754109 {
171 Audit {
172 Stats,
173 Flow
178 ----------------------------------------
180 ]]----------------------------------------
182 -- debug printer, set DEBUG to true to enable printing debug info
183 -- set DEBUG2 to true to enable really verbose printing
184 local DEBUG, DEBUG2 = true, false
186 local dprint = function() end
187 local dprint2 = function() end
188 if DEBUG or DEBUG2 then
189 dprint = function(...)
190 print(table.concat({"Lua:", ...}," "))
193 if DEBUG2 then
194 dprint2 = dprint
198 -- this should be done as a preference setting
199 local ALWAYS_UDP = true
202 local fh = FileHandler.new("Oracle Acme Packet logs", "acme",
203 "A file reader for Oracle Acme Packet message logs such as sipmsg.log","rs")
206 -- There are certain things we have to create fake state/data for, because they
207 -- don't exist in the log file for example to create IP headers we have to create
208 -- fake identification field values, and to create timestamps we have to guess the
209 -- year (and in some cases month/day as well), and for TCP we have to create fake
210 -- connection info, such as sequence numbers. We can't simply have a global static
211 -- variable holding such things, because Wireshark reads the file sequentially at
212 -- first, but then calls seek_read for random packets again and we don't want to
213 -- re-create the fake info again because it will be wrong. So we need to create it
214 -- for each packet and remember what we created for each packet, so that seek_read
215 -- gets the same values. We could store the variables in a big table, keyed by the
216 -- specific header info line for each one; but instead we'll key it off of the file
217 -- position number, since read() sets it for Wireshark and seek_read() gets it from
218 -- Wireshark. So we'll have a set of global statics used during read(), but the
219 -- actual per-packet values will be stored in a table indexed/keyed by the file
220 -- position number. A separate table holds TCP peer connection info as described
221 -- later.
223 -- I said above that this state is "global", but really it can't be global to this
224 -- whole script file, because more than one file can be opened for reading at the
225 -- same time. For example if the user presses the reload button, the capture file
226 -- will be opened for reading before the previous (same) one is closed. So we have
227 -- to store state per-file. The good news is Wireshark gives us a convenient way to
228 -- do that, using the CaptureInfo.private_table attribute/member. We can save a Lua
229 -- table with whatever contents we want, to this private_table member, and get it
230 -- later during the other read/seek_read/cose function calls.
232 -- So to store this per-file state, we're going to use Lua class objects. They're
233 -- just Lua tables that have functions and meta-functions and can be treated like
234 -- objects in terms of syntax/behavior.
236 local State = {}
237 local State_mt = { __index = State }
239 function State.new()
240 local new_class = { -- the new instance
241 -- stuff we need to keep track of to cerate fake info
242 ip_ident = 0,
243 tyear = 0,
244 tmonth = 0,
245 tmin = 0,
246 tsec = 0,
247 tmilli = 0,
248 nstime = NSTime(),
249 -- the following table holds per-packet info
250 -- the key index will be a number - the file position - but it won't be an array type table (too sparse).
251 -- Each packet's entry is a table holding the "static" variables for that packet; this sub-table will be
252 -- an array style instead of hashmap, to reduce size/performance
253 -- This table needs to be cleared whenever the file is closed/opened.
254 packets = {},
256 -- the following local table holds TCP peer "connection" info, which is basically
257 -- TCP control block (TCB) type information; this is needed to create and keep track
258 -- of fake TCP sockets/headers for messages that went over TCP, for example for fake
259 -- sequence number info.
260 -- The key index for this is the local+remote ip:port strings concatenated.
261 -- The value is a sub-table, array style, holding the most recent sequence numbers.
262 -- This whole table needs to be cleared whenever the file is closed/opened.
263 tcb = {},
266 setmetatable( new_class, State_mt ) -- all instances share the same metatable
267 return new_class
270 -- the indices for the State.packets{} variable sub-tables
271 local IP_IDENT = 1
272 local TTIME = 2
273 local LOCAL_SEQ = 3
274 local REMOTE_SEQ = 4
276 -- the indices for the State.tcb{} sub-tables
277 local TLOCAL_SEQ = 1
278 local TREMOTE_SEQ = 2
280 -- helper functions
281 local char = string.char
282 local floor = math.floor
284 -- takes a Lua number and converts it into a 2-byte string binary (network order)
286 local function dec2bin16(num)
287 return Struct.pack(">I2",num)
290 -- takes a Lua number and converts it into a 4-byte string binary (network order)
291 local function dec2bin32(num)
292 return Struct.pack(">I4",num)
296 -- function to skip log info before/between/after messages
297 local delim = "^%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-$"
298 -- words that must be found to be skipped. "File ..." is found in 9200 logs)
299 local skiplist = { " OPENED", " CLOSED", " STARTED", " STOPPED", "^File ", delim }
300 -- pre/post NAT entries
301 local pre_nat_header_pattern = "^Pre%-NAT to private<realm=([^>]+)> decode:\r?$"
302 local post_nat_header_pattern = "^Post%-NAT from private<realm=([^>]+)> encoded:\r?$"
304 local function skip_ahead(file, line, position)
305 repeat
306 local found = #line == 0 -- will be false unless the line is empty
307 for i, word in ipairs(skiplist) do
308 if line:find(word) then
309 found = true
310 break
313 if found then
314 position = file:seek()
315 line = file:read()
316 if not line then return nil end
317 elseif line:find(pre_nat_header_pattern) or line:find(post_nat_header_pattern) then
318 -- skip the whole message
319 found = true
320 repeat
321 line = file:read()
322 until line:find(delim)
324 until not found
325 return line, position
328 -- following pattern grabs month, day, hour, min, sec, millisecs
329 local header_time_pattern = "^(%u%l%l) ?(%d%d?) (%d%d?):(%d%d):(%d%d)%.(%d%d%d) On "
330 -- tail'ed file has no month/day
331 local header_tail_time_pattern = "^(%d%d):(%d%d)%.(%d%d%d) On "
333 -- grabs local and remote IPv4:ports (not phy/vlan), and words in between (i.e., "sent to" or "received from")
334 local header_address_pattern = "(%d%d?%d?%.%d%d?%d?%.%d%d?%d?%.%d%d?%d?):(%d+) (%l+ %l+) (%d%d?%d?%.%d%d?%d?%.%d%d?%d?%.%d%d?%d?):(%d+) ?\r?$"
335 -- grabs local and remote IPv6:ports (not phy/vlan), and words in between (i.e., "sent to" or "received from")
336 local header_v6address_pattern = "%[([:%x]+)%]:(%d+) (%l+ %l+) %[([:%x]+)%]:(%d+) ?\r?$"
338 -- grabs phy/vlan info
339 local header_phy_pattern = "%[(%d+):(%d+)%]"
341 local SENT = 1
342 local RECV = 2
343 local function get_direction(phrase)
344 if #phrase == 7 and phrase:find("sent to") then
345 return SENT
346 elseif #phrase == 13 and phrase:find("received from") then
347 return RECV
349 dprint("direction phrase not found")
350 return nil
353 -- monthlist table for getting month number value from 3-char name (number is table index)
354 local monthlist = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
356 -- Compute the difference in seconds between local time and UTC
357 -- from http://lua-users.org/wiki/TimeZone
358 local function get_timezone()
359 local now = os.time()
360 return os.difftime(now, os.time(os.date("!*t", now)))
362 local timezone = get_timezone()
364 function State:get_timestamp(line, file_position, seeking)
365 local i, line_pos, month, day, hour, min, sec, milli = line:find(header_time_pattern)
366 if not month then
367 return
370 if seeking then
371 -- we've seen this packet before, just go get the saved timestamp
372 sec = self.packets[file_position][TTIME]
373 if not sec then
374 dprint("failed to get saved timestamp for packet at position:", file_position)
375 return
377 return sec, line_pos
380 -- find the month's number
381 for index, name in ipairs(monthlist) do
382 if month == name then
383 month = index
384 break
387 if type(month) ~= "number" then return end
389 day = tonumber(day)
390 hour = tonumber(hour)
391 min = tonumber(min)
392 sec = tonumber(sec)
393 milli = tonumber(milli)
395 if not day or not hour or not min or not sec or not milli then
396 dprint("timestamp could not be determined")
397 return nil
400 -- we don't know what year the log file was created, so we have to guess
401 -- if we guess the current system year, then a log of December loaded in January will appear wrong,
402 -- as will a log file which lasts over new year
403 -- so we're going to check the current system month, and if it's less than the log file's then we'll
404 -- assume the log file started last year; if the system month is larger or equal, then we'll assume the log
405 -- file is of this year. We only do this checking once per file.
406 if self.tyear == 0 then
407 local curr_year, curr_month = tonumber(os.date("%Y")), tonumber(os.date("%m"))
408 if curr_month < month then
409 -- use last year
410 if curr_year > 0 then
411 curr_year = curr_year - 1
414 self.tyear = curr_year
416 -- XXX - but for purposes of testing, we just force the year to
417 -- 2014, so that we can compare the result of this code reading
418 -- an Acme log with the result of the pcapng reader reading a
419 -- pcapng file with the same packets - the time stamps in
420 -- pcapng files are times since the Epoch, so the year is known
421 self.tyear = 2014
424 -- if this message's month is less than previous message's, then year wrapped
425 if month < self.tmonth then
426 self.tyear = self.tyear + 1
428 self.tmonth = month
430 local timet = os.time({ ["year"] = self.tyear, ["month"] = month, ["day"] = day, ["hour"] = hour, ["min"] = min, ["sec"] = sec })
431 if not timet then
432 dprint("timestamp conversion failed")
435 timet = timet + timezone
437 -- make an NSTime
438 self.nstime = NSTime(timet, milli * 1000000)
439 self.packets[file_position][TTIME] = self.nstime
441 -- For Lua 5.3 and later, make sure we have an integer via a method
442 -- that also works on Lua 5.1. (os.date doesn't handle fractional seconds.)
443 timet = timet + math.floor(milli/1000)
444 dprint2("found time of ", os.date("%c",timet), " with value=",timet)
446 return self.nstime, line_pos
449 -- get_tail_time() gets a fictitious timestamp starting from 19:00:00 on Dec 31, 1969, and incrementing based
450 -- on the minutes/secs/millisecs seen (i.e., if the minute wrapped then hour increases by 1, etc.).
451 -- this is needed for tail'ed log files, since they don't show month/day/hour
452 function State:get_tail_time(line, file_position, seeking)
453 local i, line_pos, min, sec, milli = line:find(header_tail_time_pattern)
454 if not min then return end
456 if seeking then
457 -- we've seen this packet before, just go get the saved timestamp
458 sec = self.packets[file_position][TTIME]
459 if not sec then
460 dprint("failed to get saved timestamp for packet at position:", file_position)
461 return
463 return sec, line_pos
466 min = tonumber(min)
467 sec = tonumber(sec)
468 milli = tonumber(milli)
470 if not min or not sec or not milli then
471 dprint("timestamp could not be determined")
472 return nil
475 -- get difference in time
476 local tmin, tsec, tmilli, nstime = self.tmin, self.tsec, self.tmilli, self.nstime
477 local ttime = nstime.secs
479 -- min, sec, milli are what the log says this tail'ed packet is
480 -- tmin, tsec, tmilli are what we got from last packet
481 -- nstime is the unix time of that, and ttime is the seconds of that unix time
483 -- if minutes wrapped, or they're equal but seconds wrapped, then handle it as if in the next hour
484 if (min < tmin) or (min == tmin and sec < tsec) or (min == tmin and sec == tsec and milli < tmilli) then
485 -- something wrapped, calculate difference as if in next hour
486 ttime = ttime + (((min * 60) + sec + 3600) - ((tmin * 60) + tsec))
487 else
488 ttime = ttime + (((min * 60) + sec) - ((tmin * 60) + tsec))
490 self.tmin, self.tsec, self.tmilli = min, sec, milli
491 self.nstime = NSTime(ttime, milli * 1000000)
492 self.packets[file_position][TTIME] = self.nstime
494 return self.nstime, line_pos
497 local hexbin = {
498 ["0"]=0, ["1"]=1, ["2"]=2, ["3"]=3, ["4"]=4, ["5"]=5, ["6"]=6, ["7"]=7, ["8"]=8, ["9"]=9, ["a"]=10, ["b"]=11, ["c"]=12, ["d"]=13, ["e"]=14, ["f"]=15,
499 ["00"]=0, ["01"]=1, ["02"]=2, ["03"]=3, ["04"]=4, ["05"]=5, ["06"]=6, ["07"]=7, ["08"]=8, ["09"]=9, ["0a"]=10, ["0b"]=11, ["0c"]=12, ["0d"]=13, ["0e"]=14, ["0f"]=15,
500 ["10"]=16, ["11"]=17, ["12"]=18, ["13"]=19, ["14"]=20, ["15"]=21, ["16"]=22, ["17"]=23, ["18"]=24, ["19"]=25, ["1a"]=26, ["1b"]=27, ["1c"]=28, ["1d"]=29, ["1e"]=30, ["1f"]=31,
501 ["20"]=32, ["21"]=33, ["22"]=34, ["23"]=35, ["24"]=36, ["25"]=37, ["26"]=38, ["27"]=39, ["28"]=40, ["29"]=41, ["2a"]=42, ["2b"]=43, ["2c"]=44, ["2d"]=45, ["2e"]=46, ["2f"]=47,
502 ["30"]=48, ["31"]=49, ["32"]=50, ["33"]=51, ["34"]=52, ["35"]=53, ["36"]=54, ["37"]=55, ["38"]=56, ["39"]=57, ["3a"]=58, ["3b"]=59, ["3c"]=60, ["3d"]=61, ["3e"]=62, ["3f"]=63,
503 ["40"]=64, ["41"]=65, ["42"]=66, ["43"]=67, ["44"]=68, ["45"]=69, ["46"]=70, ["47"]=71, ["48"]=72, ["49"]=73, ["4a"]=74, ["4b"]=75, ["4c"]=76, ["4d"]=77, ["4e"]=78, ["4f"]=79,
504 ["50"]=80, ["51"]=81, ["52"]=82, ["53"]=83, ["54"]=84, ["55"]=85, ["56"]=86, ["57"]=87, ["58"]=88, ["59"]=89, ["5a"]=90, ["5b"]=91, ["5c"]=92, ["5d"]=93, ["5e"]=94, ["5f"]=95,
505 ["60"]=96, ["61"]=97, ["62"]=98, ["63"]=99, ["64"]=100, ["65"]=101, ["66"]=102, ["67"]=103, ["68"]=104, ["69"]=105, ["6a"]=106, ["6b"]=107, ["6c"]=108, ["6d"]=109, ["6e"]=110, ["6f"]=111,
506 ["70"]=112, ["71"]=113, ["72"]=114, ["73"]=115, ["74"]=116, ["75"]=117, ["76"]=118, ["77"]=119, ["78"]=120, ["79"]=121, ["7a"]=122, ["7b"]=123, ["7c"]=124, ["7d"]=125, ["7e"]=126, ["7f"]=127,
507 ["80"]=128, ["81"]=129, ["82"]=130, ["83"]=131, ["84"]=132, ["85"]=133, ["86"]=134, ["87"]=135, ["88"]=136, ["89"]=137, ["8a"]=138, ["8b"]=139, ["8c"]=140, ["8d"]=141, ["8e"]=142, ["8f"]=143,
508 ["90"]=144, ["91"]=145, ["92"]=146, ["93"]=147, ["94"]=148, ["95"]=149, ["96"]=150, ["97"]=151, ["98"]=152, ["99"]=153, ["9a"]=154, ["9b"]=155, ["9c"]=156, ["9d"]=157, ["9e"]=158, ["9f"]=159,
509 ["a0"]=160, ["a1"]=161, ["a2"]=162, ["a3"]=163, ["a4"]=164, ["a5"]=165, ["a6"]=166, ["a7"]=167, ["a8"]=168, ["a9"]=169, ["aa"]=170, ["ab"]=171, ["ac"]=172, ["ad"]=173, ["ae"]=174, ["af"]=175,
510 ["b0"]=176, ["b1"]=177, ["b2"]=178, ["b3"]=179, ["b4"]=180, ["b5"]=181, ["b6"]=182, ["b7"]=183, ["b8"]=184, ["b9"]=185, ["ba"]=186, ["bb"]=187, ["bc"]=188, ["bd"]=189, ["be"]=190, ["bf"]=191,
511 ["c0"]=192, ["c1"]=193, ["c2"]=194, ["c3"]=195, ["c4"]=196, ["c5"]=197, ["c6"]=198, ["c7"]=199, ["c8"]=200, ["c9"]=201, ["ca"]=202, ["cb"]=203, ["cc"]=204, ["cd"]=205, ["ce"]=206, ["cf"]=207,
512 ["d0"]=208, ["d1"]=209, ["d2"]=210, ["d3"]=211, ["d4"]=212, ["d5"]=213, ["d6"]=214, ["d7"]=215, ["d8"]=216, ["d9"]=217, ["da"]=218, ["db"]=219, ["dc"]=220, ["dd"]=221, ["de"]=222, ["df"]=223,
513 ["e0"]=224, ["e1"]=225, ["e2"]=226, ["e3"]=227, ["e4"]=228, ["e5"]=229, ["e6"]=230, ["e7"]=231, ["e8"]=232, ["e9"]=233, ["ea"]=234, ["eb"]=235, ["ec"]=236, ["ed"]=237, ["ee"]=238, ["ef"]=239,
514 ["f0"]=240, ["f1"]=241, ["f2"]=242, ["f3"]=243, ["f4"]=244, ["f5"]=245, ["f6"]=246, ["f7"]=247, ["f8"]=248, ["f9"]=249, ["fa"]=250, ["fb"]=251, ["fc"]=252, ["fd"]=253, ["fe"]=254, ["ff"]=255
517 local function iptobytes(ipaddr)
518 local bytes = { ipaddr:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") }
519 if not #bytes == 4 then
520 dprint("failed to get ip address bytes for '", ipaddr, "'")
521 return
523 local ip = ""
524 for i, byte in ipairs(bytes) do
525 ip = ip .. char(tonumber(byte))
527 return ip
530 local function hexword2bin(word)
531 if #word == 4 then
532 return char(hexbin[word:sub(1,2)], hexbin[word:sub(3,4)])
533 elseif #word == 3 then
534 return char(hexbin[word:sub(1,1)], hexbin[word:sub(2,3)])
535 elseif #word < 3 then
536 return char(0, hexbin[word])
538 return nil -- error
541 -- convert this 2620:0:60:8ac::102 to its 16-byte binary (=8 of 2-byte words)
542 local NUMWORDS = 8
543 local function ipv6tobytes(ipaddr)
544 -- start with all 16 bytes being zeroes
545 local words = { "\00\00", "\00\00", "\00\00", "\00\00", "\00\00", "\00\00", "\00\00", "\00\00" }
546 -- now walk from front of ipv6 address string replacing byte numbers above;
547 -- if we hit a "::", then jump to end and do it in reverse
548 local colon_s, colon_e = ipaddr:find("::%x")
549 if colon_s then
550 -- there's a double-colon, so split the string and do the end first, backwards
551 -- get each chunk first
552 local t = {}
553 local index, wordix = 1, NUMWORDS
554 for w in string.gmatch(ipaddr:sub(colon_e - 1), ":(%x+)") do
555 t[index] = hexword2bin(w)
556 index = index + 1
558 for ix=index-1, 1, -1 do
559 words[wordix] = t[ix]
560 wordix = wordix - 1
562 ipaddr = ipaddr:sub(1, colon_s)
565 local i = 1
566 for w in string.gmatch(ipaddr, "(%x+):?") do
567 words[i] = hexword2bin(w)
568 i = i + 1
571 if not #words == NUMWORDS then
572 dprint("failed to get IPv6 address bytes for '", ipaddr, "'")
573 return
576 return table.concat(words)
579 -- calculates checksum as done for IP, TCP, UDP
580 local function checksum(chunk)
581 local sum = 0
582 -- take every 2-byte value and add them up
583 for one, two in chunk:gmatch("(.)(.)") do
584 sum = sum + (string.byte(one) * 256) + (string.byte(two))
585 while floor(sum / 65536) > 0 do
586 -- add carry/overflow value
587 sum = (sum % 65536) + (floor(sum / 65536))
591 -- now get one's complement of that
592 sum = 65535 - sum
594 -- and return it as a 2-byte string
595 return dec2bin16(sum)
598 ----------------------------------------
599 -- protocol type number
600 local PROTO_UDP = "\17"
601 local PROTO_TCP = "\06"
602 -- enum
603 local IPv4 = 1
604 local IPv6 = 2
605 -- both type enums and header lengths
606 local UDP = 8
607 local TCP = 20
609 ----------------------------------------
610 -- Packet creation/serialization occurs using a Lua class object model
611 -- There's a single base class 'Packet' which has data/methods every packet type has
612 -- 'RawPacket' and 'DataPacket' both derive from 'Packet'.
613 -- 'RawPacket' is for packets which the log file has the raw IP/UDP headers for,
614 -- such as ALG log messages (MGCP/NCS). Since the IP headers are in them, we use those.
615 -- 'DataPacket' is for packets which the log file only has payload data for, and
616 -- we need to create fake IP/UDP or IP/TCP headers for.
617 -- 'BinPacket' and'AsciiPacket' both derive from 'DataPacket'.
618 -- 'BinPacket' is for binary-style logged packets, such as MBCD or DNS, while
619 -- 'AsciiPacket' is for ascii-style ones such as SIP.
620 -- 'DnsPacket' derives from 'BinPacket', for DNS-style logs.
622 -- Each class has a read_data() method, which reads in the packet data, builds the packet,
623 -- and sets the Wireshark buffer. Some classes have a get_data() method which read_data()
624 -- calls, to get the payload data before building a fake packet.
626 -- The base Packet class has a get_hex_data() and get_ascii_data() methods, to get the payload
627 -- in either form, and those base methods are called by get_data() or read_data() of derived
628 -- classes.
630 -- For performance reasons, packet data is read line-by-line into a table (called bufftbl),
631 -- which is concatenated at the end. This avoids Lua building interim strings and garbage
632 -- collecting them. But it makes the code uglier. The get_data()/get_hex_data()/get_ascii_data()
633 -- methods read into this table they get passed, while the read_data() functions handle managing
634 -- the table.
636 ----------------------------------------
637 ----------------------------------------
638 -- The base Packet class, from which others derive
639 -- all Packets have a ptype, timestamp, source and dest address:port, and data
641 local Packet = {}
642 local Packet_mt = { __index = Packet }
644 function Packet.new(state, timestamp, direction, source_ip, source_port, dest_ip, dest_port, ptype, ttype, file_position)
645 local new_class = { -- the new instance
646 ["state"] = state,
647 ["timestamp"] = timestamp,
648 ["direction"] = direction,
649 ["source_ip"] = source_ip,
650 ["source_port"] = source_port,
651 ["dest_ip"] = dest_ip,
652 ["dest_port"] = dest_port,
653 ["ptype"] = ptype,
654 ["ttype"] = ttype,
655 ["file_position"] = file_position
657 setmetatable( new_class, Packet_mt ) -- all instances share the same metatable
658 return new_class
661 function Packet:set_comment(comment)
662 self["comment"] = comment
665 function Packet:set_wslua_fields(frame)
666 frame.time = self.timestamp
667 frame.rec_type = wtap_rec_types.PACKET
668 frame.flags = wtap_presence_flags.TS -- for timestamp
669 if self.comment then
670 frame.comment = self.comment
672 return true
675 local packet_hexline_pattern = "^ %x%x%x0: %x%x"
676 function Packet:get_hex_data(file, line, bufftbl, index)
677 local start = index
679 dprint2("Packet:get_hex_data() called")
680 repeat
681 for word in line:gmatch("(%x%x) ") do
682 bufftbl[index] = char(hexbin[word])
683 index = index + 1
684 if ((index - start) % 16) == 0 then break end
686 line = file:read()
687 until not line or not line:find(packet_hexline_pattern)
689 return index - start, line
692 function Packet:get_ascii_data(file, line, bufftbl, index, only_newline)
693 local bufflen = 0 -- keep tally of total length of payload
694 local found_delim = true
696 dprint2("Packet:get_ascii_data() called")
697 repeat
698 bufftbl[index] = line
699 bufflen = bufflen + #line
701 -- sanity check if line has "\r" at end, and if so only add \n
702 if line:find("\r",-1,true) then
703 bufftbl[index+1] = "\n"
704 bufflen = bufflen + 1
705 dprint2("Found carriage-return at end of line")
706 elseif only_newline then
707 -- only add a newline
708 bufftbl[index+1] = "\n"
709 bufflen = bufflen + 1
710 else
711 bufftbl[index+1] = "\r\n"
712 bufflen = bufflen + 2
714 index = index + 2
716 -- read next line now
717 line = file:read()
718 if not line then
719 -- hit eof?
720 found_delim = false
721 break
724 until line:find(delim)
726 -- get rid of last \r\n, if we found a dashed delimiter, as it's not part of packet
727 if found_delim then
728 bufflen = bufflen - bufftbl[index-1]:len()
729 bufftbl[index-1] = nil
732 dprint2("Packet:get_ascii_data() returning", bufflen)
733 return bufflen
736 ----------------------------------------
737 -- RawPacket class, for packets that the log file contains the whole IP header for, such as algd logs
739 local RawPacket = {}
740 local RawPacket_mt = { __index = RawPacket }
741 setmetatable( RawPacket, Packet_mt ) -- make RawPacket inherit from Packet
743 function RawPacket.new(...)
744 local new_class = Packet.new(...) -- the new instance
745 setmetatable( new_class, RawPacket_mt ) -- all instances share the same metatable
746 return new_class
749 function RawPacket:read_data(file, frame, line, seeking)
750 local bufftbl = {} -- table to hold data bytes
751 local index = 1 -- start at first slot in array
753 -- need to skip "Packet:" line and first 0000: line, it's internal junk
754 line = file:read()
755 line = file:read()
757 dprint2("RawPacket:read_data() getting hex from line='", line, "'")
758 local bufflen, line = self:get_hex_data(file, line, bufftbl, index)
759 if not bufflen or bufflen < 21 then
760 dprint("error getting binary data")
761 return false
764 -- add remainder as more packet data, but first delete overlap
765 -- see if frag bits are set in IP header, to see if UDP/TCP header exists
766 if self.ptype == IPv4 then
767 -- grab byte with frag flags and first byte of offset
768 local flag = string.byte(bufftbl[7]) -- converts binary character to number
769 local frag_offset = flag % 32 -- masks off upper 3 bits
770 frag_offset = (frag_offset * 256) + string.byte(bufftbl[8])
771 flag = floor(flag / 224) -- shift right
772 flag = flag % 2 -- mask upper bits
773 if flag == 1 or frag_offset > 0 then
774 -- we have a fragmented IPv4 packet, so no proto header
775 -- only save first 20 bytes (the IP header)
776 for i=bufflen, 21, -1 do
777 bufftbl[i] = nil
779 bufflen = 20
780 else
781 -- only save first 20 + proto size bytes
782 local save
783 if bufftbl[10] == PROTO_UDP then
784 save = 28
785 elseif bufftbl[10] == PROTO_TCP then
786 save = 40
787 else
788 dprint("failed to fix raw packet overlap")
789 return
791 for i=bufflen, save+1, -1 do
792 bufftbl[i] = nil
794 bufflen = save
797 -- TODO: IPv6
799 -- now read in rest of message, if any
800 -- first skip extra empty newline
801 if #line == 0 then
802 line = file:read()
805 bufflen = bufflen + self:get_ascii_data(file, line, bufftbl, bufflen+1, true)
807 frame.data = table.concat(bufftbl)
809 return true
812 ----------------------------------------
813 -- DataPacket class, for packets that the log file contains just the payload data for
815 local DataPacket = {}
816 local DataPacket_mt = { __index = DataPacket }
817 setmetatable( DataPacket, Packet_mt ) -- make DataPacket inherit from Packet
819 function DataPacket.new(...)
820 local new_class = Packet.new(...) -- the new instance
821 setmetatable( new_class, DataPacket_mt ) -- all instances share the same metatable
822 return new_class
825 function DataPacket:set_tcbkey(key)
826 self["tcbkey"] = key
827 return
830 function DataPacket:build_ipv4_hdr(bufflen, proto, seeking)
831 local len = bufflen + 20 -- 20 byte IPv4 header size
833 -- figure out the ip identification value
834 local ip_ident
835 if seeking then
836 ip_ident = self.state.packets[self.file_position][IP_IDENT]
837 else
838 -- increment ident value
839 self.state.ip_ident = self.state.ip_ident + 1
840 if self.state.ip_ident == 65536 then
841 self.state.ip_ident = 1
843 ip_ident = self.state.ip_ident
844 -- save it for future seeking
845 self.state.packets[self.file_position][IP_IDENT] = ip_ident
848 -- use a table to concatenate as it's slightly faster that way
849 local hdrtbl = {
850 "\69\00", -- 1=ipv4 and 20 byte header length
851 dec2bin16(len), -- 2=packet length bytes
852 dec2bin16(ip_ident), -- 3=ident field bytes
853 "\00\00\64", -- 4=flags/fragment offset, ttl
854 proto, -- 5=proto
855 "\00\00", -- 6=checksum (using zero for now)
856 iptobytes(self.source_ip), -- 7=source ip
857 iptobytes(self.dest_ip) -- 8=dest ip
860 -- calc IPv4 header checksum, and set its value
861 hdrtbl[6] = checksum(table.concat(hdrtbl))
863 return table.concat(hdrtbl)
866 function DataPacket:build_ipv6_hdr(bufflen, proto)
867 -- use a table to concatenate as it's slightly faster that way
868 local hdrtbl = {
869 "\96\00\00\00", -- 1=ipv6 version, class, label
870 dec2bin16(bufflen), -- 2=packet length bytes
871 proto .. "\64", -- 4=proto, ttl
872 ipv6tobytes(self.source_ip), -- 5=source ip
873 ipv6tobytes(self.dest_ip) -- 6=dest ip
875 return table.concat(hdrtbl)
878 -- calculates TCP/UDP header checksums with pseudo-header info
879 function DataPacket:calc_header_checksum(bufftbl, bufflen, hdrtbl, proto)
880 -- first create pseudo IP header
881 if self.ptype == IPv4 then
882 local iphdrtbl = {
883 iptobytes(self.source_ip), -- 1=source ip
884 iptobytes(self.dest_ip), -- 2=dest ip
885 "\00", -- zeros
886 proto, -- proto
887 dec2bin16(bufflen) -- payload length bytes
889 bufftbl[1] = table.concat(iphdrtbl)
890 elseif self.ptype == IPv6 then
891 local iphdrtbl = {
892 ipv6tobytes(self.source_ip), -- 1=source ip
893 ipv6tobytes(self.dest_ip), -- 2=dest ip
894 "\00\00", -- zeroes
895 dec2bin16(bufflen), -- payload length bytes
896 "\00\00\00", -- zeros
897 proto -- proto
899 bufftbl[1] = table.concat(iphdrtbl)
902 -- and pseudo TCP or UDP header
903 bufftbl[2] = table.concat(hdrtbl)
905 -- see if payload is odd length
906 local odd = false
907 if bufflen % 2 == 1 then
908 -- odd number of payload bytes, add zero byte at end
909 odd = true -- remember to undo this
910 bufftbl[#bufftbl+1] = "\00"
913 local result = checksum(table.concat(bufftbl))
915 -- remove pseudo-headers
916 bufftbl[1] = nil
917 bufftbl[2] = nil
918 if odd then
919 bufftbl[#bufftbl] = nil
922 return result
926 function DataPacket:build_udp_hdr(bufflen, bufftbl)
927 local len = bufflen + 8 -- 8 for size of UDP header
928 local hdrtbl = {
929 dec2bin16(self.source_port), -- 1=source port bytes
930 dec2bin16(self.dest_port), -- 2=dest port bytes
931 dec2bin16(len), -- 3=payload length bytes
932 "\00\00" -- 4=checksum
934 if bufftbl then
935 -- calc udp checksum (only done for IPv6)
936 hdrtbl[4] = self:calc_header_checksum(bufftbl, len, hdrtbl, PROTO_UDP)
938 return table.concat(hdrtbl)
942 function DataPacket:build_tcp_hdr(bufflen, bufftbl, seeking)
943 local len = bufflen + 20 -- 20 for size of TCP header
945 local local_seq, remote_seq
946 if seeking then
947 local_seq = self.state.packets[self.file_position][LOCAL_SEQ]
948 remote_seq = self.state.packets[self.file_position][REMOTE_SEQ]
949 else
950 -- find socket/tcb info for this "stream", create if not found
951 if not self.state.tcb[self.tcbkey] then
952 -- create them
953 self.state.tcb[self.tcbkey] = {}
954 local_seq = 1
955 remote_seq = 1
956 self.state.packets[self.file_position][LOCAL_SEQ] = 1
957 self.state.packets[self.file_position][REMOTE_SEQ] = 1
958 -- set tcb to next sequence numbers, so that the correct "side"
959 -- acknowledges receiving these bytes
960 if self.direction == SENT then
961 -- this packet is being sent, so local sequence increases next time
962 self.state.tcb[self.tcbkey][TLOCAL_SEQ] = bufflen+1
963 self.state.tcb[self.tcbkey][TREMOTE_SEQ] = 1
964 else
965 -- this packet is being received, so remote sequence increases next time
966 -- and local side will acknowldge it next time
967 self.state.tcb[self.tcbkey][TLOCAL_SEQ] = 1
968 self.state.tcb[self.tcbkey][TREMOTE_SEQ] = bufflen+1
970 else
971 -- stream already exists, so send the current tcb seqs and update for next time
972 if self.direction == SENT then
973 -- this packet is being sent, so local sequence increases next time
974 local_seq = self.state.tcb[self.tcbkey][TLOCAL_SEQ]
975 remote_seq = self.state.tcb[self.tcbkey][TREMOTE_SEQ]
976 self.state.tcb[self.tcbkey][TLOCAL_SEQ] = local_seq + bufflen
977 else
978 -- this packet is being received, so the "local" seq number of the packet is the remote's seq really
979 local_seq = self.state.tcb[self.tcbkey][TREMOTE_SEQ]
980 remote_seq = self.state.tcb[self.tcbkey][TLOCAL_SEQ]
981 -- and remote seq needs to increase next time (remember local_seq is TREMOTE_SEQ)
982 self.state.tcb[self.tcbkey][TREMOTE_SEQ] = local_seq + bufflen
984 self.state.packets[self.file_position][LOCAL_SEQ] = local_seq
985 self.state.packets[self.file_position][REMOTE_SEQ] = remote_seq
989 local hdrtbl = {
990 dec2bin16(self.source_port), -- 1=source port bytes
991 dec2bin16(self.dest_port), -- 2=dest port bytes
992 dec2bin32(local_seq), -- 3=sequence
993 dec2bin32(remote_seq), -- 4=ack number
994 "\80\16\255\255", -- 5=offset, flags, window size
995 "\00\00", -- 6=checksum
996 "\00\00" -- 7=urgent pointer
999 -- calc tcp checksum
1000 hdrtbl[6] = self:calc_header_checksum(bufftbl, len, hdrtbl, PROTO_TCP)
1002 return table.concat(hdrtbl)
1005 function DataPacket:build_packet(bufftbl, bufflen, seeking)
1006 dprint2("DataPacket:build_packet() called with ptype=",self.ptype)
1007 if self.ptype == IPv4 then
1008 if self.ttype == UDP then
1009 bufftbl[2] = self:build_udp_hdr(bufflen)
1010 bufftbl[1] = self:build_ipv4_hdr(bufflen + 8, PROTO_UDP, seeking)
1011 elseif self.ttype == TCP then
1012 bufftbl[2] = self:build_tcp_hdr(bufflen, bufftbl, seeking)
1013 bufftbl[1] = self:build_ipv4_hdr(bufflen + 20, PROTO_TCP, seeking)
1015 elseif self.ptype == IPv6 then
1016 -- UDP for IPv6 requires checksum calculation, so we can't avoid more work
1017 if self.ttype == UDP then
1018 bufftbl[2] = self:build_udp_hdr(bufflen, bufftbl)
1019 bufftbl[1] = self:build_ipv6_hdr(bufflen + 8, PROTO_UDP)
1020 elseif self.ttype == TCP then
1021 bufftbl[2] = self:build_tcp_hdr(bufflen, bufftbl, seeking)
1022 bufftbl[1] = self:build_ipv6_hdr(bufflen + 20, PROTO_TCP)
1024 else
1025 dprint("DataPacket:build_packet: invalid packet type (neither IPv4 nor IPv6)")
1026 return nil
1029 return table.concat(bufftbl)
1032 -- for performance, we read each line into a table and concatenate it at end
1033 -- but it makes this code super ugly
1034 function DataPacket:read_data(file, frame, line, seeking)
1035 local bufftbl = { "", "" } -- 2 slots for ip and udp/tcp headers
1036 local index = 3 -- start at third slot in array
1037 local comment -- for any packet comments
1039 dprint2("DataPacket: read_data(): calling get_data")
1040 local bufflen = self:get_data(file, line, bufftbl, index)
1041 if not bufflen then
1042 dprint("DataPacket: error getting ascii or binary data")
1043 return false
1046 local buff = self:build_packet(bufftbl, bufflen, seeking)
1048 frame.data = buff
1050 return true
1054 ----------------------------------------
1055 -- BinPacket class, for packets that the log file contains binary payload data for, such as MBCD
1057 local BinPacket = {}
1058 local BinPacket_mt = { __index = BinPacket }
1059 setmetatable( BinPacket, DataPacket_mt ) -- make BinPacket inherit from DataPacket
1061 function BinPacket.new(...)
1062 local new_class = DataPacket.new(...) -- the new instance
1063 setmetatable( new_class, BinPacket_mt ) -- all instances share the same metatable
1064 return new_class
1067 function BinPacket:get_comment_data(file, line, stop_pattern)
1068 local comments = {}
1070 while line and not line:find(stop_pattern) do
1071 if #line > 0 then
1072 comments[#comments+1] = line
1073 comments[#comments+1] = "\r\n"
1075 line = file:read()
1078 if #comments > 0 then
1079 -- get rid of extra "\r\n"
1080 comments[#comments] = nil
1081 self:set_comment(table.concat(comments))
1084 return line
1087 function BinPacket:get_data(file, line, bufftbl, index)
1088 local is_alg = false
1090 local bufflen, line = self:get_hex_data(file, line, bufftbl, index)
1092 -- now eat rest of message until delimiter or end of file
1093 -- we'll put them in comments
1094 line = self:get_comment_data(file, line, delim)
1096 -- return the bufflen, which is the same as number of table entries we made
1097 return bufflen
1100 ----------------------------------------
1101 -- DnsPacket class, for DNS packets (which are binary but with comments at top)
1103 local DnsPacket = {}
1104 local DnsPacket_mt = { __index = DnsPacket }
1105 setmetatable( DnsPacket, BinPacket_mt ) -- make DnsPacket inherit from BinPacket
1107 function DnsPacket.new(...)
1108 local new_class = BinPacket.new(...) -- the new instance
1109 setmetatable( new_class, DnsPacket_mt ) -- all instances share the same metatable
1110 return new_class
1113 local binpacket_start_pattern = "^ 0000: %x%x %x%x %x%x %x%x %x%x %x%x %x%x %x%x "
1114 function DnsPacket:get_data(file, line, bufftbl, index)
1115 -- it's UDP regardless of what parse_header() thinks
1116 self.ttype = UDP
1118 -- comments are at top instead of bottom of message
1119 line = self:get_comment_data(file, line, binpacket_start_pattern)
1121 local bufflen, line = self:get_hex_data(file, line, bufftbl, index)
1123 -- now eat rest of message until delimiter or end of file
1124 while line and not line:find(delim) do
1125 line = file:read()
1128 -- return the bufflen, which is the same as number of table entries we made
1129 return bufflen
1132 ----------------------------------------
1133 -- AsciiPacket class, for packets that the log file contains ascii payload data for
1135 local AsciiPacket = {}
1136 local AsciiPacket_mt = { __index = AsciiPacket }
1137 setmetatable( AsciiPacket, DataPacket_mt ) -- make AsciiPacket inherit from DataPacket
1139 function AsciiPacket.new(...)
1140 local new_class = DataPacket.new(...) -- the new instance
1141 setmetatable( new_class, AsciiPacket_mt ) -- all instances share the same metatable
1142 return new_class
1145 function AsciiPacket:get_data(file, line, bufftbl, index)
1146 return self:get_ascii_data(file, line, bufftbl, index)
1150 ----------------------------------------
1151 -- To determine packet type, we peek at the first line of 'data' following the log
1152 -- message header. Its pattern determines the Packet object type.
1153 -- The following are the patterns we look for; if it doesn't match one of these,
1154 -- then it's an AsciiPacket:
1155 local packet_patterns = {
1156 { "^ 0000: %x%x %x%x %x%x %x%x %x%x %x%x %x%x %x%x ", BinPacket },
1157 { "^Packet:$", RawPacket },
1158 { "^DNS Query %d+ flags=%d+ q=%d+ ans=%d+", DnsPacket },
1159 { "^DNS Response %d+ flags=%d+ q=%d+ ans=%d+", DnsPacket }
1161 -- indeces for above
1162 local PP_PATTERN = 1
1163 local PP_CLASS = 2
1165 local function get_packet_class(line)
1166 for i, t in ipairs(packet_patterns) do
1167 if line:find(t[PP_PATTERN]) then
1168 dprint2("got class type=",i)
1169 return t[PP_CLASS]
1172 dprint2("got class type AsciiPacket")
1173 return AsciiPacket
1176 ----------------------------------------
1177 -- parses header line
1178 -- returns nil on failure
1179 -- the header lines look like this:
1180 -- Aug 10 14:30:11.134 On [1:544]10.201.145.237:5060 received from 10.210.1.193:5060
1181 -- this one has no phy/vlan info in brackets:
1182 -- Mar 6 13:39:06.122 On 127.0.0.1:2945 sent to 127.0.0.1:2944
1183 -- this one is IPv6:
1184 -- Aug 10 14:30:11.140 On [3:0][2620:0:60:8ac::102]:5060 sent to [2620:0:60:8ab::12]:5060
1185 -- this is from a tail'ed log output:
1186 -- 52:22.434 On [0:0]205.152.56.211:5060 received from 205.152.56.75:5060
1187 local loopback_pattern = "^127%.0%.0%.%d+$"
1188 local function parse_header(state, file, line, file_position, seeking)
1190 if seeking then
1191 -- verify we've seen this packet before
1192 if not state.packets[file_position] then
1193 dprint("parse_header: packet at file position ", file_position, " not saved previously")
1194 return
1196 else
1197 -- first time through, create sub-table for the packet
1198 state.packets[file_position] = {}
1201 -- get time info, and line match ending position
1202 local timestamp, line_pos = state:get_timestamp(line, file_position, seeking)
1203 if not timestamp then
1204 -- see if it's a tail'ed log instead
1205 timestamp, line_pos = state:get_tail_time(line, file_position, seeking)
1208 if not timestamp then
1209 dprint("parse_header: could not parse time portion")
1210 return
1213 local ptype, ttype = IPv4, UDP
1215 -- get phy/vlan if present
1216 -- first skip past time portion
1217 local phy, vlan, i, j, k
1218 line_pos = line_pos + 1
1219 i, j, phy, vlan = line:find(header_phy_pattern, line_pos)
1220 if i then
1221 phy = tonumber(phy)
1222 vlan = tonumber(vlan)
1223 line_pos = j -- skip past this portion for next match
1224 else
1225 -- if there's no phy/vlan info, then assume it's TCP (unless it's loopback address we'll check later)
1226 ttype = TCP
1229 -- get addresses and direction
1230 local local_ip, local_port, direction, remote_ip, remote_port = line:match(header_address_pattern, line_pos)
1231 if not local_ip then
1232 -- try IPv6
1233 local_ip, local_port, direction, remote_ip, remote_port = line:match(header_v6address_pattern, line_pos)
1234 if not local_ip then
1235 dprint("parse_header: could not parse address portion")
1236 return nil
1238 ptype = IPv6
1241 if local_ip:find(loopback_pattern) and remote_ip:find(loopback_pattern) then
1242 -- internal loopback packets never have phy/vlan but are always UDP messages (for all intents)
1243 ttype = UDP
1246 -- override above decisions based on configuration
1247 if ALWAYS_UDP then
1248 ttype = UDP
1251 direction = get_direction(direction)
1252 if direction == nil then
1253 dprint("parse_header: failed to convert direction")
1254 return nil
1257 local source_ip, source_port, dest_ip, dest_port = local_ip, local_port, remote_ip, remote_port
1258 if direction == RECV then
1259 -- swap them
1260 source_ip, source_port, dest_ip, dest_port = remote_ip, remote_port, local_ip, local_port
1262 -- convert
1263 source_port = tonumber(source_port)
1264 dest_port = tonumber(dest_port)
1266 -- peek at next line to determine packet type
1267 local position = file:seek()
1268 line = file:read()
1269 dprint2("parse_header: peeking at line='", line, "'")
1270 packet_class = get_packet_class(line)
1271 file:seek("set", position) -- go back
1273 dprint2("parse_header calling packet_class.new with:",
1274 tostring(timestamp), direction, source_ip, source_port,
1275 dest_ip, dest_port, ptype, ttype, file_position)
1277 local packet = packet_class.new(state, timestamp, direction, source_ip, source_port, dest_ip, dest_port, ptype, ttype, file_position)
1278 if not packet then
1279 dprint("parse_header: parser failed to create Packet object")
1282 if ttype == TCP then
1283 -- if the packet is tcp type, then set the key for TCB table lookup
1284 packet:set_tcbkey(table.concat({ "[", local_ip, "]:", local_port, "->[", remote_ip, "]:", remote_port }))
1287 return packet
1291 ----------------------------------------
1292 -- file handling functions for Wireshark to use
1294 -- The read_open is called by Wireshark once per file, to see if the file is this reader's type.
1295 -- It passes in (1) a File and (2) CaptureInfo object to this function
1296 -- Since there is no exact magic sequence to search for, we have to use heuristics to guess if the file
1297 -- is our type or not, which we do by parsing a message header.
1298 -- Since Wireshark uses the file cursor position for future reading of this file, we also have to seek back to the beginning
1299 -- so that our normal read() function works correctly.
1300 local function read_open(file, capture)
1301 dprint2("read_open called")
1302 -- save current position to return later
1303 local position = file:seek()
1305 local line = file:read()
1306 if not line then return false end
1308 dprint2("read_open: got this line begin:\n'", line, "'")
1310 line, position = skip_ahead(file, line, position)
1311 if not line then return false end
1313 dprint2("read_open: got this line after skip:\n'", line, "', with position=", position)
1315 local state = State.new()
1317 if parse_header(state, file, line, position) then
1318 dprint2("read_open success")
1320 file:seek("set",position)
1322 capture.time_precision = wtap_filetypes.TSPREC_MSEC -- for millisecond precision
1323 capture.encap = wtap.RAW_IP -- whole file is raw IP format
1324 capture.snapshot_length = 0 -- unknown snaplen
1325 capture.comment = "Oracle Acme Packet SBC message log"
1326 capture.os = "VxWorks or Linux"
1327 capture.hardware = "Oracle Acme Packet SBC"
1329 -- reset state variables
1330 capture.private_table = State.new()
1332 dprint2("read_open returning true")
1333 return true
1336 dprint2("read_open returning false")
1337 return false
1340 ----------------------------------------
1341 -- this is used by both read() and seek_read()
1342 local function read_common(funcname, file, capture, frame, position, seeking)
1343 dprint2(funcname, "read_common called")
1344 local state = capture.private_table
1346 if not state then
1347 dprint(funcname, "error getting capture state")
1348 return false
1351 local line = file:read()
1352 if not line then
1353 dprint(funcname, "hit end of file")
1354 return false
1356 line, position = skip_ahead(file, line, position)
1357 if not line then
1358 if file:read(0) ~= nil then
1359 dprint(funcname, "did not hit end of file after skipping but ending anyway")
1360 else
1361 dprint2(funcname, "hit end of file after skipping")
1363 return false
1366 dprint2(funcname, ": parsing line='", line, "'")
1367 local phdr = parse_header(state, file, line, position, seeking)
1368 if not phdr then
1369 dprint(funcname, "failed to parse header")
1370 return false
1373 line = file:read()
1375 dprint2(funcname,": calling class object's read_data()")
1376 phdr:read_data(file, frame, line, seeking)
1378 if not phdr:set_wslua_fields(frame) then
1379 dprint(funcname, "failed to set Wireshark packet header info")
1380 return
1383 dprint2(funcname, "read_common returning position")
1384 return position
1387 ----------------------------------------
1388 -- Wireshark/tshark calls read() for each frame/record in the file
1389 -- It passes in (1) a File, (2) CaptureInfo, and (3) a FrameInfo object to this function
1390 -- It expects in return the file offset position the record starts at,
1391 -- or nil/false if there's an error or end-of-file is reached.
1392 -- The offset position is used later: wireshark remembers it and gives
1393 -- it to seek_read() at various random times
1394 local function read(file, capture, frame)
1395 dprint2("read called")
1396 local position = file:seek()
1397 position = read_common("read", file, capture, frame, position)
1398 if not position then
1399 if file:read(0) ~= nil then
1400 dprint("read failed to call read_common")
1401 else
1402 dprint2("read: reached end of file")
1404 return false
1406 return position
1409 ----------------------------------------
1410 -- Wireshark/tshark calls seek_read() for each frame/record in the file, at random times
1411 -- It passes in (1) File, (2) CaptureInfo, (3) FrameInfo, and (4) the offset position number
1412 -- It expects in return true for successful parsing, or nil/false if there's an error.
1413 local function seek_read(file, capture, frame, offset)
1414 dprint2("seek_read called")
1415 file:seek("set",offset)
1416 if not read_common("seek_read", file, capture, frame, offset, true) then
1417 dprint("seek_read failed to call read_common")
1418 return false
1420 return true
1423 ----------------------------------------
1424 -- Wireshark/tshark calls read_close() when it's closing the file completely
1425 -- It passes in (1) a File and (2) CaptureInfo object to this function
1426 -- this is a good opportunity to clean up any state you may have created during
1427 -- file reading.
1428 -- In our case there *is* state to reset, but we only saved it in
1429 -- the capture.private_table, so Wireshark will clean it up for us.
1430 local function read_close(file, capture)
1431 dprint2("read_close called")
1432 return true
1435 ----------------------------------------
1436 -- An often unused function, Wireshark calls this when the sequential walk-through is over
1437 -- It passes in (1) a File and (2) CaptureInfo object to this function
1438 -- (i.e., no more calls to read(), only to seek_read()).
1439 -- In our case there *is* some state to reset, but we only saved it in
1440 -- the capture.private_table, so Wireshark will clean it up for us.
1441 local function seq_read_close(file, capture)
1442 dprint2("seq_read_close called")
1443 return true
1446 -- set above functions to the FileHandler
1447 fh.read_open = read_open
1448 fh.read = read
1449 fh.seek_read = seek_read
1450 fh.read_close = read_close
1451 fh.seq_read_close = seq_read_close
1452 fh.extensions = "log" -- this is just a hint
1454 -- and finally, register the FileHandler!
1455 register_filehandler(fh)