1----------------------------------------
2-- script-name: dns_dissector.lua
3--
4-- author: Hadriel Kaplan <hadrielk at yahoo dot com>
5-- Copyright (c) 2014, Hadriel Kaplan
6-- This code is in the Public Domain, or the BSD (3 clause) license if Public Domain does not apply
7-- in your country.
8--
9-- Version: 2.1
10--
11-- Changes since 2.0:
12--   * fixed a bug with default settings
13--   * added ability for command-line to overide defaults
14--
15-- Changes since 1.0:
16--   * made it use the new ProtoExpert class model for expert info
17--   * add a protocol column with the proto name
18--   * added heuristic dissector support
19--   * added preferences settings
20--   * removed byteArray2String(), and uses the new ByteArray:raw() method instead
21--
22-- BACKGROUND:
23-- This is an example Lua script for a protocol dissector. The purpose of this script is two-fold:
24--   * To provide a reference tutorial for others writing Wireshark dissectors in Lua
25--   * To test various functions being called in various ways, so this script can be used in the test-suites
26-- I've tried to meet both of those goals, but it wasn't easy. No doubt some folks will wonder why some
27-- functions are called some way, or differently than previous invocations of the same function. I'm trying to
28-- to show both that it can be done numerous ways, but also I'm trying to test those numerous ways, and my more
29-- immediate need is for test coverage rather than tutorial guide. (the Lua API is sorely lacking in test scripts)
30--
31-- OVERVIEW:
32-- This script creates an elementary dissector for DNS. It's neither comprehensive nor error-free with regards
33-- to the DNS protocol. That's OK. The goal isn't to fully dissect DNS properly - Wireshark already has a good
34-- DNS dissector built-in. We don't need another one. We also have other example Lua scripts, but I don't think
35-- they do a good job of explaining things, and the nice thing about this one is getting capture files to
36-- run it against is trivial. (plus I uploaded one)
37--
38-- HOW TO RUN THIS SCRIPT:
39-- Wireshark and Tshark support multiple ways of loading Lua scripts: through a dofile() call in init.lua,
40-- through the file being in either the global or personal plugins directories, or via the command line.
41-- See the Wireshark User's Guide chapter on Lua (https://www.wireshark.org/docs/wsdg_html_chunked/wsluarm_modules.html).
42-- Once the script is loaded, it creates a new protocol named "MyDNS" (or "MYDNS" in some places).  If you have
43-- a capture file with DNS packets in it, simply select one in the Packet List pane, right-click on it, and
44-- select "Decode As ...", and then in the dialog box that shows up scroll down the list of protocols to one
45-- called "MYDNS", select that and click the "ok" or "apply" button.  Voila`, you're now decoding DNS packets
46-- using the simplistic dissector in this script.  Another way is to download the capture file made for
47-- this script, and open that - since the DNS packets in it use UDP port 65333 (instead of the default 53),
48-- and since the MyDNS protocol in this script has been set to automatically decode UDP port 65333, it will
49-- automagically do it without doing "Decode As ...".
50--
51----------------------------------------
52-- do not modify this table
53local debug_level = {
54    DISABLED = 0,
55    LEVEL_1  = 1,
56    LEVEL_2  = 2
57}
58
59-- set this DEBUG to debug_level.LEVEL_1 to enable printing debug_level info
60-- set it to debug_level.LEVEL_2 to enable really verbose printing
61-- note: this will be overridden by user's preference settings
62local DEBUG = debug_level.LEVEL_1
63
64local default_settings =
65{
66    debug_level  = DEBUG,
67    port         = 65333,
68    heur_enabled = true,
69    heur_regmode = 1,
70}
71
72-- for testing purposes, we want to be able to pass in changes to the defaults
73-- from the command line; because you can't set lua preferences from the command
74-- line using the '-o' switch (the preferences don't exist until this script is
75-- loaded, so the command line thinks they're invalid preferences being set)
76-- so we pass them in as command arguments insetad, and handle it here:
77local args={...} -- get passed-in args
78if args and #args > 0 then
79    for _, arg in ipairs(args) do
80        local name, value = arg:match("(.+)=(.+)")
81        if name and value then
82            if tonumber(value) then
83                value = tonumber(value)
84            elseif value == "true" or value == "TRUE" then
85                value = true
86            elseif value == "false" or value == "FALSE" then
87                value = false
88            elseif value == "DISABLED" then
89                value = debug_level.DISABLED
90            elseif value == "LEVEL_1" then
91                value = debug_level.LEVEL_1
92            elseif value == "LEVEL_2" then
93                value = debug_level.LEVEL_2
94            else
95                error("invalid commandline argument value")
96            end
97        else
98            error("invalid commandline argument syntax")
99        end
100
101        default_settings[name] = value
102    end
103end
104
105local dprint = function() end
106local dprint2 = function() end
107local function reset_debug_level()
108    if default_settings.debug_level > debug_level.DISABLED then
109        dprint = function(...)
110            print(table.concat({"Lua:", ...}," "))
111        end
112
113        if default_settings.debug_level > debug_level.LEVEL_1 then
114            dprint2 = dprint
115        end
116    end
117end
118-- call it now
119reset_debug_level()
120
121dprint2("Wireshark version = ", get_version())
122dprint2("Lua version = ", _VERSION)
123
124----------------------------------------
125-- Unfortunately, the older Wireshark/Tshark versions have bugs, and part of the point
126-- of this script is to test those bugs are now fixed.  So we need to check the version
127-- end error out if it's too old.
128local major, minor, micro = get_version():match("(%d+)%.(%d+)%.(%d+)")
129if major and tonumber(major) <= 1 and ((tonumber(minor) <= 10) or (tonumber(minor) == 11 and tonumber(micro) < 3)) then
130        error(  "Sorry, but your Wireshark/Tshark version ("..get_version()..") is too old for this script!\n"..
131                "This script needs Wireshark/Tshark version 1.11.3 or higher.\n" )
132end
133
134-- more sanity checking
135-- verify we have the ProtoExpert class in wireshark, as that's the newest thing this file uses
136assert(ProtoExpert.new, "Wireshark does not have the ProtoExpert class, so it's too old - get the latest 1.11.3 or higher")
137
138----------------------------------------
139
140
141----------------------------------------
142-- creates a Proto object, but doesn't register it yet
143local dns = Proto("mydns","MyDNS Protocol")
144
145----------------------------------------
146-- multiple ways to do the same thing: create a protocol field (but not register it yet)
147-- the abbreviation should always have "<myproto>." before the specific abbreviation, to avoid collisions
148local pf_trasaction_id      = ProtoField.new   ("Transaction ID", "mydns.trans_id", ftypes.UINT16)
149local pf_flags              = ProtoField.new   ("Flags", "mydns.flags", ftypes.UINT16, nil, base.HEX)
150local pf_num_questions      = ProtoField.uint16("mydns.num_questions", "Number of Questions")
151local pf_num_answers        = ProtoField.uint16("mydns.num_answers", "Number of Answer RRs")
152local pf_num_authority_rr   = ProtoField.uint16("mydns.num_authority_rr", "Number of Authority RRs")
153local pf_num_additional_rr  = ProtoField.uint16("mydns.num_additional_rr", "Number of Additional RRs")
154
155-- within the flags field, we want to parse/show the bits separately
156-- note the "base" argument becomes the size of the bitmask'ed field when ftypes.BOOLEAN is used
157-- the "mask" argument is which bits we want to use for this field (e.g., base=16 and mask=0x8000 means we want the top bit of a 16-bit field)
158-- again the following shows different ways of doing the same thing basically
159local pf_flag_response              = ProtoField.new   ("Response", "mydns.flags.response", ftypes.BOOLEAN, {"this is a response","this is a query"}, 16, 0x8000, "is the message a response?")
160local pf_flag_opcode                = ProtoField.new   ("Opcode", "mydns.flags.opcode", ftypes.UINT16, nil, base.DEC, 0x7800, "operation code")
161local pf_flag_authoritative         = ProtoField.new   ("Authoritative", "mydns.flags.authoritative", ftypes.BOOLEAN, nil, 16, 0x0400, "is the response authoritative?")
162local pf_flag_truncated             = ProtoField.bool  ("mydns.flags.truncated", "Truncated", 16, nil, 0x0200, "is the message truncated?")
163local pf_flag_recursion_desired     = ProtoField.bool  ("mydns.flags.recursion_desired", "Recursion desired", 16, {"yes","no"}, 0x0100, "do the query recursivley?")
164local pf_flag_recursion_available   = ProtoField.bool  ("mydns.flags.recursion_available", "Recursion available", 16, nil, 0x0080, "does the server support recursion?")
165local pf_flag_z                     = ProtoField.uint16("mydns.flags.z", "World War Z - Reserved for future use", base.HEX, nil, 0x0040, "when is it the future?")
166local pf_flag_authenticated         = ProtoField.bool  ("mydns.flags.authenticated", "Authenticated", 16, {"yes","no"}, 0x0020, "did the server DNSSEC authenticate?")
167local pf_flag_checking_disabled     = ProtoField.bool  ("mydns.flags.checking_disabled", "Checking disabled", 16, nil, 0x0010)
168
169-- no, these aren't all the DNS response codes - this is just an example
170local rcodes = {
171        [0] = "No Error",
172        [1] = "Format Error",
173        [2] = "Server Failure",
174        [3] = "Non-Existent Domain",
175        [9] = "Server Not Authoritative for zone"
176}
177-- the above rcodes table is used in this next ProtoField
178local pf_flag_rcode         = ProtoField.uint16("mydns.flags.rcode", "Response code", base.DEC, rcodes, 0x000F)
179local pf_query              = ProtoField.new("Query", "mydns.query", ftypes.BYTES)
180local pf_query_name         = ProtoField.new("Name", "mydns.query.name", ftypes.STRING)
181local pf_query_name_len     = ProtoField.new("Name Length", "mydns.query.name.len", ftypes.UINT8)
182local pf_query_label_count  = ProtoField.new("Label Count", "mydns.query.label.count", ftypes.UINT8)
183local rrtypes = { [1] = "A (IPv4 host address)", [2] = "NS (authoritative name server)", [28] = "AAAA (for geeks only)" }
184local pf_query_type         = ProtoField.uint16("mydns.query.type", "Type", base.DEC, rrtypes)
185-- again, not all class types are listed here
186local classes = {
187        [0] = "Reserved",
188        [1] = "IN (Internet)",
189        [2] = "The 1%",
190        [5] = "First class",
191        [6] = "Business class",
192        [65535] = "Cattle class"
193}
194local pf_query_class        = ProtoField.uint16("mydns.query.class", "Class", base.DEC, classes, nil, "keep it classy folks")
195
196----------------------------------------
197-- this actually registers the ProtoFields above, into our new Protocol
198-- in a real script I wouldn't do it this way; I'd build a table of fields programmatically
199-- and then set dns.fields to it, so as to avoid forgetting a field
200dns.fields = { pf_trasaction_id, pf_flags,
201    pf_num_questions, pf_num_answers, pf_num_authority_rr, pf_num_additional_rr,
202    pf_flag_response, pf_flag_opcode, pf_flag_authoritative,
203    pf_flag_truncated, pf_flag_recursion_desired, pf_flag_recursion_available,
204    pf_flag_z, pf_flag_authenticated, pf_flag_checking_disabled, pf_flag_rcode,
205    pf_query, pf_query_name, pf_query_name_len, pf_query_label_count, pf_query_type, pf_query_class }
206
207----------------------------------------
208-- create some expert info fields (this is new functionality in 1.11.3)
209-- Expert info fields are very similar to proto fields: they're tied to our protocol,
210-- they're created in a similar way, and registered by setting a 'experts' field to
211-- a table of them just as proto fields were put into the 'dns.fields' above
212-- The old way of creating expert info was to just add it to the tree, but that
213-- didn't let the expert info be filterable in wireshark, whereas this way does
214local ef_query     = ProtoExpert.new("mydns.query.expert", "DNS query message",
215                                     expert.group.REQUEST_CODE, expert.severity.CHAT)
216local ef_response  = ProtoExpert.new("mydns.response.expert", "DNS response message",
217                                     expert.group.RESPONSE_CODE, expert.severity.CHAT)
218local ef_ultimate  = ProtoExpert.new("mydns.response.ultimate.expert", "DNS answer to life, the universe, and everything",
219                                     expert.group.COMMENTS_GROUP, expert.severity.NOTE)
220-- some error expert info's
221local ef_too_short = ProtoExpert.new("mydns.too_short.expert", "DNS message too short",
222                                     expert.group.MALFORMED, expert.severity.ERROR)
223local ef_bad_query = ProtoExpert.new("mydns.query.missing.expert", "DNS query missing or malformed",
224                                     expert.group.MALFORMED, expert.severity.WARN)
225
226-- register them
227dns.experts = { ef_query, ef_too_short, ef_bad_query, ef_response, ef_ultimate }
228
229----------------------------------------
230-- we don't just want to display our protocol's fields, we want to access the value of some of them too!
231-- There are several ways to do that.  One is to just parse the buffer contents in Lua code to find
232-- the values.  But since ProtoFields actually do the parsing for us, and can be retrieved using Field
233-- objects, it's kinda cool to do it that way. So let's create some Fields to extract the values.
234-- The following creates the Field objects, but they're not 'registered' until after this script is loaded.
235-- Also, these lines can't be before the 'dns.fields = ...' line above, because the Field.new() here is
236-- referencing fields we're creating, and they're not "created" until that line above.
237-- Furthermore, you cannot put these 'Field.new()' lines inside the dissector function.
238-- Before Wireshark version 1.11, you couldn't even do this concept (of using fields you just created).
239local questions_field       = Field.new("mydns.num_questions")
240local query_type_field      = Field.new("mydns.query.type")
241local query_class_field     = Field.new("mydns.query.class")
242local response_field        = Field.new("mydns.flags.response")
243
244-- here's a little helper function to access the response_field value later.
245-- Like any Field retrieval, you can't retrieve a field's value until its value has been
246-- set, which won't happen until we actually use our ProtoFields in TreeItem:add() calls.
247-- So this isResponse() function can't be used until after the pf_flag_response ProtoField
248-- has been used inside the dissector.
249-- Note that calling the Field object returns a FieldInfo object, and calling that
250-- returns the value of the field - in this case a boolean true/false, since we set the
251-- "mydns.flags.response" ProtoField to ftype.BOOLEAN way earlier when we created the
252-- pf_flag_response ProtoField.  Clear as mud?
253--
254-- A shorter version of this function would be:
255-- local function isResponse() return response_field()() end
256-- but I though the below is easier to understand.
257local function isResponse()
258    local response_fieldinfo = response_field()
259    return response_fieldinfo()
260end
261
262--------------------------------------------------------------------------------
263-- preferences handling stuff
264--------------------------------------------------------------------------------
265
266-- a "enum" table for our enum pref, as required by Pref.enum()
267-- having the "index" number makes ZERO sense, and is completely illogical
268-- but it's what the code has expected it to be for a long time. Ugh.
269local debug_pref_enum = {
270    { 1,  "Disabled", debug_level.DISABLED },
271    { 2,  "Level 1",  debug_level.LEVEL_1  },
272    { 3,  "Level 2",  debug_level.LEVEL_2  },
273}
274
275dns.prefs.debug = Pref.enum("Debug", default_settings.debug_level,
276                            "The debug printing level", debug_pref_enum)
277
278dns.prefs.port  = Pref.uint("Port number", default_settings.port,
279                            "The UDP port number for MyDNS")
280
281dns.prefs.heur  = Pref.bool("Heuristic enabled", default_settings.heur_enabled,
282                            "Whether heuristic dissection is enabled or not")
283
284----------------------------------------
285-- a function for handling prefs being changed
286function dns.prefs_changed()
287    dprint2("prefs_changed called")
288
289    default_settings.debug_level  = dns.prefs.debug
290    reset_debug_level()
291
292    default_settings.heur_enabled = dns.prefs.heur
293
294    if default_settings.port ~= dns.prefs.port then
295        -- remove old one, if not 0
296        if default_settings.port ~= 0 then
297            dprint2("removing MyDNS from port",default_settings.port)
298            DissectorTable.get("udp.port"):remove(default_settings.port, dns)
299        end
300        -- set our new default
301        default_settings.port = dns.prefs.port
302        -- add new one, if not 0
303        if default_settings.port ~= 0 then
304            dprint2("adding MyDNS to port",default_settings.port)
305            DissectorTable.get("udp.port"):add(default_settings.port, dns)
306        end
307    end
308
309end
310
311dprint2("MyDNS Prefs registered")
312
313
314----------------------------------------
315---- some constants for later use ----
316-- the DNS header size
317local DNS_HDR_LEN = 12
318
319-- the smallest possible DNS query field size
320-- has to be at least a label length octet, label character, label null terminator, 2-bytes type and 2-bytes class
321local MIN_QUERY_LEN = 7
322
323----------------------------------------
324-- some forward "declarations" of helper functions we use in the dissector
325-- I don't usually use this trick, but it'll help reading/grok'ing this script I think
326-- if we don't focus on them.
327local getQueryName
328
329
330----------------------------------------
331-- The following creates the callback function for the dissector.
332-- It's the same as doing "dns.dissector = function (tvbuf,pkt,root)"
333-- The 'tvbuf' is a Tvb object, 'pktinfo' is a Pinfo object, and 'root' is a TreeItem object.
334-- Whenever Wireshark dissects a packet that our Proto is hooked into, it will call
335-- this function and pass it these arguments for the packet it's dissecting.
336function dns.dissector(tvbuf,pktinfo,root)
337    dprint2("dns.dissector called")
338
339    -- set the protocol column to show our protocol name
340    pktinfo.cols.protocol:set("MYDNS")
341
342    -- We want to check that the packet size is rational during dissection, so let's get the length of the
343    -- packet buffer (Tvb).
344    -- Because DNS has no additional payload data other than itself, and it rides on UDP without padding,
345    -- we can use tvb:len() or tvb:reported_len() here; but I prefer tvb:reported_length_remaining() as it's safer.
346    local pktlen = tvbuf:reported_length_remaining()
347
348    -- We start by adding our protocol to the dissection display tree.
349    -- A call to tree:add() returns the child created, so we can add more "under" it using that return value.
350    -- The second argument is how much of the buffer/packet this added tree item covers/represents - in this
351    -- case (DNS protocol) that's the remainder of the packet.
352    local tree = root:add(dns, tvbuf:range(0,pktlen))
353
354    -- now let's check it's not too short
355    if pktlen < DNS_HDR_LEN then
356        -- since we're going to add this protocol to a specific UDP port, we're going to
357        -- assume packets in this port are our protocol, so the packet being too short is an error
358        -- the old way: tree:add_expert_info(PI_MALFORMED, PI_ERROR, "packet too short")
359        -- the correct way now:
360        tree:add_proto_expert_info(ef_too_short)
361        dprint("packet length",pktlen,"too short")
362        return
363    end
364
365    -- Now let's add our transaction id under our dns protocol tree we just created.
366    -- The transaction id starts at offset 0, for 2 bytes length.
367    tree:add(pf_trasaction_id, tvbuf:range(0,2))
368
369    -- We'd like to put the transaction id number in the GUI row for this packet, in its
370    -- INFO column/cell.  First we need the transaction id value, though.  Since we just
371    -- dissected it with the previous code line, we could now get it using a Field's
372    -- FieldInfo extractor, but instead we'll get it directly from the TvbRange just
373    -- to show how to do that.  We'll use Field/FieldInfo extractors later on...
374    local transid = tvbuf:range(0,2):uint()
375    pktinfo.cols.info:set("(".. transid ..")")
376
377    -- now let's add the flags, which are all in the packet bytes at offset 2 of length 2
378    -- instead of calling this again and again, let's just use a variable
379    local flagrange = tvbuf:range(2,2)
380
381    -- for our flags field, we want a sub-tree
382    local flag_tree = tree:add(pf_flags, flagrange)
383        -- I'm indenting this for clarity, because it's adding to the flag's child-tree
384
385        -- let's add the type of message (query vs. response)
386        local query_flag_tree = flag_tree:add(pf_flag_response, flagrange)
387
388        -- let's also add an expert info about it
389        if isResponse() then
390            query_flag_tree:add_proto_expert_info(ef_response, "It's a response!")
391            if transid == 42 then
392                tree:add_tvb_expert_info(ef_ultimate, tvbuf:range(0,2))
393            end
394        else
395            query_flag_tree:add_proto_expert_info(ef_query)
396        end
397
398        -- we now know if it's a response or query, so let's put that in the
399        -- GUI packet row, in the INFO column cell
400        -- this line of code uses a Lua trick for doing something similar to
401        -- the C/C++ 'test ? true : false' shorthand
402        pktinfo.cols.info:prepend(isResponse() and "Response " or "Query ")
403
404        flag_tree:add(pf_flag_opcode, flagrange)
405
406        if isResponse() then
407            flag_tree:add(pf_flag_authoritative, flagrange)
408        end
409
410        flag_tree:add(pf_flag_truncated, flagrange)
411
412        if isResponse() then
413            flag_tree:add(pf_flag_recursion_available, flagrange)
414        else
415            flag_tree:add(pf_flag_recursion_desired, flagrange)
416        end
417
418        flag_tree:add(pf_flag_z, flagrange)
419
420        if isResponse() then
421            flag_tree:add(pf_flag_authenticated, flagrange)
422            flag_tree:add(pf_flag_rcode, flagrange)
423        end
424
425        flag_tree:add(pf_flag_checking_disabled, flagrange)
426
427    -- now add more to the main mydns tree
428    tree:add(pf_num_questions, tvbuf:range(4,2))
429    tree:add(pf_num_answers, tvbuf:range(6,2))
430    -- another way to get a TvbRange is just to call the Tvb like this
431    tree:add(pf_num_authority_rr, tvbuf(8,2))
432    -- or if we're crazy, we can create a sub-TvbRange, from a sub-TvbRange of the TvbRange
433    tree:add(pf_num_additional_rr, tvbuf:range(10,2):range()())
434
435    local num_queries = questions_field()()
436    local pos = DNS_HDR_LEN
437
438    if num_queries > 0 then
439        -- let's create a sub-tree, using a plain text description (not a field from the packet)
440        local queries_tree = tree:add("Queries")
441
442        local pktlen_remaining = pktlen - pos
443
444        while num_queries > 0 and pktlen_remaining > 0 do
445            if pktlen_remaining < MIN_QUERY_LEN then
446                -- old way: queries_tree:add_expert_info(PI_MALFORMED, PI_ERROR, "query field missing or too short")
447                queries_tree:add_proto_expert_info(ef_bad_query)
448                return
449            end
450
451            -- we don't know how long this query field in total is, so we have to parse it first before
452            -- adding it to the tree, because we want to identify the correct bytes it covers
453            local label_count, name, name_len = getQueryName(tvbuf:range(pos,pktlen_remaining))
454            if not label_count then
455                q_tree:add_expert_info(PI_MALFORMED, PI_ERROR, name)
456                return
457            end
458
459            -- now add the first query to the 'Queries' child tree we just created
460            -- we're going to change the string generated by this later, after we figure out the subsequent fields.
461            -- the whole query field is the query name field length we just got, plus the 20 byte type and 2-byte class
462            local q_tree = queries_tree:add(pf_query, tvbuf:range(pos, name_len + 4))
463
464            q_tree:add(pf_query_name, tvbuf:range(pos, name_len), name)
465            pos = pos + name_len
466
467            pktinfo.cols.info:append(" "..name)
468
469            -- the following tree items are generated by us, not encoded in the packet per se, so mark them as such
470            q_tree:add(pf_query_name_len, name_len):set_generated()
471            q_tree:add(pf_query_label_count, label_count):set_generated()
472
473            q_tree:add(pf_query_type, tvbuf:range(pos, 2))
474            q_tree:add(pf_query_class, tvbuf:range(pos + 2, 2))
475            pos = pos + 4
476
477            -- now change the query text
478            q_tree:set_text(name..": type "..query_type_field().display ..", class "..query_class_field().display)
479
480            pktlen_remaining = pktlen_remaining - (name_len + 4)
481            num_queries = num_queries - 1
482        end  -- end of while loop
483
484        if num_queries > 0 then
485            -- we didn't process them all
486            queries_tree:add_expert_info(PI_MALFORMED, PI_ERROR, num_queries .. " query field(s) missing")
487            return
488        end
489    end
490
491    dprint2("dns.dissector returning",pos)
492
493    -- tell wireshark how much of tvbuff we dissected
494    return pos
495end
496
497----------------------------------------
498-- we want to have our protocol dissection invoked for a specific UDP port,
499-- so get the udp dissector table and add our protocol to it
500DissectorTable.get("udp.port"):add(default_settings.port, dns)
501
502----------------------------------------
503-- we also want to add the heuristic dissector, for any UDP protocol
504-- first we need a heuristic dissection function
505-- this is that function - when wireshark invokes this, it will pass in the same
506-- things it passes in to the "dissector" function, but we only want to actually
507-- dissect it if it's for us, and we need to return true if it's for us, or else false
508-- figuring out if it's for us or not is not easy
509-- we need to try as hard as possible, or else we'll think it's for us when it's
510-- not and block other heuristic dissectors from getting their chance
511--
512-- in practice, you'd never set a dissector like this to be heuristic, because there
513-- just isn't enough information to safely detect if it's DNS or not
514-- but I'm doing it to show how it would be done
515--
516-- Note: this heuristic stuff is new in 1.11.3
517local function heur_dissect_dns(tvbuf,pktinfo,root)
518    dprint2("heur_dissect_dns called")
519
520    -- if our preferences tell us not to do this, return false
521    if not default_settings.heur_enabled then
522        return false
523    end
524
525    if tvbuf:len() < DNS_HDR_LEN then
526        dprint("heur_dissect_dns: tvb shorter than DNS_HDR_LEN of:",DNS_HDR_LEN)
527        return false
528    end
529
530    local tvbr = tvbuf:range(0,DNS_HDR_LEN)
531
532    -- the first 2 bytes are transaction id, which can be anything so no point in checking those
533    -- the next 2 bytes contain flags, a couple of which have some values we can check against
534
535    -- the opcode has to be 0, 1, 2, 4 or 5
536    -- the opcode field starts at bit offset 17 (in C-indexing), for 4 bits in length
537    local check = tvbr:bitfield(17,4)
538    if check == 3 or check > 5 then
539        dprint("heur_dissect_dns: invalid opcode:",check)
540        return false
541    end
542
543    -- the rcode has to be 0-10, 16-22 (we're ignoring private use rcodes here)
544    -- the rcode field starts at bit offset 28 (in C-indexing), for 4 bits in length
545    check = tvbr:bitfield(28,4)
546    if check > 22 or (check > 10 and check < 16) then
547        dprint("heur_dissect_dns: invalid rcode:",check)
548        return false
549    end
550
551    dprint2("heur_dissect_dns checking questions/answers")
552
553    -- now let's verify the number of questions/answers are reasonable
554    check = tvbr:range(4,2):uint()  -- num questions
555    if check > 100 then return false end
556    check = tvbr:range(6,2):uint()  -- num answers
557    if check > 100 then return false end
558    check = tvbr:range(8,2):uint()  -- num authority
559    if check > 100 then return false end
560    check = tvbr:range(10,2):uint()  -- num additional
561    if check > 100 then return false end
562
563    dprint2("heur_dissect_dns: everything looks good calling the real dissector")
564
565    -- don't do this line in your script - I'm just doing it so our test-suite can
566    -- verify this script
567    root:add("Heuristic dissector used"):set_generated()
568
569    -- ok, looks like it's ours, so go dissect it
570    -- note: calling the dissector directly like this is new in 1.11.3
571    -- also note that calling a Dissector object, as this does, means we don't
572    -- get back the return value of the dissector function we created previously
573    -- so it might be better to just call the function directly instead of doing
574    -- this, but this script is used for testing and this tests the call() function
575    dns.dissector(tvbuf,pktinfo,root)
576
577    -- since this is over a transport protocol, such as UDP, we can set the
578    -- conversation to make it sticky for our dissector, so that all future
579    -- packets to/from the same address:port pair will just call our dissector
580    -- function directly instead of this heuristic function
581    -- this is a new attribute of pinfo in 1.11.3
582    pktinfo.conversation = dns
583
584    return true
585end
586
587-- now register that heuristic dissector into the udp heuristic list
588if default_settings.heur_regmode == 1 then
589    -- this is the "normal" way to register a heuristic: using a lua function
590    dns:register_heuristic("udp",heur_dissect_dns)
591elseif default_settings.heur_regmode == 2 then
592    -- this is to test the fix for bug 10695:
593    dns:register_heuristic("udp",dns.dissector)
594elseif default_settings.heur_regmode == 3 then
595    -- and this too is to test the fix for bug 10695:
596    dns:register_heuristic("udp", function (...) return dns.dissector(...); end )
597end
598
599-- We're done!
600-- our protocol (Proto) gets automatically registered after this script finishes loading
601----------------------------------------
602
603----------------------------------------
604-- DNS query names are not just null-terminated strings; they're actually a sequence of
605-- 'labels', with a length octet before each one.  So "foobar.com" is actually the
606-- string "\06foobar\03com\00".  We could create a ProtoField for label_length and label_name
607-- or whatever, but since this is an example script I'll show how to do it in raw code.
608-- This function is given the TvbRange object from the dissector() function, and needs to
609-- parse it.
610-- On success, it returns three things: the number of labels, the name string, and how
611-- many bytes it covered of the buffer (which is always 2 more than the name length in this case).
612-- On failure, it returns nil and the error message.
613getQueryName = function (tvbr)
614    local label_count = 0
615    local name = ""
616
617    local len_remaining = tvbr:len()
618    if len_remaining < 2 then
619        -- it's too short
620        return nil, "invalid name"
621    end
622
623    local barray = tvbr:bytes() -- gets a ByteArray of the TvbRange
624    local pos = 0 -- unlike Lua, ByteArray uses 0-based indexing
625
626    -- get the first octet/label-length
627    local label_len = barray:get_index(pos)
628    if label_len == 0 then
629        return nil, "invalid initial label length of 0"
630    end
631
632    while label_len > 0 do
633        if label_len >= len_remaining then
634            return nil, "invalid label length of "..label_len
635        end
636        pos = pos + 1  -- move past label length octet
637        -- append the label and a dot to name string
638        -- note: this uses the new method of ByteArray:raw(), added in 1.11.3
639        name = name .. barray:raw(pos, label_len) .. "."
640        len_remaining = len_remaining - (label_len + 1) -- subtract label and its length octet
641        label_count = label_count + 1
642        pos = pos + label_len -- move past label
643        label_len = barray:get_index(pos)
644    end
645
646    -- we appended an extra dot, so get rid of it
647    name = name:sub(1, -2)
648
649    return label_count, name, name:len() + 2
650end
651