1.5.0 – unreleased

The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. Three more extraction engines, ESP with payload decryption, SCTP and NGAP over SCTP, a library logger that no longer hijacks the consumer’s, and a defect programme run through the issue tracker across some 140 issues and pull requests between #326 and #509.

  • Added – three extraction engines: engine='pypcap' and engine='pcap_ct', two independent distributions of the same libpcap interface, and engine='pypcapfile' (#386, #405). They buy speed by doing less – neither pypcap nor pcap_ct dissects at all, so they offer neither reassembly nor flow tracing, and pypcapfile has no IPv6 decoder. Install only one of pypcap and pcap-ct: both own the top-level pcap module, and with both present pcap-ct wins the import and the other becomes unselectable. The matching interface constants PyPCAP, PCAP_CT and PyPCAPFile were missing and are now exported alongside DPKT, Scapy, PyShark and PCAPKit (#412). That brings the built-in set to seven engines; 3.11 is the last interpreter on which every one of them can run, and even there two of them cannot coexist.

  • AddedEngineBase.unsupported_reason, a preflight every engine answers and Extractor.run consults before anything is imported. Asking for an engine that cannot run in the current environment now gives one warning naming the real cause – a Python version, a missing tshark, a missing libpcap, the wrong pcap distribution – and a clean fall back to pcapkit’s own parser, rather than an error from inside the third-party package (#396, #405).

  • Added – ESP parsing and construction [RFC 4303], with optional payload decryption and ICV verification through cryptography (pip install pypcapkit[crypto]) (#378). Keys reach the dissector through a new caller-state channel, pcapkit.corekit.context, surfaced as the context= keyword on extract() and Extractor, carrying an esp.SecurityAssociation. Nothing here raises: with no association the SPI and sequence number are still reported and the ciphertext is left opaque, with status=NO_SA, and a failed decryption or ICV check is recorded on the parsed result the same way. Extended Sequence Numbers, TFC padding and anti-replay are not implemented, and an unsupported cipher or MAC is refused with a clear error rather than half-processed.

  • Added – SCTP as a transport protocol [RFC 9260]: all 13 chunk types, 8 chunk parameters and 13 error causes, with the CRC32c both recorded and verifiable – it covers the SCTP packet alone, with no IP pseudo-header, so it can be checked from the SCTP bytes. Upper layers register on the DATA chunk’s Payload Protocol Identifier through register_sctp, not on a port (#379).

  • Added – NGAP over SCTP (3GPP TS 38.413), decoding aligned PER through pycrate (pip install pypcapkit[NGAP]) (#251, #417). Decoding is generic by ASN.1 shape rather than per-procedure, so all 81 elementary procedures and 438 protocol IEs work and a new 3GPP release needs no code change. Registered as a default on PPID 60 and 66, but only 60 decodes: PPID 66 is an NGAP PDU inside a DTLS record, and there is no DTLS dissector. pycrate is deliberately excluded from the all extra – it is LGPL-2.1+ where this package is BSD-3-Clause, and lands some 238 MB to obtain one module.

  • Added – the Mobility Header registry, completed (#383, #437). The RFC 5568 fast-handover messages and options first, then all 24 registered message data types, 70 of the 71 registered options – with nested sub-option registries for the flow identification, access network identifier, quality-of-service and LMA-controlled MAG parameter families – and all 4 CGA extensions. Only the CGA Parameters option remains on the generic handler.

  • Added – dispatch entries for dissectors that existed but were reachable from no registry (#436): FTP-DATA on TCP 20, HTTP/1 on TCP 8080, HTTP on UDP 8080, L2TPv2 on UDP 1701 and OSPF at TransType 89. VLAN became an abstract base with C_Tag (802.1Q) and S_Tag (802.1ad) as concrete subclasses, so a Q-in-Q frame no longer collapses into one opaque Raw; L2TP likewise became a base, with L2TPv2 carrying the RFC 2661 implementation.

  • Addedpcapkit.utilities.logging as a real interface: get_logger() for per-module children, configure() to set level, handler, stream, format or propagation at runtime, reset() to return to library-neutral, and ensure_output(). Seventeen modules now log under their own __name__, so a consumer can silence pcapkit.foundation.registry while keeping pcapkit.foundation.extraction (#384).

  • Addedconflict on the reassembly data models: absolute, inclusive ranges where two fragments claimed the same span with different bytes, which was previously lost silently on both the IP (#482) and TCP (#443, #478) paths.

  • Added – an end-to-end test tier (#376), sample-capture generators so a fresh clone can rebuild every fixture (#340), a Dockerised engine benchmark covering every supported Python version (#410), and registry round-trip coverage that records the entries which cannot close the cycle rather than skipping them (#440, #504).

  • Changedpcapkit no longer configures logging at import. It installs a NullHandler and sets no level, so verbosity is inherited from the application instead of being seized by whichever library was imported second; the old stderr handler stays as the PCAPKIT_DEVMODE opt-in. Three consequences worth knowing: the previous behaviour is configure(logging.INFO, stream=sys.stderr); 38 registry and extractor info calls became debug, so those messages are invisible even at INFO; and the handler is no longer logger.handlers[0]. verbose= output stays on stdout and is not logging (#384).

  • Changed – each warning is reported once per channel, and pcapkit no longer inserts a simplefilter('ignore', ...) at the front of the process-global warnings.filters (#362–#364, #390). The application’s own filter therefore wins now, which is the point of the change and also the sharp edge in it: under -W error, or pytest’s filterwarnings = error, a pcapkit warning that used to be suppressed will raise. Suppress them deliberately with warnings.filterwarnings('ignore', category=BaseWarning). quiet=True now means no record at any level and no longer sets sys.tracebacklimit, and the pcapkit.utilities.warnings.DEVMODE re-export is gone – its canonical home is pcapkit.utilities.logging.

  • Changedlayer= and protocol= are honoured rather than inert. Both were read under the wrong names, so every value a caller passed was dropped into **kwargs and discarded; the CLI’s -L also now validates its argument instead of accepting anything. The packet context reaches the schema layer for the first time as well, so a field the wire elides can be resolved from its enclosing packet (#404). follow_tcp_stream dispatches on the engine type, where both branches of the old test were dead and the native adapter ran against every engine’s frames (#402).

  • Changed – two reassembly and flow-tracing defaults moved (#435), and both are visible to a caller. Datagram.completed widened from bool to a Completion enumeration (COMPLETE, PARTIAL, TIMEOUT); only COMPLETE is truthy, so if datagram.completed: is unaffected but datagram.completed == True no longer holds. TCP flow tracing is bidirectional by default, which merges each flow’s two halves and closes one only once both have FINed – 331 flows become 111 on the sample HTTP capture, the difference being single-frame stray tails; pass trace_bidirectional=False for the old behaviour. IP reassembly also gained the 60-second timeout [RFC 1122, RFC 8200], clocked off the capture’s own timestamps rather than the wall clock, tunable with reasm_timeout=; TCP reassembly gets no timeout by default. trace_analyse= is new, and reassembles each traced flow’s application layer.

  • Changed – conflicting TCP overlaps resolve first-write-wins, per RFC 9293 section 3.10, where they had silently resolved last-write-wins (#443, #478). A deliberate behaviour break, and a narrow one: a conforming retransmission carries identical bytes, so nothing changes for it. IP fragment reassembly keeps last-write-wins, because RFC 791 specifies the opposite resolution, and records the disagreement instead (#482).

  • ChangedProbe, CipherSuite and IntegritySuite are Info subclasses rather than typing.NamedTuple, and no NamedTuple remains in the package. They are Mappings now, so len() and iteration yield field names rather than values.

  • Changed – renames with no compatibility alias left behind: HoleDiscriptor is spelled HoleDescriptor and its package alias TCP_HoleDiscriptor is TCP_HoleDescriptor (#350); PCAP-NG Option subclasses spell the namespace class keyword ns= instead of namespace= (#439); and examples/sample and examples/samples – one letter apart, holding different things – are now examples/captures and examples/generators.

  • Changed – extraction is around 46% faster on a 1,117-frame HTTP capture, with byte-identical output (#420). A reassembled datagram’s payload is now analysed on first read rather than eagerly, which cuts IP reassembly’s own cost by 90.7% and TCP’s by 23.7% – IP reassembly submits a datagram for every frame, fragmented or not (#424). Flow tracing over the same capture went from 1416.6 ms to 744.0 ms, because the flow dumper had been handing each record to a Frame constructor that re-dissected the whole protocol stack to return bytes it had just been given; options are no longer parsed twice either (#427). All output compared byte-for-byte across the sample captures in each case.

  • Fixed – next-layer, option, chunk, block and parameter dispatch all read defaultdict registries, so a lookup miss inserted the key into class-level state shared by every later instance, after which a legitimate register_* call warned that the code was already registered. Every read now goes through a lookup that does not grow the table, and IPv4.__option__ and HIP.__parameter__ became inspectable class attributes rather than names assembled at call time (#426, #428, #429, #434). One break comes with it: a tuple-registered handler pair written to the documented OptionParser/OptionConstructor signature now works where it could previously never be called at all, and a pair written with an explicit leading self – the only shape that used to work – now does not.

  • Fixed – on Python 3.10 and older, no Schema subclass got its own _abc_impl: all of them fell through to collections.abc.Mapping’s, so a single isinstance or issubclass answer poisoned every later question about that class for the rest of the process. A terminating PCAP-NG EndRecord tested True as an IPv4Record (#439).

  • Fixed – construction, which was broken in several places at once: the generated typed __init__ was never installed, so __post_init__ did not run and a schema built from a subset of its fields could not be packed at all – UDP(srcport=53, dstport=5353) now packs (#430); IPv6 and Mobility Header option padding was wrong, leaving construction wholly broken (#398); HTTP.make called the versioned make unbound, so every real call raised TypeError (#452, #462); and IPv4._make_data returned the fragment offset in octets where the wire wants 8-octet units, and read data.options on a packet that has none (#494, #499).

  • Fixed – a truncated or under-declared area no longer parses “successfully”, and no longer wedges the process. The option and list loops could spin forever with no exception on a truncated area, reachable from untrusted input through HOPOPT, IPv6-Opts, MH, HIP and SCTP; each iteration must now advance the stream by at least one octet, and the error names the option, the offset and the octets remaining (#431, #432). Separately, wire-derived lengths in ipv6_opts, CALIPSO, MPL, REG_INFO and four HIP list callbacks underflowed below zero, which ListField’s own while length > 0 then turned into a silent empty list; they are floored and raise instead (#449, #456, #460, #463).

  • Fixed – field widths and units, each measured against the specification rather than inferred: HIP’s TRANSPORT_FORMAT_LIST, NAT_TRAVERSAL_MODE and ESP_TRANSFORM list entries are two octets, not one [RFC 7401, RFC 5770, RFC 7402] (#463, #472); the MN-ID option sizes from its subtype, not from identifier’s Python type (#448, #464, #467); IPv6-Route’s Hdr Ext Len is computed in 8-octet units on both sides [RFC 8200] (#487, #489); and the Fast Binding Update and Acknowledgment Lifetimes are plain seconds [RFC 5568], not RFC 6275’s four-second units (#502).

  • Fixed – stdlib exceptions leaking out where the library’s own were promised: a malformed IP field value raised a bare ValueError instead of FieldValueError (#465); a bool address was silently packed as 0.0.0.1 or 0.0.0.0, bool being an int subclass (#491, #500); and @prepare discarded extra arguments silently and treated a declared zero length as end of stream, which is now distinguished from a genuinely exhausted one and raises StreamEOFError (#454, #458).

  • Fixed – the engine adapters, which were quietly wrong rather than loud. The dpkt toolkit split TCP and IPv4 headers at their fixed struct size instead of their real length, so option octets overwrote payload in the sequence-indexed reassembly buffer; it also read an ipv6_frag.nh that dpkt does not have, and passed fragment offsets unscaled (#351, #370, #385, #395). scapy never loaded its layer registry, so that engine did not dissect at all (#409), and its IPv4 fragment offset reached reassembly unscaled (#483, #484). The four IPv6 adapters disagreed about whether the 8-octet Fragment header belongs to ihl, header and tl; per RFC 8200 section 4.5 it belongs to none of them, and all four now agree (#415, #424).

  • Fixed – PCAP and PCAP-NG output and parsing: bytes(frame) returned the next frame’s octets, files=True wrote names like Frame 1..json, and a PCAP-NG timestamp_epoch was shifted by the reading host’s timezone (#403); seven further parser defects (#341–#347, #371) and, on the write path, four more block-parsing ones (#388); BitField packed every named bit as set (#359, #374); the extension-header walk failed to advance past the last IPv6 extension header (#348, #373); and IPv6 fragment offsets went unscaled, with reassembly keyed on the flow label – optional, and routinely zero, so distinct datagrams collapsed together – rather than on the fragment identification (#389).

  • Fixed – TCP reassembly mixed absolute sequence numbers with buffer-relative slicing, so on any capture carrying a SYN with a realistic initial sequence number incomplete datagrams were dropped silently, and the completed=False branch of the public API was unreachable (#349, #376).

  • Fixed – constant lookups that rejected a value the registry defines. RouterAlert(0) is the only value RFC 2113 defines and the one IGMP, RSVP and MLD actually send, and it was discarded because the vendor crawler skipped a header row IANA’s CSV does not have; IPX Socket(0) is that protocol’s own default, so bytes(IPX(...)) crashed on its own defaults; and two FTP _missing_ overrides were plain methods rather than classmethods, so every unregistered value raised TypeError instead of extending the enumeration (#492, #503).

  • Fixedformat='text' raised AttributeError before writing anything, naming a dictdumper.Text that has never existed. It now points at Tree, as the 'txt' alias beside it already did.

  • Fixed – 45 places where a documentation page contradicted the code (#413), ambiguous cross-references and five autodoc signature failures (#416), and Extractor’s documented exception plus 40 phantom or stale Args: labels (#501).

Preceded by 1.5.0a1 (2026-09-15), 1.5.0b1 and 1.5.0b2 (both 2026-09-18) and 1.5.0b3 (2026-09-19), all published as prereleases and so resolved only by pip install --pre. 1.5.0b1 half-shipped: the tag, the GitHub release and the Conda deployments landed, but PyPI rejected the wheel because twine check found a Sphinx-only :mod: role in README.rst, which pyproject.toml declares as the dynamic long description. 1.5.0b2 is what reshipped it – the release workflow is version-driven, so an existing version cannot republish – and 1.5.0b3 followed the CI change that stops a TestPyPI outage from costing a release its wheels (#497, #498).