tftp.capture
tftp.capture
Seeing TFTP on the wire: packet events, trace hooks, filters, captures.
Three sources of :class:PacketEvent:
trace=hooks onTFTPClient,TFTPServerandTFTPRelay-- what this library sent and received, with session ids;- pcap/pcapng captures, from a file or a live
tcpdump -w -pipe, read by pktcap (pktcap.read_datagrams) and followed by :class:FlowTracker, or by :func:analyzein one call; - live capture on Linux (
pktcap.sniff).
:func:dissect_tftp is TFTP as a pktcap layer (:class:TFTPLayer), registered by
:func:register_tftp_dissector and by nothing else; :func:pktcap_plugin is the hook that
lets pktcap load it by name, with the filter keys op, file, block, code and
session. :func:follow_transfers gives a transfer's datagrams, which run between ports
chosen for it, the layer and the transfer's session.
:func:replay_transfers asks a server the caller names for each transfer a capture holds again.
:func:trace_to turns a pktcap writer (pktcap.PcapWriter,
pktcap.PcapngWriter) into a trace hook, so a run is recorded for Wireshark, and
:func:combine_hooks joins several trace hooks into the one trace= takes.
Analysis
Bases: NamedTuple
The TFTP in a capture: its events and every transfer seen.
CapturedTransfer(session, time, client, server, request)
One transfer reconstructed from a capture.
:ivar session: id shared with the transfer's :class:PacketEvent objects.
:ivar client, server: the request's source, and its destination.
:ivar server_tid: the server's transfer address, once it answered.
:ivar operation: "read" (RRQ) or "write" (WRQ).
:ivar requested, acknowledged: the request's options, the OACK's.
:ivar blksize, windowsize: what the transfer ran with.
:ivar error: (code, message, sent_by) of an ERROR, or None.
:ivar is_complete: the final DATA was seen and acknowledged.
:ivar retransmissions: DATA packets seen more than once.
:ivar missing_blocks: (first, last) ranges (inclusive) of the blocks never
seen, up to the highest one placed; :attr:missing_count counts them.
Source code in src/tftp/capture/_flows.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
missing_blocks
property
(first, last) ranges of the logical block numbers never seen whole, up to the highest one seen.
missing_count
property
How many blocks :attr:missing_blocks names.
add_data(wire, payload, keep=True, cut=False)
Account for one DATA: always its size, and its payload when keep.
A block further ahead of the highest one seen than a window can be is not
placed: it is counted as a packet and nothing else. A DATA a snap length cut is
not placed either: its block is missing until a whole one arrives, and its short
payload never ends the transfer.
Source code in src/tftp/capture/_flows.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
data(decode_netascii=True)
The transferred bytes, in order; a gap ends what can be returned.
Netascii transfers are decoded to local form unless
decode_netascii=False. Empty when the tracker did not keep payloads.
Source code in src/tftp/capture/_flows.py
221 222 223 224 225 226 227 228 229 230 231 232 233 | |
write_to(directory)
Write the transferred bytes into directory and return the path, or None if there is nothing.
The file is named <session>-<name>, with .partial added when the
transfer is not complete or has gaps. <name> is the last component of
the requested name with every character outside A-Za-z0-9_.- replaced
by _ and at most 100 octets long, so a capture cannot name a file
outside directory. The directory is created when it does not exist.
Without kept payloads (keep_payloads=False) there is nothing to write.
Source code in src/tftp/capture/_flows.py
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
DatagramLike
Bases: Protocol
What :meth:FlowTracker.feed reads of a datagram.
Required: time, source, destination and payload.
Optional member, read once per datagram when present:
truncated--Truewhenpayloadis shorter than the datagram was because a snap length cut the capture. Such a datagram is counted as a packet of its transfer but never read as a DATA: a short payload is not the final block, and its block is reported missing. Without the member a datagram is whole.
pktcap.CapturedDatagram has all five.
DatagramWriter
Bases: Protocol
What :func:trace_to needs of a writer: pktcap.PcapWriter and pktcap.PcapngWriter have it.
write(time, source, destination, payload)
Append one UDP datagram: seconds since the epoch, both (host, port) addresses, its octets.
Source code in src/tftp/capture/_hook.py
79 80 81 82 83 | |
FlowTracker(ports=(69,), *, keep_payloads=True, on_complete=None, max_tracked=1024)
Assigns datagrams to transfers and reconstructs them.
:param ports: request ports (where RRQ/WRQ are sent).
:param keep_payloads: keep DATA payloads (for :meth:CapturedTransfer.data).
Turn off for long captures where only metadata matters: sizes,
retransmissions and missing blocks are counted either way.
:param on_complete: on_complete(transfer) for each transfer the tracker
stops following (see max_tracked).
:param max_tracked: at most this many transfers are followed; None
follows every one, as a whole file's analysis wants. When a request would
make one more, the transfer that is finished (complete or ended by an
ERROR) and quietest goes first, else the quietest one: it is handed to
on_complete and dropped, so a live capture holds a bounded number of
transfers however long it runs. A datagram of a dropped transfer is no
longer attributed to it.
:raises ValueError: max_tracked below 1.
Source code in src/tftp/capture/_flows.py
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | |
feed(datagram)
Account for one datagram; its event if it is TFTP, else None.
Source code in src/tftp/capture/_flows.py
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | |
PacketEvent
Bases: NamedTuple
A datagram seen by a client, server, relay or capture.
:ivar time: wall-clock seconds (time.time()).
:ivar direction: "in" (received) or "out" (sent), from role's
point of view; "seen" for a passive capture.
:ivar local: the observer's address for this datagram (for a capture,
the destination).
:ivar remote: the other end (for a capture, the source).
:ivar data: the datagram itself.
:ivar role: "client", "server", "relay" or "capture".
:ivar session: id correlating one transfer's events, or None for
datagrams outside any transfer (requests that were refused...).
:ivar leg: for a relay, "client" or "upstream": which side of it.
destination
property
Who it was sent to.
source
property
Who sent it.
__str__()
A human line: 12:00:00.123456 [t3] 10.0.0.5:2000 > 10.0.0.1:69 RRQ 'f' octet.
Source code in src/tftp/capture/_events.py
139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
decode()
The parsed packet; raises :class:TFTPDecodeError.
Source code in src/tftp/capture/_events.py
121 122 123 | |
to_dict(payload=False)
JSON-ready metadata; DATA payloads only with payload=True (hex).
Source code in src/tftp/capture/_events.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
ReplayedTransfers
Bases: NamedTuple
What :func:replay_transfers did: the transfers it ran, and how many it did not.
TFTPLayer
Bases: NamedTuple
The TFTP packet a datagram carries, as plain values.
opcode is the packet's name ("RRQ", "WRQ", "DATA", "ACK", "ERROR",
"OACK"). Every other field is None where that kind of packet has none: block
(DATA, ACK), filename and mode (RRQ, WRQ, mode lower-cased), options (RRQ,
WRQ, OACK: (name, value) pairs, names lower-cased, in the order sent), code and
message (ERROR). session is the id of the transfer the datagram belongs to, None
for a layer read from the octets alone: :func:follow_transfers sets it. Equal to another
TFTPLayer with the same fields and to nothing else.
summary()
One line saying what the packet is, for pktcap's text output: [c1] RRQ 'boot.efi' octet.
The transfer's id leads when it is known. The file name, the mode, the options and an ERROR's
message are the peer's text, shown as it came: pktcap escapes the line. A field a hand-built
layer lacks reads as None; nothing here raises.
Source code in src/tftp/capture/_dissector.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
analyze(source, *, ports=(69,), keep_payloads=True)
Read a whole capture (path, stream, or datagrams) and reconstruct its transfers.
A path or a stream is read by pktcap.read_datagrams, which raises
pktcap.CaptureFormatError for a file that is not a capture. Every
transfer is kept: the result holds what the capture holds.
Source code in src/tftp/capture/_analysis.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
combine_hooks(*hooks)
One trace hook that calls each of hooks in order; None entries are skipped.
Returns None when no hook is left, and the hook itself when one is. A
hook that raises does not keep the others from seeing the event: the first
exception is raised once all have been called, for the guard around the
combined hook to count.
Source code in src/tftp/capture/_hook.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
dissect_tftp(data)
Read the TFTP packet data holds: its :class:TFTPLayer, and a DATA's octets as the payload.
The payload of every other packet is empty, and nothing follows a TFTP packet. Raises
pktcap.DissectError (a ValueError) for octets that are not a TFTP packet; its text
holds none of them. Follows the contract pktcap.check_dissector checks.
Source code in src/tftp/capture/_dissector.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
follow_transfers(frames, tracker)
frames with a :class:TFTPLayer on each datagram tracker attributes to a transfer.
pktcap dissects TFTP by port, so only a datagram to or from the request port has a layer; a
transfer's DATA, ACK and OACK run between ports chosen for it. Every UDP datagram is fed to
tracker, and one it attributes to a transfer is yielded with its layer, whose session is
the transfer's: a layer the dissector already made is replaced, otherwise the layer is added
after the UDP layer and the octets that follow it (a DATA's) become the frame's payload. A frame
with no UDP datagram, one the tracker does not attribute, one a snap length cut (the tracker
counts it) and one whose octets are not a TFTP packet are yielded as they are. The result is
lazy and keeps the order; nothing is read from frames before it is iterated, and closing the
result closes frames when it can be closed. ImportError when pktcap is not installed.
Source code in src/tftp/capture/_follow.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
new_session_id(prefix='t')
A short id, unique in this process, for correlating a transfer's events.
Source code in src/tftp/capture/_events.py
20 21 22 | |
pktcap_plugin(registry)
Declare :class:TFTPLayer and its filter keys in registry, then register the dissector.
The keys are op, file, block, code and session (the transfer's id, as
:func:follow_transfers sets it, compared exactly), and TFTPLayer fields as tftp.FIELD.
A call that raises leaves registry as it found it: ValueError for a layer name or a
port that is taken.
Source code in src/tftp/capture/_plugin.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
register_tftp_dissector(registry=None, *, ports=(69,))
Have registry (default: pktcap's process-wide one) dissect the datagrams to ports.
Registers :func:dissect_tftp under ("udp", port) for each port. Nothing registers on
import and nothing outside this call does. Only the request port is a selector: a transfer's
DATA and ACK run between ports chosen per transfer, which :class:FlowTracker follows.
ValueError for a port that is not 1 to 65535 or that already has a dissector (pktcap's
rule), in which case nothing is registered by this call; TypeError for a port that is
not an int.
Source code in src/tftp/capture/_dissector.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
replay_transfers(source, host, port=69, *, ports=(69,), writes=False, speed=1.0, max_delay=5.0, limit=None, timeout=1.0, retries=5)
Ask the server at host and port for each transfer the capture source holds again.
source is what :func:analyze takes; ports are its request ports. Transfers run one at
a time in the order their requests were seen, each after the wait pktcap.replay_schedule
gives (speed, max_delay and limit mean what they mean there: speed=None removes
the waits, max_delay is the longest single one, limit ends the replay after that many
transfers and the rest are neither run nor counted). Each is requested by a client built from
what the capture's client asked for: the mode, the options (timeout is the argument's, not
the captured value), and the file name.
Nothing is sent to an address the capture holds: host and port are the only
destination. A read is asked for again and its octets are discarded; a write is replayed only
with writes=True, and it uploads the octets the capture holds, which overwrites a file on
the server. A write the capture holds only partly (a missing block, an ERROR, no final
acknowledgement) is never replayed. A transfer that is not replayed, or that the client refuses
(a mode it does not send, an option it does not accept), is counted in skipped.
A transfer that fails (an ERROR from the server, a timeout) is a :class:~tftp.TransferResult
with error set and the replay goes on. ValueError for an argument out of range;
pktcap.CaptureFormatError for a file that is not a capture; OSError when host does
not resolve or a socket fails.
Source code in src/tftp/capture/_replay.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
summarize(data)
A one-line description of a TFTP datagram (never raises).
Text the peer chose (the mode, the options) is escaped, and a file name or an
ERROR message is shown as repr shows it, so the line holds no control
character.
Source code in src/tftp/capture/_events.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
trace_to(writer)
A trace hook that appends every datagram it is given to writer.
writer stays the caller's to close. A write that raises (a full disk, a
port or a time the format cannot hold) is the hook's failure: the transfer
goes on and the first one is logged. ImportError when pktcap is not installed.
Source code in src/tftp/capture/_hook.py
86 87 88 89 90 91 92 93 94 95 96 97 98 | |