Skip to main content

flowreassemble

flowreassemble turns packet captures into application byte streams, so you can write a decoder for a custom protocol (such as C2 traffic) in a few lines of Python. It defragments IP, reassembles TCP and UDP flows, and parses HTTP with libhtp. The engine is written in Rust and reads from libpcap directly, either a pcap file or a live interface.

from flowreassemble import conversations, pcap

h = pcap.open_offline("c2.pcap")
h.setfilter("host 203.0.113.7")

for conv in conversations(h, server=("203.0.113.7", 8443)):
    print(conv.client, "->", conv.server, conv.close_reason)
    for turn in conv.turns:
        print(f"  {turn.direction.value}: {turn.data[:64]!r}")

It is a modern take on libnids. It keeps libnids's model (IP defragmentation, TCP reassembly, delivery of new bytes per direction) but not its API: there are no callbacks and no global state, and every connection's state lives on the flow.

Status: early development. The API can still change.

Install

pip install flowreassemble

Wheels exist for Python 3.10 and later on Linux (x86_64 and aarch64, glibc 2.28 or later) and macOS (11 or later, Apple silicon and Intel). Linux wheels contain their own libpcap, so you don't need to install one. macOS wheels use the libpcap that ships with macOS. There are no Windows wheels.

Two ways to read traffic

Both functions take the same source and options. Pick the one that matches how much data you expect.

  • conversations() yields one Conversation per connection after the connection closes, with all of its bytes. Use it for most investigations.
  • reassemble() yields events as packets arrive and never holds bytes back. Use it for live capture, for long-lived connections, or when a capture is too large to keep each connection in memory.

Conversations

from flowreassemble import Direction, conversations, pcap

for conv in conversations(pcap.open_offline("c2.pcap")):
    beacon = conv.to_server  # every byte sent to the server
    tasking = conv.to_client
    for turn in conv.turns:  # direction changes, in order
        if turn.direction is Direction.TO_SERVER:
            ...

A Conversation has these fields:

Field Content
client, server (ipaddress, port) pairs
transport "tcp" or "udp"
start, end Timestamps (seconds since the epoch, as float)
close_reason CloseReason.FIN, RST, TIMEOUT, or EOF
turns Turn(direction, ts, data) list. A TCP turn is all the bytes sent in one direction before the other side spoke; a UDP turn is one datagram.
to_server, to_client All captured bytes of one direction, concatenated
gaps (direction, offset, length) for bytes missing from the capture. offset counts every byte of that direction, missing ones included.
anomalies Anomaly(kind, direction, ts): "overlap" when a retransmission carried different bytes (the first copy is kept), "truncated" when max_bytes was reached
transactions HTTP mode only: the htp.Tx objects, in order
flow The Flow, the same object that reassemble() uses

max_bytes caps the bytes kept per conversation, both directions together (64 MiB by default; None removes the cap). Past the cap, the bytes are dropped, a "truncated" anomaly is recorded, and the conversation is still yielded.

Events

from flowreassemble import Closed, Data, Direction, Gap, Opened, pcap, reassemble

for ev in reassemble(pcap.open_offline("c2.pcap")):
    match ev:
        case Opened(flow):
            flow.state = MyDecoder()
        case Data(flow, Direction.TO_SERVER, data):
            flow.state.feed_request(data)
        case Data(flow, Direction.TO_CLIENT, data):
            flow.state.feed_response(data)
        case Gap(flow, direction, length):
            flow.state.resync(direction)
        case Closed(flow, reason):
            flow.state.done(reason)
Event Fields Meaning
Opened flow, ts A new connection (TCP) or exchange between two endpoints (UDP)
Data flow, direction, data, ts Bytes not delivered before. For UDP, exactly one datagram.
Gap flow, direction, length, ts length bytes of that direction were never captured
Closed flow, reason, ts The flow ended: CloseReason.FIN, RST, TIMEOUT, or EOF

flow.state is yours: store your decoder's state there. It disappears with the flow, so you don't need to clean anything up. Data only carries new bytes; if a message spans several Data events, buffer it yourself.

Use Direction.TO_SERVER and Direction.TO_CLIENT in case patterns. A bare name in a pattern captures the value instead of comparing it.

Options

Option Default Effect
server= None The server, as an address or an (address, port) pair. Without it, the TCP handshake tells which side is the server; when the handshake isn't in the capture, the side with the lower port is.
idle_timeout= 3600.0 Seconds without packets before a flow closes with TIMEOUT. None means never. The default is large so that sparse beacons on one connection stay one conversation.
http= None An htp.Config: parse every TCP connection as HTTP (see HTTP)
max_bytes= 64 MiB conversations() only: bytes kept per conversation

When the input ends, every open flow closes with reason EOF, so no data is lost.

Packet sources

Capture files and filters

flowreassemble.pcap exposes libpcap with its own names, without the pcap_ prefix. Open and filter the handle, then pass it to reassemble() or conversations():

h = pcap.open_offline("c2.pcap")  # pcap or pcapng
h.setfilter("tcp and host 203.0.113.7")
h.datalink()  # the link-layer type (DLT_*)

Live capture

Live capture needs capture privileges: root or CAP_NET_RAW on Linux, access to the BPF devices on macOS.

h = pcap.open_live("eth0", 65535, True, 100)  # device, snaplen, promisc, timeout_ms

To reach every setting, use libpcap's create and activate sequence instead:

h = pcap.create("eth0")
h.set_snaplen(65535)
h.set_immediate_mode(True)
h.set_buffer_size(16 << 20)
h.set_tstamp_precision("nano")
h.activate()

pcap.findalldevs() lists the devices you can open.

To stop a capture and still get the open flows, call h.breakloop(). It's safe to call from a signal handler. reassemble() and conversations() then close every open flow (reason EOF), yield what's left, and end normally:

import signal

signal.signal(signal.SIGTERM, lambda *_: h.breakloop())
for conv in conversations(h):
    ...

Ctrl-C raises KeyboardInterrupt right away instead, and the flows still open are not yielded.

Any other frame source

Both functions also accept any iterable of (timestamp, linktype, frame) tuples, such as frames read by dpkt or scapy. This is slower than a pcap handle, because every frame crosses from Python into the engine.

import dpkt

with open("c2.pcap", "rb") as f:
    reader = dpkt.pcap.Reader(f)
    frames = ((ts, reader.datalink(), buf) for ts, buf in reader)
    for conv in conversations(frames):
        ...

HTTP

With http=htp.Config(...), libhtp (0.5.53, vendored) parses every TCP connection. In reassemble(), each HTTP transaction comes as an htp.Tx event instead of Data. In conversations(), transactions go to conv.transactions, and turns keeps only the bytes libhtp didn't parse.

from flowreassemble import htp, pcap, reassemble

config = htp.Config(keep_bodies=10_000_000)
for ev in reassemble(pcap.open_offline("c2.pcap"), http=config):
    match ev:
        case htp.Tx() as tx if tx.parsed_uri.path == b"/my/c2/dispatcher/":
            if b"magic" in tx.request_cookies:
                print(tx.request_body, tx.response_status_number, tx.response_body)

htp.Tx is a copy of libhtp's htp_tx_t, with libhtp's field names: request_method, request_uri, parsed_uri, request_headers, request_cookies, request_auth_username, response_status_number, response_headers, and so on. Byte strings are bytes, and a field is None when libhtp has no value. Headers and cookies are htp.Table objects, where lookups by name ignore case. Two fields are added: flow and ts.

htp.Config holds libhtp's settings, with libhtp's defaults:

Setting Default
server_personality htp.ServerPersonality.MINIMAL
request_decompression False
response_decompression True
parse_request_cookies True
keep_bodies None

keep_bodies isn't a libhtp setting. It keeps up to that many bytes of each request and response body in request_body and response_body, de-chunked and decompressed if decompression is on. Compare a body with request_entity_len or response_entity_len to tell whether it was cut.

A transaction is yielded when libhtp completes it, or when its connection closes; check request_progress and response_progress to tell. If libhtp stops parsing a connection (after a CONNECT or a 101 tunnel, on a parse error, or on a gap it refuses, such as one inside a request line), the rest of that connection comes as plain Data events.

What the reassembler handles

  • Link layers and encapsulation: Ethernet, Linux cooked capture (SLL and SLL2), raw IP, null/loopback, VLAN and QinQ, MPLS, PPPoE, GRE, VXLAN, and IP-in-IP. VLAN traffic needs no special filter.
  • IP: IPv4 and IPv6, IPv6 extension headers, and defragmentation of both.
  • Checksums are never verified, so captures taken on a host with NIC offload work as they are.
  • Retransmissions and overlaps: the first copy of each byte wins. A later copy with different content is reported as an "overlap" anomaly.
  • Missing data: a hole is skipped and reported as a Gap when the receiver acknowledges data past it, when more than 16 MiB is buffered behind it, or when the connection closes.
  • Mid-stream pickup: connections whose handshake isn't in the capture are still reassembled. Use server= when port numbers don't tell which side is the server.
  • Port reuse: a new connection on the same address and port pair is a new flow, and late packets of a closed connection are dropped.

Development

Everything runs in the Nix dev shell (nix develop), which provides Rust, maturin, uv, Python, and libpcap.

nix develop
make dev      # create .venv and install the package in editable mode
make test     # Rust unit tests, then pytest
make lint     # rustfmt, clippy, ruff
make build    # release wheel

CI runs make lint and make test on Linux and macOS. To publish a release, set the same version in pyproject.toml and Cargo.toml, then push a tag named vX.Y.Z. The release workflow builds and tests the wheels and the sdist, attaches them to a GitHub release, and uploads them to PyPI.

License

BSD 3-Clause. The vendored libhtp (vendor/libhtp/) keeps its own license.

Metadata

Release files for flowreassemble 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flowreassemble 0.1.0
File Size Uploaded Provenance Actions
flowreassemble-0.1.0.tar.gz 218.4 kB With provenance Details for flowreassemble-0.1.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for flowreassemble 0.1.0
File Interpreter ABI Platform Actions
flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_x86_64.whl CPython 3.10 abi3 Linux glibc 2.28+ x86-64 Details for flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_x86_64.whl
flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_aarch64.whl CPython 3.10 abi3 Linux glibc 2.28+ ARM64 Details for flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_aarch64.whl
flowreassemble-0.1.0-cp310-abi3-macosx_11_0_arm64.macosx_11_0_universal2.macosx_11_0_x86_64.whl CPython 3.10 abi3 macOS 11.0+ universal2 (ARM64, x86-64), macOS 11.0+ x86-64, macOS 11.0+ ARM64 Details for flowreassemble-0.1.0-cp310-abi3-macosx_11_0_arm64.macosx_11_0_universal2.macosx_11_0_x86_64.whl

Total release size: 2.2 MB

Release files / flowreassemble-0.1.0.tar.gz

Download URL flowreassemble-0.1.0.tar.gz
Size 218.4 kB
Tags Source
SHA-256 checksum
How to use checksums
716814b57c8ba991d1f3aea86f2c33912faefd4b53e46c58b94215785ed583a3
BLAKE2b-256 checksum
How to use checksums
d5345a475d28e2d2a2ffe754d5a03da1f1f14a9b508983effa4a8b3c2b9a23cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Release files / flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_x86_64.whl

Download URL flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_x86_64.whl
Size 590.8 kB
Tags CPython 3.10 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
475cfcb84d22f8efd6ef14a56bd791221081a4610ae15d28250d37026813024b
BLAKE2b-256 checksum
How to use checksums
baffc008d9ed07c72d623bec3ac94fe20976bcb9cf387d7773d032a95a5ed757
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Release files / flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_aarch64.whl

Download URL flowreassemble-0.1.0-cp310-abi3-manylinux_2_28_aarch64.whl
Size 576.3 kB
Tags CPython 3.10 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
cebd9b32d0460546d6d816c8d644549b170d42e5e89f207daef80e212e74b340
BLAKE2b-256 checksum
How to use checksums
10e6f1b2860a1da66a28f1f33e1c1b4611213472493748c5248df761adc0d119
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Release files / flowreassemble-0.1.0-cp310-abi3-macosx_11_0_arm64.macosx_11_0_universal2.macosx_11_0_x86_64.whl

Download URL flowreassemble-0.1.0-cp310-abi3-macosx_11_0_arm64.macosx_11_0_universal2.macosx_11_0_x86_64.whl
Size 787.6 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64 macOS 11.0+ universal2 (ARM64, x86-64) macOS 11.0+ x86-64
SHA-256 checksum
How to use checksums
6000541c17e90788f7fc9cf4855022e20b363de6ac439c47dbd05ea53d07ce77
BLAKE2b-256 checksum
How to use checksums
b408bd4f879c29e2a1168a58be459ac24cb1ffd7de7e2e1f4977ff39ae1485be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Security for flowreassemble 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 4 files in this release have been uploaded with provenance metadata.

Select from 4 files

Attestation for flowreassemble-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

4 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page