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 listEvery 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
limitread 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", 2) gives [{"number": 1, "time": 1773306845.05, "when": "2026-03-12 09:14:05", "length": 42, "captu…pcap.read("apps/data/sample.pcap", 3) gives [{"number": 1, "time": 1773306845.05, "when": "2026-03-12 09:14:05", "length": 42, "captu…pcap.read("apps/data/sample.pcap", 5) 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 listLike 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…
Real result: worked out by running this example with TB while the site was built.
each(path, fn)
returns numberCall 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
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"
Real result: worked out by running this example with TB while the site was built.
flows(packets)
returns listGroup 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,…
Real result: worked out by running this example with TB while the site was built.
top_talkers(packets, n?)
returns listThe 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, 1) gives [{"ip": "192.0.2.10", "packets": 92, "bytes": 6313, "sent": 3420, "received": 2893, "sent…pcap.top_talkers(packets, 3) gives [{"ip": "192.0.2.10", "packets": 92, "bytes": 6313, "sent": 3420, "received": 2893, "sent…pcap.top_talkers(packets, 5) 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 listEvery 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…
Real result: worked out by running this example with TB while the site was built.
http_requests(packets)
returns listEvery 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…
Real result: worked out by running this example with TB while the site was built.
sni(packets)
returns listThe 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…
Real result: worked out by running this example with TB while the site was built.
port_scans(packets, min_ports?)
returns listSources 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(packets, 25) gives [{"src": "198.51.100.77", "dst": "192.0.2.10", "ports": [21, 22, 23, 25, 53, 80, 110, 111…pcap.port_scans(packets, 30) gives []
Real result: worked out by running this example with TB while the site was built.
summary(path_or_packets)
returns mapAn 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…
Real result: worked out by running this example with TB while the site was built.
filter(packets, fn)
returns listKeep 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…
Real result: worked out by running this example with TB while the site was built.
write(path, packets)
returns numberSave 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
Real result: worked out by running this example with TB while the site was built.
defragment(packets)
returns listPut 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
fragmentsthe 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…
Real result: worked out by running this example with TB while the site was built.
streams(packets, options?)
returns listEvery 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_bytesthe most bytes kept for each side of a connection (default: 16 MB).
Fields of each stream
id1 for the oldest connection, then 2, 3 and so on.src, sportthe client (the side that sent the first SYN).dst, dportthe server.statehow the connection went, one of:- "established", "closing", "closed" (FIN both ways), "reset", "refused", "no_reply"
"partial"the capture started after the handshake.
handshaketrue when both SYNs were seen.- packets, first, last, first_when, last_when, duration
app"http", "tls", "ssh" or nil, from the first bytes.servicethe usual name for the port.client, serverwhat each side sent, as a map.
Fields of client and server
datathe bytes, in order.- bytes, packets
gapsholes where segments were never captured, as maps of "at" (where in data) and "length".missinghow many bytes the gaps add up to.retransmissions, overlaps, out_of_orderhow many segments were repeats, partly repeats, or late.truncatedtrue when max_bytes cut the data short.fintrue 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…
Real result: worked out by running this example with TB while the site was built.
http_exchanges(packets, options?)
returns listEvery 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_bytesthe 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
streamthe id from pcap.streams().time, whenwhen 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_decodedfalse 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.completetrue 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"…
Real result: worked out by running this example with TB while the site was built.
http_objects(packets, options?)
returns listThe 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_bytesthe 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
namefrom 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
sizethe number of bytes.sha256, md5hashes of the bytes, as hex text.datathe bytes themselves.decodedfalse when gzip or deflate bytes were kept as sent (see max_bytes).- stream, time, when
src, dst, dportthe 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"…
Real result: worked out by running this example with TB while the site was built.
service(port)
returns textThe usual service name for a port (443 -> "https"), or nil.
Visualizehow pcap.service works
pcap.service(22) gives "ssh"pcap.service(53) gives "dns"pcap.service(443) gives "https"pcap.service(3389) gives "rdp"pcap.service(40100) gives nil
Real result: worked out by running this example with TB while the site was built.
Values
| Name | Value | What it is |
|---|---|---|
pcap.MAX_PACKETS | 1000000 | The most packets pcap.read() will load from one file. Visualizehow pcap.MAX_PACKETS works
No inputs: call it as it is. Real value, read from TB while the site was built. |