How to write a Wireshark tap plugin in Lua

Using packet captures for Wi-Fi troubleshooting is extremely useful and a major reason why native packet capture is included in the top managed wireless network solutions. One of the first things people look for when troubleshooting a crowded network is a list of all of the wireless networks that are transmitting data found in the capture.

CloudShark 3.10 improves the Wireless Networks analysis tool to display a report of all the wireless networks found in a capture file. This update correctly detects the latest Wi-Fi Alliance security modes, including WPA3.

Under the hood, CloudShark uses Wireshark as part of its packet analysis engine.

This level of Wi-Fi information isn’t native to Wireshark, however. To get cool CloudShark features like the Wireless Networks analysis tool to work,  we created the Wi-Fi Networks plugin for TShark. This plugin inspects the wireless packets in a capture file and prints a list of all the wireless networks found and information about each network.

Creating Lua plugins can be very useful to Wireshark and TShark users, and there’s a great community around building them. The Wireshark Developers Guide describes how to write plugins for Wireshark for Lua, but we find examples are useful when writing your own plugins! We will describe how ours works so you can use it while creating your own custom plugins for Wireshark/TShark.

We chose to write our plugin in Lua for its ease of development. This avoids having to recompile Wireshark each time a change is made. The development section of the Wireshark wiki is a great resource to learn more.

Plugin Types

Before diving into the Wi-Fi Networks plugin, let's review the types of Wireshark plugins that can be written in Lua:

Dissectors: Analyze the data inside a packet and create the initial packet dissection tree. This breaks data in the packet into specific fields, such as the IPv4 source address or the TCP destination port

Post-dissectors: Add additional information to a packet after dissectors have been run. For example, verifying that a checksum is valid and adding the result to the packet details pane.

Taps: Collect information from the packet after the dissectors and post-dissectors run.

How the plugin works

Which of the 3 types above is the WiFi Networks plugin? It's a tap!

The WiFi Networks plugin reads information from every Beacon and Probe Response packet in the pcap. It does not add any information to the dissection tree of a packet. The wireless networks found in the capture are printed after all of the packets have been processed.

The plugin can be broken down into three parts:

Register: Tell Wireshark there is a new tap that should execute whenever it processes a packet

Action on packet: What to do when Wireshark processes that packet

Draw: Called when Wireshark has finished processing each packet


The Wireshark Lua wiki page states that Lua scripts get run before any packets are processed by Wireshark. How does the plugin collect information from all the packets in the capture file before processing any packets?

It doesn't! The first step when writing a Tap is registering a new Listener for packets matching a specific filter. In our WiFi networks plugin this is done with the following:

local tap ="wlan")

Once the tap has been registered, then the `packet` function can be defined. This function is known as a callback function and will be called whenever a packet matching the filter defined when registering the plugin is seen by Wireshark.

Packet Action

In the Wi-Fi networks plugin, this callback function will be called when a packet matching the `wlan` filter is encountered:

function tap.packet(tvb, pinfo, tree)

This Lua function will now be called once each time Wireshark encounters a packet matching the `wlan` filter. In the Wi-Fi Networks plugin, each beacon and probe response packet is inspected, and a list of all the wireless networks is built. Before looking at this callback function, first we need to define which fields in the packet we want to inspect.

The plugin will need to determine if the packet is a beacon or probe response from the `wlan.fc.subtype` field to decide whether or not to continue processing the packet. It will also need to collect the SSID being advertised to start building a list of all of the WiFi networks found in the capture file. 

The specific fields that the plugin will get the values from are defined outside of the packet action callback function. For example, to inspect the packet type and SSID the following variables are declared at the start of the file so that the values of these fields can be used:


local wlan_fc_subtype_f ="wlan.fc.subtype")

local ssid_f ="wlan.ssid")

local bssid_f ="wlan.bssid")

local vendor_f ="wlan.bssid_resolved")

 There are many additional fields that the plugin will need to inspect to determine all of the information shown for a WiFi network! These can all be found following the examples above.

Once all the fields that we wish to inspect have been declared, a table is created in Lua. All of the WiFi networks found and information about each one are added to the `networks` table:

networks = {}

There are many additional fields that the plugin inspects to determine the security type and information about the channel and signal strength!

With these fields defined, the type can be checked when each packet is passed to the `tap.packet()` callback function. The plugin ends if it is not a beacon or probe response, without processing any further:

-- only look at management beacon and probe-response frames

if not(wlan_fc_subtype.value == 8 or wlan_fc_subtype.value == 5) then return end

Next, the values of the SSID fields can be assigned to local variables inside the callback function and assigned to the `networks` table using the BSSID as the index:


-- basic information about the network

local bssid, ssid, vendor = bssid_f(), ssid_f(), vendor_f()

-- if no ssid field, ssid=nil, we should skip it.

if not ssid then return end

-- bssid mac address as a string

local b = tostring(bssid)

if string.sub(b,1,6) == "ff:ff:" then return end

if networks[b] == nil then

networks[b] = {packets=0, signal=0, noise=0, snr=0}


if #ssid == 0 then

networks[b].hidden = true

networks[b].ssid = ""


networks[b].hidden = false

networks[b].ssid = ssid.display


The function then inspects all the fields necessary to determine information about each WiFi network found in the capture file.


After all of the packets in the capture file have been processed, the `draw` function in the Lua plugin will be called. This is where the table of all of the networks seen in the capture is printed:

function tap.draw()

    print("BSSID", "SSID", "Security", "Vendor", "Hidden", "Signal", "Noise", "SNR", "Channel")

    for bssid,v in pairs(networks) do

        print(bssid, v.ssid,, v.vendor, v.hidden, v.signal/v.packets, v.noise/v.packets, v.snr/v.packets,



Here is an example of the TShark output when it is run on the wifi-networks.pcapng capture file:


# tshark -q -X lua_script:wifi-networks.lua -r wifi-networks.pcapng | column -s $'\t' -t -o " | "

BSSID         | SSID            | Security    | Vendor        | Hidden | Signal | Noise | SNR | Channel

4e:9d:08:53:d1:12 | ACME Corp       | WPA2-Personal   | 4e:9d:08:53:d1:12 | false  | -49 | -74   | 25  | 11

3e:71:ce:4f:32:68 | Ye Olde Coffee Shop | WPA3-Personal   | 3e:71:ce:4f:32:68 | false  | -41 | -74   | 33  | 11

de:dd:d7:51:ba:95 | The Neighbors   | WPA2/3-Personal | de:dd:d7:51:ba:95 | false  | -41 | -73   | 32  | 11

de:4e:cb:fe:62:d4 | ISEEYOURPACKETS | Open        | de:4e:cb:fe:62:d4 | false  | -39 | -76   | 37  | 11

62:ab:eb:6c:fb:c0 | CloudShark      | WPA2-Personal   | 62:ab:eb:6c:fb:c0 | false  | -34 | -74   | 40  | 11


The output of the WiFi Networks plugin is tab separated and can be piped to the command `column -s $'\t' -t -o " | "` to create a human-readable table.

Additional resources

Here are some additional resources that you can use to learn more about creating your own Lua plugins for Wireshark:

We would love to hear about any of your own custom Wireshark Lua plugins! Let us know on Twitter @cloudshark if you have a plugin you'd like to share!