802.15.4 · 6LoWPAN · RPL · Thread · CoAP

Low-power wireless packets.
Dissected in your browser.

dpkt-js takes pcap captures of IEEE 802.15.4 networks — Contiki-NG, OpenThread, Zephyr, RIOT — apart frame by frame, from the MAC header up to RPL, MLE, CoAP or a DNS-SD registration. Give it a Thread network key and the encrypted frames open too. TypeScript with no dependencies: the same code in a browser tab and in Node.

no dependenciesnothing uploadedMIC-verified decryptionBSD-3-Clause
openthread-services-nrf52840.pcap · frame 56
IEEE 802.15.4  Data seq=127 0x4c00 → 0x4000
  decrypted: Thread MAC key, MIC f69c3b27 verified
  6LoWPAN  fragment @312
    Reassembled IPv6 datagram  404 bytes from frames 50, 52, 54, 56
      IPv6  fdde:ad00:beef:0:2abe:b681:7c09:b3e2 → …:9051:6500:1c8a:a323
        UDP  49154 → 53536 len=364
          DNS (SRP)  UPDATE default.service.arpa, 7 records
            Updates
              _test._udp…  PTR  ins1._test._udp.default.service.arpa
              ins1._test._udp…  SRV  0 0 1234 dev2.default.service.arpa
              dev2.default.service.arpa  AAAA  fdde:ad00:beef:0:2abe:…
              dev2.default.service.arpa  KEY  68 bytes
            OPT (EDNS)  update lease 7200 s, key lease 1209600 s
▶ Open this frame
Try it

Four captures to open.

Each one was recorded in Cooja-NG, running the real firmware on emulated boards. Or drop your own pcap on the demo: it never leaves your browser.

IEEE 802.15.4  Data seq=91 4e:4e:7b:d3:… → ca:04:97:31:…
  6LoWPAN  IPHC + UDP
    IPv6  fe80::4c4e:7bd3:a12a:34dc → fe80::c804:9731:abf:997b
      UDP  19788 → 19788 len=72
        MLE  Child ID Response
          Source Address  0x8800
          Leader Data  partition 0x71ac6d98, leader router 34
          Active Timestamp  1 s + 0/32768
          Address16  0x8801
          Network Data
          Route64  1 router, ID sequence 121
OpenThread · CC2538748 frames

A Thread network forming

Five CC2538s running OpenThread's CLI: a leader, routers, a sleepy child polling its parent and a node two hops out. Parent and Child ID exchanges, mesh headers, a fragmented multicast ping. The key the scenario set is filled in.

IEEE 802.15.4  Data seq=159 3f:23:dc:2f:… → 22:24:52:65:…
  6LoWPAN  fragment @424
    Reassembled IPv6 datagram  475 bytes from frames 190 … 198
      IPv6  fe80::3d23:dc2f:b44e:ef30 → fe80::2024:5265:cc51:df6a
        UDP  1000 → 1000 len=435
          DTLS  Client Hello
          a joiner starting its commissioning handshake
OpenThread · nRF52840459 frames

Services and commissioning

An nRF52840 build with SRP, CoAP and the commissioner: a node registers a service over SRP, another GETs a CoAP resource, and a joiner discovers the network and runs its DTLS handshake.

IEEE 802.15.4  Data seq=130 00:12:74:02:… → 00:12:74:01:…
  6LoWPAN  IPHC + Hop-by-Hop Options
    IPv6  fd00::212:7402:2:202 → fd00::212:7401:1:101
      Hop-by-Hop Options
        RPL Option  instance 0, sender rank 353, up
      ICMPv6  RPL DAO
        RPL DAO  instance 0, sequence 241, ACK requested
          RPL Target  fd00::212:7402:2:202/128
          Transit Information  parent fd00::212:7401:1:101
Contiki-NG · Tmote Sky20 frames

RPL-UDP on the Tmote Sky

Contiki-NG's classic example on MSP430 motes: RPL DIOs and DAOs, the hop-by-hop RPL option, source routes, "hello" over UDP. The 6LoWPAN prefix is learned from the DIOs.

What it reads

From the MAC header up.

Every layer is rebuilt into its real bytes, not a description of them, so checksums can be checked: a wrong 6LoWPAN context or a misplaced fragment shows up as a bad UDP checksum, and a frame only counts as decrypted when its MIC verifies.

IEEE 802.15.4 2006 · 2015

Data, ACK and Enh-ACK, beacons and the Thread beacon, MAC commands, header and payload IEs: TSCH, CSL, time correction, 6P.

Decryption AES-CCM*

Thread MAC frames and MLE from the network key, Thread's well-known key, TSCH with its ASN nonce, 6TiSCH and Contiki-NG keys, any key you give it.

6LoWPAN RFC 4944 · 6282

Compressed headers rebuilt with their contexts, NHC for UDP and extension headers, mesh and broadcast headers, fragments reassembled.

IPv6 and RPL RFC 6550 · 6554

RPL, MPL and Trickle options, source routes, Neighbor Discovery and 6LoWPAN-ND, MLD, RPL control messages with their options.

Thread 1.x

MLE commands and TLVs, discovery and MeshCoP TLVs, management messages (TMF) by URI, SRP service registration, commissioning DTLS.

Applications UDP · TCP

CoAP with block-wise and responses matched to requests, DTLS, EDHOC, LwM2M (TLV and JSON), DNS and mDNS, HTTP, NAT64.

125 156frames from Contiki-NG's own regression suite, 85 simulations
9 235frames from the Cooja-NG examples, 2.4 GHz and sub-GHz
1 207frames of OpenThread on the CC2538 and the nRF52840
0left with an error; every checksum over the rebuilt IPv6 verifies

All captured in Cooja-NG and checked with npm run scan, which reports every frame a capture leaves undissected. The only frames left encrypted are a failed joiner's, sent under a key of its own.

Use it

One import, in Node or a browser.

The demo page is a thin layer over the library. Read a pcap, hand frames to a Dissector, and walk the tree it returns: labels, summaries, fields and children.

  1. Install itAn ES module with type declarations and no runtime dependencies.
  2. Dissect a captureThe Dissector keeps what reaches across frames: fragments, contexts, keys and the addresses decryption needs.
  3. Or build packetsEvery protocol from dpkt is a class that parses and serialises: new UDP(bytes), udp.toBytes().
  4. Check what a capture holdsnpm run scan lists the frames that don't dissect completely.
shell
$ npm install dpkt-js
dissect.mjs
import { readFileSync } from 'node:fs';
import { PcapReader, Dissector } from 'dpkt-js';

const pcap = new PcapReader(readFileSync('capture.pcap'));
const d = new Dissector(pcap.linktype, {
  threadNetworkKey: '00112233445566778899aabbccddeeff',
});
const packets = pcap.packets();
d.learn(packets.map((p) => p.data));   // 6LoWPAN context from RPL
for (const p of packets) {
  const tree = d.dissect(p.data, p.ts);
  console.log(p.ts.toFixed(3), tree.summary);
}
Where it comes from

dpkt, ported, then grown around real captures.

dpkt-js began as a TypeScript port of dpkt, the Python packet library: the same packet classes that parse and build frames, with dpkt's own tests ported alongside. It keeps that core and its BSD licence.

The dissector grew from there, around what low-power networks actually send: 6LoWPAN's compression, RPL, Thread's layers and the security they run under.

Its yardstick is Cooja-NG, which emulates networks of real firmware and writes every frame to a pcap. A capture of Contiki-NG's whole regression suite, or of OpenThread commissioning a joiner, is a test dpkt-js has to pass frame by frame.

It runs the same way in a browser, so a capture can be read where it was made: in a web page, without installing anything.