14.3. Getting Started

A small postdissector is enough to exercise everything the debugger does. This one measures the response time of any protocol over UDP, what dns.time shows for DNS. On each reply it adds the time since the client’s latest request in the same UDP stream, as udprtt.time, and a link to that request, as udprtt.request. Download udprtt.lua or save the following as udprtt.lua:

-- Time from a UDP request to its reply in the same stream,
-- like dns.time for DNS, for any protocol over UDP

local udprtt    = Proto("udprtt", "UDP Response Time")
local f_rtt     = ProtoField.relative_time("udprtt.time", "Time since request")
local f_request = ProtoField.framenum("udprtt.request", "Request in frame",
                                      base.NONE, frametype.REQUEST)
udprtt.fields   = { f_rtt, f_request }
local f_stream  = Field.new("udp.stream")
local f_time    = Field.new("frame.time")
local timeout   = NSTime(2, 0)  -- later replies are not matched
local state     = {}

function state:reset()
  self.stream = {}  -- stream index -> client address:port and latest request
  self.reply  = {}  -- frame number -> request answered by this frame
end

function udprtt.init()
  state:reset()
end

function udprtt.dissector(tvb, pinfo, tree)
  local stream = f_stream()
  if not stream then return end
  local id  = stream.value
  local now = f_time().value

  if not pinfo.visited then
    local src = tostring(pinfo.src) .. ":" .. pinfo.src_port
    local s = state.stream[id] or { client = src }
    state.stream[id] = s
    if src == s.client then
      s.request = { frame = pinfo.number, time = now }
    elseif s.request and now - s.request.time <= timeout then
      state.reply[pinfo.number] = s.request
    end
  end

  local request = state.reply[pinfo.number]
  if request then
    local subtree = tree:add(udprtt)
    subtree:add(f_request, request.frame)
    subtree:add(f_rtt, now - request.time)
  end
end

register_postdissector(udprtt)

Every example later in this chapter resolves against names this script defines, so you can paste any of them into the Watch panel without setting up extra state. What’s in scope while udprtt.dissector is paused:

The first sender in a stream is its client. state.stream holds, for each stream index, the client’s address and port and its latest request, a table with the request’s frame number and time. state.reply maps the frame number of each reply to the request it answers; that is the request’s own table, not a copy, which is why every request gets a new table. The state is updated only the first time a frame is dissected, while pinfo.visited is false; later passes, in any order, read it back from state.reply. udprtt.init() clears both tables when a capture file is opened or dissected again.

Times are NSTime values with nanosecond precision, not numbers: f_time().value is the time of the frame, the difference of two NSTime values is exact, and the ProtoField.relative_time field shows it like udp.time_delta. pinfo.abs_ts is a Lua number of seconds since the epoch, too coarse for differences of a few microseconds. NSTime(2, 0) builds the timeout of two seconds, so that it compares with an NSTime difference.

Field.new(…​) is called at file scope on purpose: extractors can only be constructed at script load, before any dissector or tap callback runs. Inside the dissector callback the script references f_stream and f_time (and so do the watch examples below), but never calls Field.new again.

Drop it into your personal plugins directory (the exact path on your install is also shown under Help → About Wireshark → Folders → Personal Lua Plugins). The canonical locations are:

Then:

  1. Pick Analyze → Reload Lua Plugins (Ctrl+Shift+L) so Wireshark picks up the new file. The same shortcut also works from the Reload Lua Plugins toolbar action inside the Lua Debugger dialog once it’s open.
  2. Open Tools → Lua Debugger and tick the Enabled checkbox at the start of the toolbar.
  3. Expand the Files section and double-click udprtt.lua to open it in the editor.
  4. Click in the gutter on a line inside udprtt.dissector to toggle a breakpoint (a red circle appears).
  5. Load a capture file with UDP requests and replies, such as DNS. Dissection starts, the breakpoint hits, and the debugger pauses — the paused line gets a yellow stripe (and an orange right-pointing triangle in the gutter), the Enabled checkbox circle turns orange, the window title changes to Lua Debugger — Paused, and the main window grays out behind a "Lua debugger paused" banner.
  6. Inspect the paused state:

    • Variables shows Locals (tvb, pinfo, tree, stream, id, now), Upvalues, and Globals for the current frame.
    • Stack Trace shows the postdissector call on top of whatever frame drove the dissection.
    • Try Add Watch on pinfo.src (via the Variables context menu or the + button in the Watch section header, Ctrl+Shift+W).
  7. Use Step Over (F10), Step Into (F11), or Continue (F5) to advance. The Watch tree updates on every pause; values that changed since the previous pause are drawn in a bold accent color and briefly flash.