A Wireshark/tshark dissector for Ubiquiti AirMAX (AC + M) and the companion Mikrotik / RouterBoard vendor Information Elements, written in Lua.
It is the over-the-wire counterpart to pyrmax: the decode logic mirrors that Python package field-for-field. The AC packet format was reverse-engineered from the AirMAX AC firmware binary; the M and RouterBoard layouts come from prior published notes on the older AirMAX-M vendor IE.
AirMAX rides inside 802.11 vendor-specific IEs (tag 221). Wireshark already parses the 802.11 frame and shows the vendor IE as an opaque blob; wiremax takes that blob, decrypts it (AES-128-ECB, key derived from the frame MACs) and renders a parsed, filterable subtree.
Tag: Vendor Specific: Ubiquiti Inc (built-in 802.11 dissector)
AirMAX AC (Vendor Specific IE) ← added by wiremax
Flags: 0x02 ( .... ..1. = Encrypted: True )
Message Type: Beacon (1)
Encrypted Length: 48
[Decrypted payload (AES-128-ECB)]
[AES Key …: 1f162a13… (dst=broadcast)]
Version: 9
Source MAC: 24:5a:4c:44:57:fd
Radio MAC (mac_0c): 24:5a:4c:44:57:fd
Capability Flags: 0x0000003e
Mixed Mode: 0
Radioname: LB1
SSID: labalUBI2
TLV: Radioname (1), len 3
TLV: SSID (2), len 9
TLV: Padding (0)
The decrypted layout depends on Message Type: beacons carry mac_0c / cap_flags /
mixed_mode + name TLVs (above); assoc req/resp carry chainmasks, cap_flags and
version-gated tail fields (field_9c, rssi, fwname, txpower); probe req is
header-only; deauth carries a jiffies nonce and an opaque auth token (and its
source MAC is XOR-masked, which wiremax un-masks).
| Variant | Coverage |
|---|---|
| AirMAX AC | outer header + decrypted shared head (version + src_mac) + per-message-type, version-gated body (beacon, assoc req/resp, probe req, deauth) + name TLVs |
| AirMAX M | outer shell + decrypted 9 documented bytes (version, msg_type, src_mac, enable) + raw rest |
| RouterBoard (Mikrotik) | OUI/type + sub-IEs + device name (cleartext) |
Undocumented byte regions are shown raw (Unknown [n:len]), never invented —
same discipline as pyrmax and the ac/ knowledge base.
A postdissector (it does not replace the built-in 802.11 dissector, it runs after it). For each frame it:
- reads every
wlan.tag.ouipluswlan.sa/wlan.daviaFieldextractors; - matches the AirMAX AC (
00:27:22), M (00:15:6d) or RouterBoard (00:0c:42) OUI — for AC/M it also requires OUI-typeFF FF FF, because00:15:6dis shared with Mikrotik and with another Ubiquiti IE that appears in the same frame; - locates the IE bytes in the frame and, for AC/M, decrypts the payload with
AES-128-ECB, key= HMAC-SHA1(dst_mac, src_mac)[:16], trying the captured 802.11 destination first and falling back to broadcast (identity frames use the broadcast key even when unicast) — the winner is the one whose decrypted payload's embedded source MAC matches the 802.11 source; - builds the subtree, exposing the decrypted bytes as their own data source so field selection highlights them.
The crypto (SHA-1, HMAC-SHA1, AES-128 decrypt) is self-contained pure Lua — Wireshark exposes no crypto API to Lua. Payloads are a few 16-byte blocks, so performance is irrelevant. The key is derived from public MACs, so this is obfuscation, not security.
One-off (no install):
tshark -X lua_script:airmax.lua -r capture.pcap -V
wireshark -X lua_script:airmax.lua capture.pcapPersistent (Wireshark GUI + tshark): put the whole wiremax/ folder in a
Personal Lua Plugins directory. The exact path is shown in Wireshark under
Help ▸ About Wireshark ▸ Folders ▸ Personal Lua Plugins; the defaults are:
| OS | Default Personal Lua Plugins path |
|---|---|
| Linux | ~/.local/lib/wireshark/plugins/ |
| macOS | ~/.local/lib/wireshark/plugins/ (or ~/.config/wireshark/plugins/) |
| Windows | %APPDATA%\Wireshark\plugins\ |
A symlink is ideal while iterating — edits are picked up on reload
(Ctrl+Shift+L) with no copy step:
ln -s "$PWD" ~/.local/lib/wireshark/plugins/wiremaxWireshark recursively loads every .lua under the plugin folder; the submodules
under wsairmax/ are plain modules (harmless if loaded standalone) and
test/selftest.lua no-ops inside Wireshark.
| Filter | Meaning |
|---|---|
airmax.ac.msg_type |
message type: 1 Beacon, 2 Assoc Req, 3 Assoc Resp, 4 Probe Req, 0xC Deauth |
airmax.ac.flags.encrypted |
encrypted bit (0x02) |
airmax.ac.version |
decrypted format version (the gate; TX emits 9) |
airmax.ac.src_mac |
source MAC from the decrypted head (deauth un-masks it) |
airmax.ac.mac_0c |
beacon radio MAC (BSSID) |
airmax.ac.cap_flags + airmax.ac.cap_flags.chanbw/.high_density/.auth_deauth/.compat_11ax |
capability bitfield (§11) |
airmax.ac.radioname, airmax.ac.ssid, airmax.ac.fwname |
AC name / firmware strings |
airmax.ac.jiffies_nonce, airmax.ac.enc_token |
deauth nonce + opaque auth token |
airmax.ac.tlv.tag / .len / .data |
raw TLV walk |
airmax.m.version, airmax.m.msg_type, airmax.m.src_mac, airmax.m.enable |
M documented fields |
airmax.m.unknown_rest |
undocumented M tail |
airmax.rb.device_name |
RouterBoard device name |
airmax.ac.key / airmax.m.key |
derived AES key (generated, for verification) |
Examples:
tshark -r cap.pcap -Y 'airmax.ac.ssid' -T fields -e wlan.sa -e airmax.ac.ssid
tshark -r cap.pcap -Y 'airmax.m.msg_type == 1' -T fields -e airmax.m.src_mac
tshark -r cap.pcap -Y 'airmax.rb.device_name' -T fields -e airmax.rb.device_nameCaptures must be 802.11 monitor mode (radiotap); the management frames carrying the IEs (beacon / probe response / assoc) are unencrypted at the 802.11 layer — only the AirMAX payload inside the IE is AES-wrapped, and wiremax unwraps it.
test/selftest.lua runs the crypto and decoders under the standalone Lua 5.4
interpreter (no Wireshark), checking against ground-truth vectors plus standard
SHA-1 / RFC-2202 HMAC vectors:
lua test/selftest.lua # from this directoryEnd-to-end against a real capture (pyrmax ships sample pcaps under
tests/samples/):
tshark -X lua_script:airmax.lua -r airmax_ac_beacon.pcap -V
tshark -X lua_script:airmax.lua -r airmax_m_probe_response.pcap -Vwiremax/
├── airmax.lua # Wireshark entry: ProtoFields + postdissector + tree
├── wsairmax/ # pure-Lua modules (no Wireshark dependency)
│ ├── util.lua # hex / MAC helpers
│ ├── sha1.lua # SHA-1 + HMAC-SHA1
│ ├── aes.lua # AES-128 ECB decrypt
│ ├── crypto.lua # key derivation + decrypt (mirrors pyrmax/_crypto.py)
│ ├── ac.lua # AirMAX AC decoder (outer ksy: airmax_ac; payload hand-written per 09b)
│ ├── m.lua # AirMAX M decoder (ksy: airmax_m[_payload])
│ └── routerboard.lua # RouterBoard decoder (ksy: routerboard)
├── test/selftest.lua # standalone-Lua test harness
└── README.md
- M
msg_typelabels1 = Beaconper the older AirMAX-M notes, but value1is also observed on probe responses — the M message-type space is not fully mapped, so trust the raw number. - The AC cleartext length is
enc_len, a u16 big-endian (09b §3); the old 1-byte read was accidentally correct only for payloads < 256. - AC assoc-req / assoc-resp / deauth layouts are decoded straight from the
spec, but the capture corpus has no real AC samples of them yet — only
beacon and probe-req are wire-verified. The
version < 9decoders are likewise unverified (TX only emits 9). See the synthetic vectors intest/selftest.lua. - Decryption depends on the 802.11 source MAC being present in the capture. If a frame can't be decrypted, the cleartext outer header is still shown with an expert note.
When a field is added/renamed in pyrmax
(src/pyrmax/ac.py or its ksy/*.ksy schemas), mirror it in the matching
wsairmax/*.lua decoder and, if user-visible, add a ProtoField in
airmax.lua. The ground-truth vectors in test/selftest.lua come from the
pyrmax sample pcaps, so a divergence will surface there.