Skip to content

DRILL-8556: Various PCAP and PCAP-NG Improvements - #3087

Open
cgivre wants to merge 61 commits into
apache:masterfrom
cgivre:pcap-improvements
Open

cgivre wants to merge 61 commits into
apache:masterfrom
cgivre:pcap-improvements

Conversation

@cgivre

@cgivre cgivre commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

DRILL-8556: Various PCAP and PCAP-NG Improvements

Description

The PCAP-NG reader read the whole file into memory (twice: the raw bytes plus the third-party library's list of every parsed block), so large captures needed several times their size in heap. This PR streams PCAP-NG files block by block, removes the pcapngdecoder dependency, and fixes a set of decoding bugs in both the PCAP and PCAP-NG readers.

PCAP-NG reader

  • Streams blocks one at a time; memory no longer grows with file size.
  • Tracks interfaces per section: timestamps honor if_tsresol and if_tsoffset (nanosecond captures were off by 1000x), and packets are decoded for their interface's link type.
  • Decodes Ethernet (including 802.1Q/QinQ tags), raw IP, BSD loopback, PPP, Linux SLL/SLL2, and 802.11 / radiotap data frames. Unsupported link types return null IP fields instead of misread ones.
  • Skips unknown block types (previously it stopped silently at the first one) and tolerates a truncated final block.
  • Decodes each packet once per row instead of once per column.
  • Supports sessionizeTCPStreams.

Packet decoding (both readers)

  • The TCP header length was read from the wrong byte and then over-counted, and payloads included link-layer headers, Ethernet padding, and bytes past the captured length.
  • IPv6: extension headers were walked incorrectly and TCP fields ignored the 40-byte IPv6 header, so IPv6 TCP sequence numbers, flags, and ports were misread.
  • ARP_PROTOCOL was defined as IP protocol 0 (IPv6 hop-by-hop), so IPv6 multicast packets were labeled ARP and real ARP frames were not. ARP is now detected by EtherType; ICMPv6 is labeled ICMP.

Format plugin

  • The file format was a static field shared by all readers and was detected from the first file in the scan, so directories mixing .pcap and .pcapng sent files to the wrong reader.

TCP sessions (both readers)

  • One shared TcpSessionizer; only TCP packets are tracked (all non-TCP packets previously accumulated in a "session 0" that was never written).
  • Sessions still open at end of file are returned with a new session_closed column instead of dropped. Packets trailing a closed session no longer start a phantom session. connection_time is null when the handshake was not captured.

Protocol decoders

  • Pluggable decoders for application protocols, discovered with ServiceLoader: PacketProtocolDecoder for single packets and SessionProtocolDecoder for reassembled TCP sessions. A decoder only handles traffic that parses as its protocol. Design and decoder-writing guide: docs/dev/PcapProtocolDecoders.md.
  • New columns in both readers: parsed_protocol, parsed_data (one map per decoder) and decode_error. Decoding runs only when these columns are queried.
  • Packet decoders: DNS (including mDNS and LLMNR), HTTP/1.x, TLS ClientHello/ServerHello (SNI, ALPN, JA3, JA3S, JA4), DHCP, DHCPv6, ICMP and ICMPv6, ARP, NTP, Syslog, SSDP, SIP, TFTP, NetBIOS name service, RADIUS, SNMP (v1, v2c, v3), STUN, Kerberos, and QUIC v1 Initial (the ClientHello is decrypted with the initial keys derived from public values per RFC 9001: SNI, ALPN, JA4).
  • Session decoders: HTTP/1.x (keep-alive and chunked bodies handled), TLS handshake (negotiated version and cipher, certificate subject, issuer, validity, SANs), FTP control channel, SSH (identification and HASSH), DNS over TCP, SMTP, POP3 and IMAP (credentials, envelope and message headers), SMB2/3 (dialect, signing, and the authenticating identity from the NTLMSSP blob via a shared NTLMSSP parser), LDAP (bind DNs, result, search filters), Telnet, RDP (the mstshash cookie and requested security protocols) and MQTT.
  • Authentication protocols output identity metadata only. SMB/NTLM surfaces the account, domain and workstation but never the NTLM challenge, responses or session key, with or without exposeCredentials, since those are offline-crackable material rather than a password; Kerberos records only the encryption types, never the encrypted parts. Only JA4 is implemented, not the JA4S/JA4H/JA4SSH variants, whose license is not compatible with Apache.
  • Every decoder field can be queried without quotes; TestDecoderFieldNames checks this for every registered decoder.
  • Errors never stop a file from being read: malformed packets keep their readable fields and explain the problem in decode_error, and damaged or non-capture files return a row with only decode_error set instead of failing the query.
  • New format option exposeCredentials (default false): cleartext passwords found by decoders are only returned when it is set; usernames and a password_present flag are always returned.

Drill scan framework fixes (exec/java-exec), found while implementing the decoders:

  • OutputBatchBuilder rebuilt map arrays nested in maps as plain maps, dropping their offsets, so such batches failed validation.
  • ScanProjectionParser counted array dimensions from the root of the path, so projecting an element of an array inside a map (m.a[0]) failed as incompatible.
  • ResultSetLoaderImpl could fail a query while harvesting a full batch: filling the many unwritten decoder columns asked for more memory than the batch budget allowed. Those fills are now allowed while harvesting.

Read reliability (classic PCAP)

  • The reader could not read gzip-compressed files: a refill overwrote unread bytes, and packets kept for sessions pointed into the reused buffer.
  • A read failure part way through a file keeps the packets already read and adds an error row.

Documentation

Breaking change: the PCAP-NG timestamp column is renamed packet_timestamp, the name PCAP files already use. timestamp is a reserved word, so it had to be quoted in every query and an unquoted MIN(timestamp) was a parse error.

New PCAP-NG packet columns: captured_length, interface_id, interface_name, link_type, comment, direction, reception_type, fcs_length, drop_count, packet_hash. New session column: session_closed. In stat mode (stat: true), options a block does not have are now null (previously "" or -1), a comment column is added, and if_speed / if_tsoffset are BIGINT. The plugin README documents the columns and supported link types.

User-visible value changes from the bug fixes: classic PCAP data payloads, IPv6 TCP fields, type for ARP/ICMPv6, and is_corrupt (testv1.pcap reported 16 corrupt packets that were snaplen-truncated, not corrupt).

Testing

  • Replaced the corrupted todo/ fixtures (they had been run through a UTF-8 conversion) with valid Wireshark captures, and added generated fixtures covering link types, timestamp resolutions, packet options, multiple sections, encapsulations and sessionization. Fixtures were cross-checked with scapy.

  • Every classic PCAP payload in the test fixtures was compared against scapy (6,736 exact matches; 17 encrypted payloads differ only in how invalid UTF-8 renders). Test expectations that encoded the old bugs were corrected after verifying the new values against scapy.

  • Protocol decoders: unit tests for the registry, TCP stream reassembly, DNS and HTTP parsers (including compression-pointer loops, record and header caps, keep-alive pairing with chunked bodies), and query tests on generated fixtures for decoder columns and every error-handling case. Expected values were cross-checked with scapy.

  • Every decoder has unit tests and a query test on a generated fixture holding valid traffic, other traffic on the same port (left undecoded) and a malformed message (reported in decode_error). Fixtures are cross-checked with scapy where scapy supports the protocol (DNS, DHCP, NTP, SNMP, TLS, SSH KEXINIT and others); HASSH and JA3 values are computed independently.

  • The format-pcapng module: 332 tests pass, plus checkstyle and the license check.

  • Scan framework fixes: regression tests TestOutputBatchBuilder.testMapArrayInMap, TestProjectionParser.testArrayInMap and TestResultSetLoaderLimits.testHarvestFillsUnwrittenColumnsOverBudget; 501 scan/result-set-loader/projection tests and 246 JSON reader tests pass.

cgivre added 2 commits October 2, 2026 14:18
PCAP-NG reader:
- Stream blocks one at a time instead of reading the whole file into
  memory, and parse them directly, removing the pcapngdecoder dependency
- Track interfaces per section: timestamps honor if_tsresol and
  if_tsoffset, and each packet is decoded for its interface's link type
- Decode Ethernet (with 802.1Q/QinQ tags), raw IP, BSD loopback, PPP,
  Linux SLL/SLL2, and 802.11 / radiotap data frames
- Add interface_id, interface_name, link_type, captured_length,
  comment, direction, reception_type, fcs_length, drop_count and
  packet_hash columns
- Skip unknown block types instead of stopping, and tolerate a
  truncated final block
- Decode each packet once per row instead of once per column
- Stat queries: return null for missing options, add comment, make
  if_speed and if_tsoffset BIGINT, and use the interface's resolution
  for statistics timestamps
- Support sessionizeTCPStreams

Packet decoding (both readers):
- Fix the TCP header length and payload offsets, bound payloads by the
  IP length and captured length, and walk IPv6 extension headers
  correctly; IPv6 TCP sequence numbers, flags and ports were misread
- Detect ARP by EtherType and label ICMPv6 as ICMP; IPv6 packets with a
  hop-by-hop header were labeled ARP

Format plugin:
- Detect PCAP vs PCAP-NG per file; the format was a static field and was
  detected from the first file in the scan

TCP sessions (both readers):
- Share one TcpSessionizer; only TCP packets are tracked
- Return sessions still open at end of file, with a new session_closed
  column, and ignore packets trailing a closed session
- connection_time is null when the handshake was not captured

Tests: replace the corrupted todo/ fixtures with valid captures, add
fixtures for metadata, link types and sessionization, and correct
expectations that encoded the old decoding bugs.
@cgivre cgivre self-assigned this Oct 2, 2026
@cgivre cgivre added enhancement PRs that add a new functionality to Drill code-cleanup refactoring PR related to code refactoring doc-impacting PRs that affect the documentation dependencies labels Oct 2, 2026
@cgivre
cgivre marked this pull request as ready for review October 2, 2026 19:15
cgivre added 9 commits October 2, 2026 15:19
A leftover probe test and the three ServiceLoader registration files had no
license header, which failed the Apache RAT check.
Interim 1xx responses such as 100 Continue were paired as answers, shifting
every later request-response pair on the connection by one.
- Cap PCAP-NG block lengths at 16 MiB and the PCAP snapshot length at
  libpcap's 262144 bytes, so a damaged or crafted header becomes an error
  row instead of a multi-gigabyte allocation
- In session mode, where packets are not rows, report each distinct
  packet or interface error once as an error row instead of dropping it
timestamp is a reserved word, so every query had to quote it, and an unquoted
reference such as MIN(timestamp) was a parse error. packet_timestamp is also
the name classic PCAP files already use.
- Keep the IP addresses of a packet whose IPv4 header cannot be parsed,
  with ports left empty, instead of discarding everything after the link layer
- DNS records past the 64-item cap are skipped, so later sections are kept
- An invalid HTTP Content-Length is reported once
- Compare HTTP chunk sizes without risk of overflow
- Give each reader its own copy of the parsed_data schema
getLinkPayload returns the bytes after the link-layer header (the ARP
message of an ARP frame); getIpPayload returns the bytes after the IP header
(the ICMP message of an ICMP packet). ARP frames read from PCAP-NG now record
where their link payload starts on every link type.
- PCAP: refill the buffer by appending after the unread bytes until it is
  full. Topping up at the read position overwrote unread data whenever a read
  came back short, so gzip-compressed captures could not be read at all
- PCAP: read the file header fully instead of with a single read
- Each PCAP packet copies its record; packets kept for sessionization pointed
  into the reader's buffer and changed when it was reused, corrupting session
  data in captures larger than the buffer
- A read that fails partway through a file (for example a corrupted gzip
  stream) now returns the packets read before it and an error row, in both
  readers, instead of ending silently (PCAP) or failing the query (PCAP-NG)
cgivre added 29 commits October 2, 2026 16:01
Decode the TLS ClientHello and ServerHello that start a TCP segment on the
TLS ports: versions, session ID, SNI, ALPN, cipher suites, extensions,
groups, point formats, signature algorithms, and the JA3 and JA3S
fingerprints.
Decode STUN messages (RFC 5389) on UDP 3478 and 19302: class, method,
transaction ID, mapped and XOR-mapped addresses, SOFTWARE, REALM, NONCE,
ERROR-CODE, USERNAME and the attribute list. Add query tests for the TLS
and STUN decoders.
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.PacketProtocolDecoder
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.PacketProtocolDecoder
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.PacketProtocolDecoder
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.PacketProtocolDecoder
Harvesting a batch fills in values for columns that were not written, and that
fill may need to grow a vector. When the batch was already over its memory
budget, the growth was refused and treated as overflow, which is not possible
during harvest, so the query failed with Unexpected state: FULL_BATCH. Readers
with many sparse columns, such as nested maps that most rows leave empty, hit
this on ordinary files.
Decodes the cleartext TLS handshake of TCP sessions on TLS ports: SNI,
ALPN, offered and selected versions, cipher suite, session resumption and,
for TLS 1.2 and earlier, the server certificate chain (parsed with the JDK
X.509 certificate factory). Records and handshake messages are reassembled
across segments and records, bounded at 64 KB per direction. In TLS 1.3 the
certificate is encrypted and only that fact is recorded.
# Conflicts:
#	contrib/format-pcapng/src/test/resources/fixtures/tls_fixtures.py
FTP control channel (port 21): banner, login, SYST, directories, transfers
with their data addresses, commands and replies; stops at an accepted AUTH TLS.
SSH (ports 22 and 2222): identification strings and the first KEXINIT of each
side with HASSH client and server fingerprints.
DNS over TCP (port 53): every length-prefixed message of a session, including
zone transfers, reusing DnsParser.
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.SessionProtocolDecoder
Session decoders for the mail protocols, sharing header parsing (folded
lines, RFC 2047 UTF-8 encoded-words), SASL credential handling and bounded
lists in protocol/mail. Each records the greeting, commands and replies
with passwords masked unless exposeCredentials is set, STARTTLS/STLS
switches, and the headers and sizes of transferred or retrieved messages.
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.SessionProtocolDecoder
Add an identity-only NTLMSSP parser (protocol.ntlm) and an SMB2/SMB3
session decoder (protocol.smb, TCP 445 and 139). The decoder reports the
negotiated and offered dialects, signing and encryption state, the server
and client GUIDs, and the user, domain and workstation from the NTLMSSP
blob inside the SPNEGO token of SESSION_SETUP. Pure SMB1 sessions report
only dialect SMB1; an SMB3 transform header marks the stream encrypted and
opaque; Kerberos is reported as auth_type kerberos with no identity fields.

The NTLM helper exposes identity metadata only: server challenges, LM/NT
responses and session keys are never read out, regardless of
exposeCredentials.
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.SessionProtocolDecoder
Decodes the cleartext metadata of Kerberos v5 (RFC 4120) AS-REQ, AS-REP,
TGS-REQ, TGS-REP and KRB-ERROR messages over UDP and single-segment TCP
port 88: message type, realm, client and server principal names, offered
encryption types, the ticket enc-part etype (the Kerberoasting signal),
the error code and name, pre-auth presence and the requested till time.
Encrypted parts are never read.

Adds a shared, bounded ASN.1 BER reader (protocol.asn1.Asn1Reader),
modelled on the SNMP decoder's package-private BerReader, with value
accessors for integers, strings, OIDs and GeneralizedTime.
Reassembles LDAP (RFC 4511) messages across TCP segments on port 389 and
reads the bind version, DN and authentication type, whether a password was
sent (and the cleartext password only with exposeCredentials), the bind
result code and name, search base DNs, scopes and RFC 4515 filter strings,
and the DNs of returned entries. Lists are capped at 64 and sessions at
1000 messages. Reuses the shared protocol.asn1.Asn1Reader.
# Conflicts:
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.PacketProtocolDecoder
#	contrib/format-pcapng/src/main/resources/META-INF/services/org.apache.drill.exec.store.pcap.protocol.SessionProtocolDecoder

private static byte[] aesEcb(byte[] key, byte[] data) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code-cleanup dependencies doc-impacting PRs that affect the documentation enhancement PRs that add a new functionality to Drill refactoring PR related to code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants