pcap

Read network captures (pcap and pcapng) into packet maps: Ethernet and VLAN, IPv4/IPv6, ARP, TCP, UDP and ICMP, with DNS questions and answers, HTTP requests, and the server name (SNI) from TLS hellos. Then group packets into flows, find top talkers, list DNS lookups, HTTP requests and TLS names, spot port scans, and write filtered packets back out as pcap. Reassemble IP fragments and TCP streams to read whole HTTP exchanges and pull out the files they carried, with hashes. Times are UTC. Pure Python, no tcpdump needed.

import pcap

Example

import pcap

# apps/data/sample.pcap is a small made-up capture (documentation addresses).
let packets = pcap.read("apps/data/sample.pcap")
say(f"{len(packets)} packets")

for q in pcap.dns_queries(packets).slice(0, 3) {
  say(f"DNS {q.type} {q.name} -> {q.answers} ({q.rcode})")
}
for flow in pcap.top_talkers(packets, 3) {
  say("talker:", flow)
}
for hello in pcap.sni(packets) {
  say(f"TLS to {hello.dst}:{hello.dport} for {hello.sni}")
}
for s in pcap.port_scans(packets) {
  say(f"{s.src} probed {s.count} ports on {s.dst}")
}
# Reassemble the TCP streams, then pull out the files that crossed plain HTTP.
for f in pcap.http_objects(packets) {
  say(f"{f.name}: {f.size} bytes from {f.url}, sha256 {f.sha256.slice(0, 16)}...")
}

Functions

read(path, limit?)

returns list

Every packet in a pcap or pcapng file, as a list of maps. Fails if the file isn't a capture. Fails too past MAX_PACKETS packets or 512 MB of packet data: use pcap.each() for those.

Arguments

  • limit read only the first limit packets.

Fields of each packet

  • "number", "time", "when", "length"
  • "protocol" like tcp, udp, icmp or arp.
  • "src", "dst" the addresses.
  • "sport", "dport" the ports.
  • "flags" the TCP flags.
  • "ttl"
  • "app" the application protocol, like dns, http, tls or ssh.
  • "dns", "http", "tls", "arp", "icmp" detail maps for that protocol.
  • "payload" the payload bytes.
  • "payload_length" the size of the payload.
  • "summary" one readable line.
  • "raw" the whole frame.
Visualizehow pcap.read works

pcap.read("apps/data/sample.pcap", 3) gives [{"number": 1, "time": 1773306845.05, "when": "2026-03-12 09:14:05", "length": 42, "captu…

Drawing…

Real result: worked out by running this example with TB while the site was built.

parse(data, limit?)

returns list

Like pcap.read() but for capture bytes already in memory.

Visualizehow pcap.parse works

pcap.parse(data, 2) gives [{"number": 1, "time": 1773306845.05, "when": "2026-03-12 09:14:05", "length": 42, "captu…

pcap.parse: inputs flowing into the function and the resultdatacapture bytesd4 c3 b2 a1 …limit?2pcap.parseresult · one map per packetnumberprotocolsrcdst1arp192.0.2.10192.0.2.12arp192.0.2.1192.0.2.10

Real result: worked out by running this example with TB while the site was built.

each(path, fn)

returns number

Call fn(packet) for every packet in the file, one at a time, streaming from disk, so captures of any size use little memory (and there is no packet limit). .gz and one-file .zip captures are unpacked as they are read. Return false from fn to stop early. Gives back how many packets fn saw.

Visualizehow pcap.each works

pcap.each("apps/data/sample.pcap", p => seen.set(p.protocol, true)) gives 94

pcap.each: inputs flowing into the function and the resultpathapps/data/sample.pcapone packet at a timepacket 1 → fnpacket 2 → fn… packet 94 → fnfnp => seen.set(p.protocol, true)pcap.eachresult · how many packets94

Real result: worked out by running this example with TB while the site was built.

format(path)

returns text

"pcap", "pcap-ns" (nanosecond) or "pcapng"; fails for other files.

Visualizehow pcap.format works

pcap.format("apps/data/sample.pcap") gives "pcap"

pcap.format: inputs flowing into the function and the resultpathapps/data/sample.pcapfirst 4 bytesd4 c3 b2 a1 = classic pcappcap.formatresult · text"pcap"

Real result: worked out by running this example with TB while the site was built.

flows(packets)

returns list

Group packets into conversations, oldest first. A conversation is one protocol, addresses and ports, in both directions.

Fields of each flow

  • "protocol", "src", "sport", "dst", "dport"
  • "packets", "bytes", "client_bytes", "server_bytes"
  • "first", "last" the first and last timestamps.
  • "first_when", "last_when" the same times as text.
  • "duration"
  • "flags", "app", "service"
  • "names" DNS names, HTTP hosts and SNI names seen.
  • "state" for TCP, one of:
    • "established", "refused", "no_reply", "half_open" or "other"
Visualizehow pcap.flows works

pcap.flows(packets) gives [{"protocol": "arp", "src": "192.0.2.10", "sport": nil, "dst": "192.0.2.1", "dport": nil,…

pcap.flows: inputs flowing into the function and the resultpackets94 packets(from pcap.read)grouped byprotocol + both addresses + ports,either directionpcap.flowsresult · 39 conversations, oldest firstprotocolsrcdstdportarp192.0.2.10192.0.2.1niludp192.0.2.10192.0.2.153tcp192.0.2.10203.0.113…80tcp192.0.2.10203.0.113…443udp2001:db8:…2001:db8:…53… 34 more rows (39 in all)

Real result: worked out by running this example with TB while the site was built.

top_talkers(packets, n?)

returns list

The busiest addresses by bytes (default: top 10).

Fields of each address

  • "ip", "packets", "bytes", "sent", "received", "peers"
Visualizehow pcap.top_talkers works

pcap.top_talkers(packets, 3) gives [{"ip": "192.0.2.10", "packets": 92, "bytes": 6313, "sent": 3420, "received": 2893, "sent…

Drawing…

Real result: worked out by running this example with TB while the site was built.

dns_queries(packets)

returns list

Every DNS question asked, with the answers that came back.

Fields of each query

  • "time", "when", "src", "dst", "name", "type"
  • "answers" a list of the values that came back.
  • "rcode", "answered"
Visualizehow pcap.dns_queries works

pcap.dns_queries(packets) gives [{"time": 1773306845.101, "when": "2026-03-12 09:14:05", "src": "192.0.2.10", "dst": "192…

pcap.dns_queries: inputs flowing into the function and the resultpackets94 packets8 of them DNSpcap.dns_queriesresult · each question with its answerssrcnametype192.0.2.10www.example.…A2001:db8::10mail.example…AAAA192.0.2.10xk3v9q2mz7w1…A192.0.2.10update-check…A

Real result: worked out by running this example with TB while the site was built.

http_requests(packets)

returns list

Every plain HTTP request.

Fields of each request

  • "time", "when", "src", "dst", "dport"
  • "method", "host", "path", "url"
  • "user_agent", "headers", "content_type"
Visualizehow pcap.http_requests works

pcap.http_requests(packets) gives [{"time": 1773306845.203, "when": "2026-03-12 09:14:05", "src": "192.0.2.10", "sport": 51…

pcap.http_requests: inputs flowing into the function and the resultpackets94 packetsplain HTTP on port 80pcap.http_requestsresult · every request seenmethodhostpathdportGETwww.examp…/80GET198.51.10…/stage2.p…80

Real result: worked out by running this example with TB while the site was built.

sni(packets)

returns list

The server names from TLS Client Hellos. They show what HTTPS connections are for, even though the traffic is encrypted.

Fields of each hello

  • "time", "when", "src", "dst", "dport"
  • "sni" the server name.
  • "version", "alpn"
Visualizehow pcap.sni works

pcap.sni(packets) gives [{"time": 1773306845.324, "when": "2026-03-12 09:14:05", "src": "192.0.2.10", "sport": 51…

pcap.sni: inputs flowing into the function and the resultpackets94 packetsHTTPS: encrypted, but...TLS Client Hellonames the server in clear textpcap.sniresult · which sites HTTPS went todstsniversion203.0.113.50www.example.…TLS 1.2198.51.100.2…login.exampl…TLS 1.3198.51.100.2…login.exampl…TLS 1.3198.51.100.2…login.exampl…TLS 1.3198.51.100.2…login.exampl…TLS 1.3

Real result: worked out by running this example with TB while the site was built.

port_scans(packets, min_ports?)

returns list

Sources that sent bare TCP SYNs to many ports of one host (default: 10 or more ports).

Fields of each scan

  • "src", "dst", "ports", "count"
  • "open_ports" the ports that answered SYN-ACK.
  • "first_when", "last_when", "duration"
Visualizehow pcap.port_scans works

pcap.port_scans(packets, 10) gives [{"src": "198.51.100.77", "dst": "192.0.2.10", "ports": [21, 22, 23, 25, 53, 80, 110, 111…

pcap.port_scans: inputs flowing into the function and the resultpackets94 packetsmin_ports?10bare TCP SYNsone source → many portsof one hostpcap.port_scansresult · scanners and the open ports they foundsrcdstcount198.51.100.77192.0.2.1025packets94 packetsmin_ports?25bare TCP SYNsone source → many portsof one hostpcap.port_scansresult · scanners and the open ports they foundsrcdstcount198.51.100.77192.0.2.1025packets94 packetsmin_ports?30bare TCP SYNsone source → many portsof one hostpcap.port_scansresult · scanners and the open ports they found[] (empty list)

Real result: worked out by running this example with TB while the site was built.

summary(path_or_packets)

returns map

An overview of a capture.

Fields

  • "format", "packets", "bytes"
  • "first_when", "last_when", "duration"
  • "hosts", "flows"
  • "protocols" a map of name -> count.
  • "apps"
  • "ports" the top destination ports.
  • "top_talkers", "dns_queries", "http_requests", "tls_hellos", "port_scans"
Visualizehow pcap.summary works

pcap.summary("apps/data/sample.pcap") gives {"format": "pcap", "packets": 94, "bytes": 6541, "first": 1773306845.05, "last": 17733068…

pcap.summary: inputs flowing into the function and the resultpath_or_packetsapps/data/sample.pcappcap.summaryresult · an overview of the capturepackets94duration34.651protocols{"tcp": 82, "udp": 8, "arp": 2, "…apps{"dns": 8, "tls": 6, "http": 4, "…flows39hosts7

Real result: worked out by running this example with TB while the site was built.

filter(packets, fn)

returns list

Keep the packets for which fn(packet) is true (same as packets.filter(fn); here so pipelines read naturally).

Visualizehow pcap.filter works

pcap.filter(packets, p => p.dport == 443) gives [{"number": 10, "time": 1773306845.293, "when": "2026-03-12 09:14:05", "length": 54, "cap…

pcap.filter: inputs flowing into the function and the resultpackets94 packetsfnp => p.dport == 443pcap.filterresult · only the packets fn keptnumbersrcdstdport10192.0.2.10203.0.113…44312192.0.2.10203.0.113…44326192.0.2.10198.51.10…44328192.0.2.10198.51.10…44329192.0.2.10198.51.10…443… 6 more rows (11 in all)

Real result: worked out by running this example with TB while the site was built.

write(path, packets)

returns number

Save packets (from pcap.read, maybe filtered) as a classic pcap file; gives back how many were written. Lets you cut a huge capture down to the packets that matter and open the result in Wireshark.

Visualizehow pcap.write works

pcap.write("https-only.pcap", packets.filter(p => p.dport == 443)) gives 11

pcap.write: inputs flowing into the function and the resultpacketsthe 443 packets(after filter)pcap.writepathhttps-only.pcapresult · how many packets were written11

Real result: worked out by running this example with TB while the site was built.

defragment(packets)

returns list

Put fragmented IPv4 and IPv6 packets back together. Each complete set of fragments becomes one packet, decoded again (so TCP, UDP, DNS and HTTP inside it show up), in the place where its last fragment arrived. Sets with a piece missing are left as they are.

Fields added to a reassembled packet

  • fragments the numbers of the packets it was built from.

Fragments that are left over keep a "fragment" map with "offset", "more" and "id".

Visualizehow pcap.defragment works

pcap.defragment(packets) gives [{"number": 1, "time": 1773306845.05, "when": "2026-03-12 09:14:05", "length": 42, "captu…

pcap.defragment: inputs flowing into the function and the resultpackets94 packetsIP fragments in piecespieces joinedby address + IP id,in offset orderpcap.defragmentresult · whole packets, decoded againnumberprotocolsrcdst1arp192.0.2.10192.0.2.12arp192.0.2.1192.0.2.103udp192.0.2.10192.0.2.14udp192.0.2.1192.0.2.105tcp192.0.2.10203.0.113…… 89 more rows (94 in all)

Real result: worked out by running this example with TB while the site was built.

streams(packets, options?)

returns list

Every TCP connection with the bytes each side sent, in order. Fragments are joined first, segments that arrived out of order are sorted, and repeated bytes (retransmissions and overlaps) are kept once; where the same bytes arrive twice, the first copy wins.

Options

  • max_bytes the most bytes kept for each side of a connection (default: 16 MB).

Fields of each stream

  • id 1 for the oldest connection, then 2, 3 and so on.
  • src, sport the client (the side that sent the first SYN).
  • dst, dport the server.
  • state how the connection went, one of:
    • "established", "closing", "closed" (FIN both ways), "reset", "refused", "no_reply"
    • "partial" the capture started after the handshake.
  • handshake true when both SYNs were seen.
  • packets, first, last, first_when, last_when, duration
  • app "http", "tls", "ssh" or nil, from the first bytes.
  • service the usual name for the port.
  • client, server what each side sent, as a map.

Fields of client and server

  • data the bytes, in order.
  • bytes, packets
  • gaps holes where segments were never captured, as maps of "at" (where in data) and "length".
  • missing how many bytes the gaps add up to.
  • retransmissions, overlaps, out_of_order how many segments were repeats, partly repeats, or late.
  • truncated true when max_bytes cut the data short.
  • fin true when this side sent FIN.
Visualizehow pcap.streams works

pcap.streams(packets, {"max_bytes": 65536}) gives [{"id": 1, "src": "192.0.2.10", "sport": 51001, "dst": "203.0.113.50", "dport": 80, "stat…

pcap.streams: inputs flowing into the function and the resultpackets94 packetsoptions?max_bytes65536TCP segmentssorted by sequence number,repeats kept once, gaps markedpcap.streamsresult · one map per connectionidsrcdstdport1192.0.2.10203.0.113…802192.0.2.10203.0.113…4433192.0.2.10198.51.10…804192.0.2.10198.51.10…4435192.0.2.10198.51.10…443… 28 more rows (33 in all)

Real result: worked out by running this example with TB while the site was built.

http_exchanges(packets, options?)

returns list

Every HTTP request with its response, read from the reassembled TCP streams, so bodies split over many packets come out whole. Handles keep-alive, Content-Length, chunked transfer encoding and gzip or deflate content. Damaged packets and streams are skipped.

Options

  • max_bytes the most bytes kept for each side of a connection (default: 16 MB). One call unpacks at most 4 times max_bytes of gzip and deflate in total; bodies past that come back as sent.

Fields of each exchange

  • stream the id from pcap.streams().
  • time, when when the request was sent.
  • src, sport, dst, dport
  • method, host, path, url, user_agent
  • request_headers, request_body
  • status, reason, response_headers, content_type, response_body
  • request_decoded, response_decoded false when a gzip or deflate body comes back as sent, because it was broken, bigger than max_bytes, or over the call's unpacking limit.
  • complete true when both bodies arrived in full.

A request with no answer has status nil.

Visualizehow pcap.http_exchanges works

pcap.http_exchanges(packets, {"max_bytes": 65536}) gives [{"stream": 1, "time": 1773306845.203, "when": "2026-03-12 09:14:05", "src": "192.0.2.10"…

pcap.http_exchanges: inputs flowing into the function and the resultpackets94 packetsoptions?max_bytes65536reassembled streamsrequests paired with responsespcap.http_exchangesresult · request + response, whole bodiesmethodurlstatuscontent_t…GEThttp://ww…200text/htmlGEThttp://19…200applicati…

Real result: worked out by running this example with TB while the site was built.

http_objects(packets, options?)

returns list

The files carried over plain HTTP: response bodies (downloads) and request bodies (uploads), with their hashes, ready to check against threat intelligence or save with fs.write_bytes().

Options

  • max_bytes the most bytes kept for each side of a connection (default: 16 MB). One call unpacks at most 4 times max_bytes of gzip and deflate in total.

Fields of each object

  • name from the Content-Disposition header or the end of the URL. Never a folder path, and always a name Windows can save.
  • direction "download" or "upload".
  • url, host, status, content_type
  • size the number of bytes.
  • sha256, md5 hashes of the bytes, as hex text.
  • data the bytes themselves.
  • decoded false when gzip or deflate bytes were kept as sent (see max_bytes).
  • stream, time, when
  • src, dst, dport the client and the server.
Visualizehow pcap.http_objects works

pcap.http_objects(packets, {"max_bytes": 65536}) gives [{"name": "index.html", "direction": "download", "url": "http://www.example.com/", "host"…

pcap.http_objects: inputs flowing into the function and the resultpackets94 packetsoptions?max_bytes65536HTTP bodiesdownloads and uploads,hashed with SHA-256 and MD5pcap.http_objectsresult · the files that crossed the wirenamesizecontent_t…sha256index.html15text/htmlb6c951ed9…stage2.ps183applicati…1bc20a264…

Real result: worked out by running this example with TB while the site was built.

service(port)

returns text

The usual service name for a port (443 -> "https"), or nil.

Visualizehow pcap.service works

pcap.service(22) gives "ssh"

pcap.service: inputs flowing into the function and the resultport22pcap.serviceresult · text"ssh"port53pcap.serviceresult · text"dns"port443pcap.serviceresult · text"https"port3389pcap.serviceresult · text"rdp"port40100pcap.serviceresult · textnil

Real result: worked out by running this example with TB while the site was built.

Values

NameValueWhat it is
pcap.MAX_PACKETS1000000

The most packets pcap.read() will load from one file.

Visualizehow pcap.MAX_PACKETS works

pcap.MAX_PACKETS gives 1000000

The value pcap.MAX_PACKETSpcap.MAX_PACKETS1000000what it isThe most packets pcap.read() will load fromone file.

No inputs: call it as it is.

Real value, read from TB while the site was built.