Follow TCP Stream¶
pcapkit.foundation.traceflow.tcp is the interface to trace
TCP flows from a series of packets and connections.
- class pcapkit.foundation.traceflow.tcp.TCP(fout, format, byteorder='little', nanosecond=False, bidirectional=True, analyse=False)[source]¶
Bases:
TraceFlowBase[tuple[_AT,int,_AT,int],Buffer[_AT],Index,Packet[_AT]],Generic[_AT]Trace TCP flows.
- Parameters:
byteorder (
Literal['little','big']) – output file byte ordernanosecond (
bool) – output nanosecond-resolution file flagbidirectional (
bool) – trace both halves of a conversation as one flowanalyse (
bool) – reassemble each flow’s application layer, so thatIndex.packetcan be read*args – Arbitrary positional arguments.
**kwargs – Arbitrary keyword arguments.
Note
A TCP connection has two halves, and by default they are traced as one flow – which is what “following a TCP stream” means everywhere else, and what this module’s own title claims to do. Keying a flow on (source, destination) instead put a client’s packets and the server’s replies in separate flows, separate labels and separate output files, leaving a caller to pair them up by inspecting the labels.
Two consequences of the change are worth knowing:
The reverse half of a conversation no longer produces a flow of its own, so a capture of n connections yields n flows rather than
2n, and one output file each rather than two.A teardown does not end a flow. Seeing a connection close is not the same as knowing nothing more will arrive on its endpoints: the four-way close of RFC 9293 Section 3.6 is FIN, ACK, FIN, ACK, so the acknowledgement that completes it follows the second FIN, and duplicates of that acknowledgement can follow in turn. A flow is therefore finalised only by proof that no more of it can come – a new connection’s SYN on the same endpoints, or the end of the capture (
finish()). What the teardown does is get recorded, which is how that SYN is told from the peer’s SYN-ACK; see_ended().Finalising a flow is also when its callbacks run, so they run against the whole conversation rather than a truncated one.
Pass
bidirectional=Falsefor the older per-direction behaviour: a flow is one direction, and closes on that direction’s FIN. That mode reproduces what flow tracing did before conversations became one flow, RST included – which is to say it ignores RST, as it always did.Note
With
analyse=Truea flow also carries its application layer:Index.packetholds the flow’s reassembled datagrams, each with its payload parsed on demand. The tracer does not reassemble the stream itself – it feedsTCP, which already implements RFC 815 and copes with the reordering and retransmission that a tracer concatenating payloads in capture order would silently corrupt.How many datagrams that is follows from the reassembler’s own notion of a datagram, which is per direction and per acknowledgement number: it buckets as
self._buffer[BUFID].ack[ACK]and emits one datagram per bucket. Since_make_segment()passes each segment’sackthrough untouched, a direction yields one datagram per distinct acknowledgement number it carried – not one datagram per direction. Three request/response round trips on one connection therefore give six datagrams, three each way, one per exchange:src=12345 ack=501 payload=b'req1' src=443 ack=105 payload=b'resp1' src=12345 ack=506 payload=b'req2' src=443 ack=109 payload=b'resp2' src=12345 ack=511 payload=b'req3' src=443 ack=113 payload=b'resp3'
That is deliberate rather than incidental: the acknowledgement number advances exactly when the peer has spoken, so bucketing on it splits a direction wherever the other end got a word in – one datagram per exchange. Merging a whole direction into one stream would instead hand the application parser every message it ever sent, concatenated, and have it read only the first.
Be precise about what that does and does not buy, though, because the boundary is the peer’s turn and not the application’s message boundary. Where an exchange is one request and one reply, the two coincide and each datagram’s payload is a single message that
Datagram.packetcan parse alone. Where a sender pipelines – several requests in flight before any reply – they all carry the same acknowledgement number, so they share a bucket and are concatenated after all. Measured: two requests sent back to back on one acknowledgement number come back as a single datagram carryingb'req1req2', whichtest_pipelined_sends_share_a_datagram_because_the_ack_never_moved()pins. So this narrows the concatenation to within one exchange rather than eliminating it, and an application parser handed a pipelined datagram still sees only the first message.A direction that carried one acknowledgement number throughout – a single request and its reply, which is what most of the unit tests exercise – does collapse to one datagram each way, which is where “one per direction” holds.
It is off by default because buffering every traced payload is a cost tracing does not otherwise pay, and tracing’s per-packet cost is something this package has deliberately driven down. Nothing is reassembled, parsed or retained unless it is asked for.
- dump(packet)[source]¶
Dump frame to output files.
- Parameters:
packet (
Packet[TypeVar(_AT, IPv4Address, IPv6Address)]) – a flow packet (trace.tcp.packet)
- make_bufid(packet)[source]¶
Derive the buffer ID a packet belongs to.
- Parameters:
packet (
Packet[TypeVar(_AT, IPv4Address, IPv6Address)]) – a flow packet (trace.tcp.packet)- Return type:
tuple[TypeVar(_AT, IPv4Address, IPv6Address),int,TypeVar(_AT, IPv4Address, IPv6Address),int]- Returns:
Buffer ID, i.e.
(address, port, address, port).
Tracing bidirectionally means both halves of a conversation have to land on the same key, so the two endpoints are ordered canonically – the lesser
(address, port)pair first – rather than as (source, destination). Sorting is what makes the key direction-independent: the client’sA→Band the server’sB→Aboth reduce tomin(A, B), max(A, B).The result stays a plain
tupleof the same shape, which is deliberate and not merely convenient: it is adictkey, and anInfocannot be one – inheritingcollections.abc.Mappingsets__hash__toNone.Note
Both endpoints of a connection are of the same address family, so the comparison never has to order an
IPv4Addressagainst anIPv6Address– which raisesTypeError.
- trace(packet, *, output=False)[source]¶
Trace packets.
- Overloads:
self, packet (Packet[_AT]), output (Literal[True]) → Dumper
self, packet (Packet[_AT]), output (Literal[False]) → str
- Parameters:
packet (
Packet[TypeVar(_AT, IPv4Address, IPv6Address)]) – a flow packet (trace.tcp.packet)output (
bool) – flag if has formatted dumper
- Returns:
If
outputisTrue, returns the initiatedDumperobject, which will dump data to the output file named after the flow label; otherwise, returns the flow label itself.
Notes
The flow label is formatted as following:
f'{packet.src}_{packet.srcport}-{packet.dst}_{info.dstport}-{packet.timestamp}'
It is built from the packet that opened the flow, not from the canonical buffer ID, so the label still names the initiator first and reads the way it always did. The reverse half of a bidirectional conversation joins that flow rather than minting a label of its own.
- finish()[source]¶
Finalise every flow still being traced.
The end of the capture is the second of the two things that can prove a bidirectional flow is over – the first being a new connection on the same endpoints. Draining the buffer here is what lets a flow keep the acknowledgement that completes its close and still have its callback fired, rather than having to choose between the two.
Idempotent: it drains the buffer, so a second call finds nothing to do.
- submit()[source]¶
Submit traced TCP flows.
- Return type:
- Returns:
Traced TCP flow (trace.tcp.index).
Note
This reports flows still being traced without finalising them, so that reading
TraceFlow.indexpart-way through a capture cannot disturb the tracing – popping a buffer there would strand the rest of its conversation in a second flow. Such a flow is therefore reported here but has not fired its callback;finish()is what does that, at the end of the capture.With
analyse=Truethat makes an open flow’sIndex.packeta snapshot, and a mid-capture one. TheDeferredresolves when it is first read and is then fixed in place, so it holds the flow as it stood at that read – not as it stood when this method returned, and not as it will stand once the conversation ends. Read it while the flow is open and the datagrams cover only the segments seen so far; segments arriving afterwards are reassembled into the buffer but cannot reach an already-resolved snapshot, and nothing on the returnedIndexdistinguishes one from the final result thatfinish()produces.This call’s own cache is not the cause and does not soften it: both
trace()andfinish()clear__cached__['submit'], so a later call rebuilds the tuple with freshDeferredobjects. It is the previously returnedIndex, if a caller kept one, that cannot catch up. For a result that is final, read afterfinish().
- __protocol_type__: Type[ProtocolBase] = <class 'pcapkit.protocols.transport.tcp.TCP'>¶
Protocol of current reassembly object.
Terminology¶
- trace.tcp.packet¶
Data structure for TCP flow tracing (
TraceFlow.dump) is as following:tract_dict = dict( protocol=data_link, # data link type from global header index=frame.info.number, # frame number frame=frame.info, # extracted frame info syn=tcp.flags.syn, # TCP synchronise (SYN) flag fin=tcp.flags.fin, # TCP finish (FIN) flag rst=tcp.flags.rst, # TCP reset (RST) flag seq=tcp.seq, # TCP sequence number ack=tcp.ack, # TCP acknowledgement number header=tcp.packet.header, # raw bytes type header payload=bytearray( tcp.packet.payload), # raw bytearray type payload src=ip.src, # source IP dst=ip.dst, # destination IP srcport=tcp.srcport, # TCP source port dstport=tcp.dstport, # TCP destination port timestamp=frame.info.time_epoch, # frame timestamp )
- trace.tcp.buffer¶
Data structure for internal buffering when performing flow tracing algorithms (
TraceFlow._buffer) is as following:(dict) buffer --> memory buffer for reassembly |--> (tuple) BUFID : (dict) | |--> ip.src | | |--> tcp.srcport | | |--> ip.dst | | |--> tcp.dstport | | |--> 'fpout' : (dictdumper.dumper.Dumper) output dumper object | |--> 'index': (list) list of frame index, both directions | | |--> (int) frame index | |--> 'label': (str) flow label generated from the packet | | that opened the flow | |--> 'origin': (tuple) (address, port) of the endpoint | | that opened the flow | |--> 'forward': (list) frame index sent by 'origin' | |--> 'reverse': (list) frame index sent to 'origin' | |--> 'fin': (set) endpoints seen to have sent a FIN | |--> 'reset': (bool) whether a RST has been seen | |--> 'reassembly': (Optional[TCP]) the flow's own | reassembler, or None when | analyse is off |--> (tuple) BUFID ...
When tracing bidirectionally – the default –
BUFIDorders the two endpoints canonically rather than as (source, destination), so both halves of a conversation reduce to the same key. It stays a plaintupleeither way, because it is adictkey and anInfocannot be one –collections.abc.Mappingsets its__hash__toNone.A teardown – a FIN from each endpoint, or a RST from either – is recorded in
finandresetbut does not finalise the flow. The four-way close of RFC 9293 Section 3.6 is FIN, ACK, FIN, ACK, so the acknowledgement that completes it arrives after the second FIN; finalising on that FIN would drop the ACK from the flow and let it open a fresh buffer under the sameBUFID, which a later connection reusing those endpoints would then merge into. The flow is finalised instead by proof that nothing more can arrive – a new connection’s SYN on the same endpoints, orTCP.finishat the end of the capture. Telling that SYN from the peer’s SYN-ACK is what the recorded teardown is for.- trace.tcp.index¶
Data structure for TCP flow tracing (element from
TraceFlow.indextuple) is as following:(tuple) index |--> (Info) data | |--> 'fpout' : (Optional[str]) output filename if exists | |--> 'index': (tuple) tuple of frame index, both directions, | | in capture order | | |--> (int) frame index | |--> 'label': (str) flow label generated from the packet that | | opened the flow | |--> 'forward': (tuple) frame index in the direction that | | opened the flow | |--> 'reverse': (tuple) frame index the other way; empty when | | tracing unidirectionally | |--> 'packet': (Optional[tuple]) one reassembled datagram per | direction, or None when analyse is off |--> (Info) data ...
forwardandreversepartitionindex, soframe_number in flow.forwardanswers which way a packet went without taking the label apart.forwardis the direction of the packet that opened the flow, whose endpoints the label names first.packetis the conversation’s application layer: one reassembled datagram per direction, present only when the tracer was constructed withanalyse=True. It is reassembled on the first read, and each datagram’s ownpacketis parsed later still, so a caller that wanted only frame numbers pays for neither. The tracer does not reassemble the stream itself – it feedsTCP, whose RFC 815 algorithm handles the reordering and retransmission that concatenating payloads in capture order would corrupt.
Data Structures¶
- class pcapkit.foundation.traceflow.data.tcp.Packet(*args: VT, **kwargs: VT)[source]¶
-
Data structure for TCP flow tracing.
See also
pcapkit.foundation.traceflow.TraceFlow.dump()
- rst: bool¶
TCP reset (RST) flag. A connection can end abruptly as well as politely (RFC 9293 Section 3.5.2), and the tracer cannot notice that unless the flag reaches it – which it did not, so a reset connection used to look merely idle and a later connection reusing the same endpoints merged into it.
- pcapkit.foundation.traceflow.data.tcp.BufferID¶
Buffer ID, i.e.
(address, port, address, port).A plain
tuplerather than anInfodeliberately:Infoinheritscollections.abc.Mapping, which sets__hash__ = None, so anInfocannot be adictkey at all.When tracing bidirectionally – the default – the two endpoints are ordered canonically rather than as (source, destination), so that both halves of one conversation produce the same key; see
TCP.make_bufid. The shape is unchanged either way.
- class pcapkit.foundation.traceflow.data.tcp.Buffer(*args: VT, **kwargs: VT)[source]¶
-
Data structure for TCP flow tracing.
See also
pcapkit.foundation.traceflow.TraceFlow.index
- index: list[int]¶
List of frame index, both directions, in capture order. This is the authoritative ordering;
forwardandreverseare subsequences of it.
- origin: tuple[_AT, int]¶
(address, port)of the endpoint whose packet opened this flow. It defines what “forward” means for the flow, and it is the endpoint thelabelnames first.
- reverse: list[int]¶
List of frame index sent to
origin, in capture order. Always empty when tracing unidirectionally, since the reverse half of the conversation is then a flow of its own.
- fin: set[tuple[_AT, int]]¶
Endpoints observed to have sent a TCP FIN. A bidirectional flow is a whole connection, and a connection closes only once both halves have finished (RFC 9293 Section 3.6), so the set has to be tracked rather than a single flag: submitting on the first FIN would cut the peer’s FIN and the final acknowledgement out of the flow.
- reset: bool¶
Whether a TCP RST has been seen on this flow. A reset ends the connection at once (RFC 9293 Section 3.5.2), where a polite close needs a FIN from each side, so it is tracked as a flag rather than per endpoint.
- class pcapkit.foundation.traceflow.data.tcp.Index(*args: VT, **kwargs: VT)[source]¶
Bases:
DeferredPacket,InfoData structure for TCP flow tracing.
See also
element from
pcapkit.foundation.traceflow.TraceFlow.indextuple
- forward: tuple[int, ...]¶
Frame index of the packets travelling in the direction that opened the flow, in capture order. That endpoint is the one the
labelnames first, soframe_number in index.forwardanswers “which way did this packet go” without having to take the label apart.
- reverse: tuple[int, ...]¶
Frame index of the packets travelling the other way, in capture order. Empty when tracing unidirectionally, in which case
forward==index.
- packet: tuple[Datagram, ...] | None¶
one reassembled datagram per direction, or
Nonewhen the tracer was not asked for it (analyse=False, the default).Reassembled on the first read, not when the flow is finalised, and each datagram’s own
packetis parsed later still – two layers of the same postponement, so a caller that only wanted frame numbers pays for neither.- Type:
The conversation’s application layer
Type Variables¶
- pcapkit.foundation.traceflow.data.tcp._AT: ipaddress.IPv4Address | ipaddress.IPv6Address¶