tftp
A pure-Python TFTP client, server, relay and capture decoder for IPv4 and
IPv6 that implements the whole of modern TFTP: RFC 1350, the option
extension (RFC 2347) with blksize, timeout, tsize and windowsize
(RFC 2348, 2349, 7440), tftp-hpa's extensions, block-number rollover and
netascii. Built as infrastructure for network boot: it interoperates with
iPXE, tftp-hpa, BusyBox, dnsmasq and curl, serves files, HTTP or another TFTP
server, and shows you what crossed the wire. The documentation is at
https://jose-pr.github.io/pytftp/.
import tftp
tftp.TFTPClient("192.0.2.1").download("pxelinux.0", "pxelinux.0")
with tftp.TFTPServer("/srv/tftp") as server: # IPv6 + IPv4, port 69
server.serve_forever()
Features
- Complete option support —
blksizeup to 65464, RFC 7440 windows,tsizeboth ways,timeout, tftp-hpa'sutimeout,rollover,blksize2,cookie, and Windows bootmgr'smstfwindow. Unlimited file size. A registry for your own options, and profiles (pxe,hpa,legacy,strict) for quirky peers. - Fast — windowed transfers move hundreds of MiB/s over loopback in pure Python (see benchmarks); the hot path reads into preallocated buffers and keeps a sent window in memory instead of re-reading.
- Correct under loss — Sorcerer's Apprentice fix (with a window, one duplicated, lost or late ACK costs at most one window of DATA, never every window after it), exponential backoff, gap detection inside a window, repeated OACKs tolerated, ERROR 5 for stray packets without disturbing the transfer, dallying on the last ACK. The engine is tested over a simulated lossy link and fuzzed.
- IPv4 and IPv6 — listens on every address of both families by default;
listen=names addresses, adapters or MACs instead. - Replies from the request's address — on a multi-homed host or a VIP, each transfer answers from the address the request was sent to (pktinfo, via netimps); tested on Linux and Windows.
- Safe file serving — no path escapes
root(.., symlinks, Windows device names), uploads are atomic (a failed upload leaves nothing), quotas and disk-space checks fromtsize. - Pluggable backends — a handler is two methods, so boot menus or images can be generated per client; built in: a directory, memory, an HTTP(S) gateway, and a terminating proxy to another TFTP server. Slow backends apply backpressure instead of blocking other transfers.
- Transparent relay — forwards requests byte for byte to upstream servers (routed by subnet, filename or interface), so unknown extensions still work; blocking or on asyncio, with coroutine routes.
- Capture and debugging — trace every datagram from the client, server or
relay; write Wireshark-readable pcap or pcapng; read pcap/pcapng files or a
live
tcpdumppipe (both through pktcap, thepktcapextra), reconstruct each transfer and extract its file, or ask a server for the transfers a capture holds again (pytftp replay: reads only, unless told to upload too). - Directory listings — an opt-in extension between pytftp peers:
pytftp ls,TFTPClient.listdir(), anditerdir/walk/globon paths; other servers simply ignore it. - Deployment — transfer ports pinned to a range for firewalls; per-client
roots, case-insensitive names and filename remapping, as handlers
(
tftp.backends) and aspytftp serveflags. - asyncio —
AsyncTFTPClientandAsyncTFTPServerwith async handlers and streams. - pathlib —
client.path("boot/x").read_bytes(), andtftp://URLs in pathlib-next, soUriPath("tftp://h/x").copy("s3://bucket/x")just works (optional extra). - Bounded — limits on requests, sessions (500 by default), sessions per client, idle time (60 s by default), window memory and transfer time; counters for metrics.
- CLI —
pytftp get|put|ls|serve|relay|capture|replaywith--json,--traceand--pcap(optional extra).
Installation
pip install tftp # library
pip install "tftp[cli]" # plus the pytftp command
pip install "tftp[cli,pktcap]" # plus pytftp capture and replay, and --pcap
| Extra | Adds | Needed for |
|---|---|---|
cli |
duho |
the pytftp command and python -m tftp |
path |
pathlib-next[uri] |
TFTPPath, and tftp:// URLs in pathlib_next.uri.UriPath |
pktcap |
pktcap |
reading, writing and replaying captures: pytftp capture, pytftp replay, --pcap, tftp.capture.analyze of a file |
Requires Python 3.9+. The one required dependency is
netimps, which has none of its own.
Quick start
Client:
import tftp
client = tftp.TFTPClient("boot.example.net", blksize=1428, windowsize=16)
result = client.download("images/vmlinuz", "vmlinuz")
print(result.bytes, result.duration, result.negotiated)
data = client.get("pxelinux.cfg/default") # into memory
client.upload("logs/boot.txt", b"ok\n") # needs a writable server
A tftp:// URL names a file, and may carry transfer options in either
spelling: ?name=value&name=value, or ;name=value;name=value (RFC 3617's own
;mode=netascii is the second). Options in a URL are this library's extension:
another tool reads the text after ? as part of the file name.
import tftp
tftp.download_url("tftp://boot.example.net/images/vmlinuz?blksize=1428&windowsize=16", "vmlinuz")
tftp.download_url("tftp://boot.example.net/images/vmlinuz;blksize=1428;windowsize=16", "vmlinuz")
tftp.download_url("tftp://boot.example.net/images/vmlinuz;blksize=1428", "vmlinuz", blksize=512) # 512 wins
Server with uploads, a bounded window and a completion hook:
import tftp
server = tftp.TFTPServer(
"/srv/tftp",
port=69,
writable=True,
options=tftp.TFTPServerOptions(max_blksize=8192, max_windowsize=32),
on_complete=lambda r: print(r.operation, r.filename, r.peer[0], r.error or "ok"),
)
server.serve_forever()
Generated content:
import io
import tftp
class Menu(tftp.FilesystemBackend):
def open_read(self, context):
if context.filename == "menu.cfg":
return io.BytesIO(b"client %s\n" % context.peer[0].encode())
return super().open_read(context)
tftp.TFTPServer(Menu("/srv/tftp")).serve_forever()
Relay, capture and asyncio (the commands are under "Command line"):
import tftp
from tftp.relay import TFTPRelay, RouteTable, by_subnet
import pktcap # the pktcap extra
from tftp.capture import analyze, trace_to
# Forward requests to per-subnet boot servers, recording everything.
with pktcap.PcapWriter("relay.pcap") as pcap:
route = RouteTable([by_subnet({"10.1.0.0/16": "10.1.0.5"})], default="10.0.0.20")
TFTPRelay(route, trace=trace_to(pcap)).serve_forever()
# What happened in a capture (yours, or tcpdump's)?
for transfer in analyze("boot.pcapng").transfers:
print(transfer, transfer.missing_blocks)
# asyncio
from tftp.client import AsyncTFTPClient
async def fetch():
async for chunk in AsyncTFTPClient("192.0.2.1").stream("vmlinuz"):
...
Command line
pip install "tftp[cli]" installs the pytftp command; python -m tftp is the same.
get, put and ls are the client, serve and relay the servers, capture and
replay read a capture. The commands, every option and the output formats are in
the command-line guide.
pytftp get 192.0.2.1 pxelinux.0 -b 1428 -w 16
pytftp put 192.0.2.1 firmware.bin
pytftp serve /srv/tftp --port 6969 --write --json
pytftp serve /srv/tftp --write --max-upload 1000000 --max-duration 600
pytftp serve /srv/tftp --per-client-only
A command exits 0 on success, 1 when the transfer, the peer or a local file
failed (one error: line on stderr) and 2 when the invocation was wrong. A
result goes to stdout (get and put print one line, which -q silences; on
stderr when get HOST FILE - sends the file to stdout) and --json prints one
JSON object per line instead. --per-client serves ROOT/<client address>/ to
a client that has one and ROOT to the rest, other clients' directories
included: it is not isolation, and --per-client-only refuses a client with no
directory. --max-upload BYTES and --max-duration SECONDS bound an upload and
a transfer. pytftp --help shows each option's default, and AGENT_HELP=1 pytftp --help prints the command tree as JSON. A settings file keeps a command's options out of the
command line: pytftp -c FILE serve (or PYTFTP_CONFIG, or the user's own pytftp/pytftp.ini) reads an
INI, JSON, TOML or YAML file whose top-level keys are the root's options and whose table [serve] holds
serve's, the argument over the file; a key nothing has is refused, not ignored, and an empty path (--config "") means no file.
pytftp serve --http https://images.example.com/pxe/ --compat pxe
pytftp relay 10.0.0.20 --route-prefix windows/=wds.lan --trace
tcpdump -i eth0 -U -w - udp | pytftp capture --input - --filter "op=RRQ,ERROR"
pytftp replay --input boot.pcapng --to 192.0.2.1 --speed 10
pktcap's own commands read TFTP too once they are told to load it: tftp.capture is a pktcap plugin
that adds the filter keys op, file, block, code and session. Only the datagrams to or from the
request port carry the layer until follow_transfers has seen them, so a transfer is followed with
pytftp capture, which prints pktcap's text listing of TFTPLayer.summary() by default.
PKTCAP_LOAD=tftp.capture pktcap convert -i boot.pcapng -f "op=RRQ and file=*.efi"
API overview
| Module | Purpose |
|---|---|
tftp.client |
TFTPClient, AsyncTFTPClient, download, upload |
tftp.server |
TFTPServer, AsyncTFTPServer, TFTPServerLimits, AtomicWriter, TFTPRequestContext |
tftp.backends |
FilesystemBackend, MemoryBackend, HTTPBackend, UpstreamBackend (proxy), Pipe |
tftp.relay |
TFTPRelay, AsyncTFTPRelay and routing helpers |
tftp.capture |
trace events, trace_to, FlowTracker, analyze, replay_transfers, and TFTP as a pktcap layer (TFTPLayer, register_tftp_dissector, pktcap_plugin, follow_transfers); reading and writing a capture are pktcap's |
tftp.options |
TFTPServerOptions, option registry, profiles, Negotiated |
tftp.packet |
TFTPOpcode, TFTPErrorCode, packet types, encode_*/decode |
tftp.transfer |
I/O-free Sender/Receiver/Requester engine |
tftp.listing |
the x-list directory listing format: loads, dumps, DirectoryListing |
tftp.path |
TFTPPath, TFTPURIPath (path extra) |
tftp.netascii |
streaming netascii translation |
tftp.exceptions |
every exception: TFTPError, the typed RemoteError subclasses, TFTPValueError and its decode subclass |
tftp.cli |
the pytftp command (cli extra) |
tftp itself exports what the common task needs (the clients and servers and
their asyncio twins, download and upload, the exceptions, TransferResult,
TFTPURL, the handler contract, the policy classes, the packet types and the
engine's Sender and Receiver); every other name is imported from the module
above that owns it. The complete reference, with every signature and gotcha and
the module each name lives in, is src/tftp/AGENTS.md
and the headers beside the code it lists; they ship inside the package.
Differences from tftp-hpa
The reference for the options only an implementation defines (blksize2,
utimeout, rollover, cookie) is tftp-hpa 5.3's server. The suite
replays what it answered to 22 requests, recorded on Linux by
tests/conformance/record.py and stored under tests/conformance/cases/, so a
run on any system, with no peer installed, checks them. Fidelity is to results:
which options a server answers and with what values, the error codes, the block
numbers and the octets of each block; message texts and the ports a reply comes
from are not compared. The library is tested against tftp-hpa 5.3 (server and
client), BusyBox 1.37, dnsmasq 2.92, curl and iPXE where those can run.
It differs on purpose in these cases, each asserted by the suite as this library behaves:
| Case | Difference |
|---|---|
blksize2-below-the-minimum |
blksize2 below 8 octets: tftp-hpa answers ERROR 8; this library leaves the option out and runs at 512, which RFC 2347 allows for a value a server cannot use. |
blksize2-beside-a-smaller-blksize |
blksize and blksize2 together: tftp-hpa answers both, even with different sizes, and sends blocks of the blksize2 one; this library answers blksize alone, one block size to a transfer. |
blksize2-beside-an-odd-blksize |
blksize and blksize2 of 1000: tftp-hpa answers blksize 1000 and blksize2 512 and sends blocks of 512; this library answers blksize 1000 alone and sends blocks of 1000. |
blksize2-beside-the-same-blksize |
blksize and blksize2 of 1024: tftp-hpa answers both; this library answers blksize alone. |
cookie-is-echoed |
cookie on a read: tftp-hpa 5.3 does not know the option and leaves it out; this library, with it allowed, answers it unchanged, as tftp-hpa's development source does. |
cookie-on-a-write |
cookie on a write: tftp-hpa 5.3 answers ACK 0; this library, with the option allowed, answers an OACK that carries it unchanged. |
cookie-of-300-octets |
A cookie of 300 octets: tftp-hpa 5.3 leaves it out; this library refuses the request with ERROR 4, an option value being limited to 255 characters (max_option_length). |
rollover-not-zero-or-one |
rollover of 2: tftp-hpa 5.3 answers it as asked; this library leaves out a value that is not 0 or 1. |
tsize-of-an-empty-file |
tsize asked of an empty file: tftp-hpa answers tsize 0; this library leaves it out, since curl rejects tsize 0 in an OACK. |
utimeout-above-the-maximum |
utimeout over 255 seconds: tftp-hpa answers ERROR 8; this library leaves the option out and uses the default time. |
utimeout-below-the-minimum |
utimeout of 1 microsecond: tftp-hpa answers ERROR 8; this library leaves the option out and uses the default time. |
utimeout-beside-a-different-timeout |
timeout beside a utimeout of another time: tftp-hpa 5.3 answers both; this library answers utimeout alone, as tftp-hpa's development source does. |
mstfwindow-a-fixed-window |
mstfwindow of 31416, the offer of Windows' boot manager: no recording exists, as that boot manager is the reference and no host here can run it; this library answers 27182 and keeps the window at 4, the one fact Serva documents. |
Development
py -3.14 -m venv .venv/3.14-nt-arm64 # or python3.14 -m venv .venv/3.14-posix-x86_64
.venv/3.14-nt-arm64/Scripts/pip install -e ".[dev]"
.venv/3.14-nt-arm64/Scripts/python -m pytest -q
.venv/3.14-nt-arm64/Scripts/python -m black --check src tests benchmarks examples
python benchmarks/run.py
The suite, its markers, the peers it uses when they are installed and the traps of running it are in tests/AGENTS.md; the layout and the conventions are in AGENTS.md.
Releasing
This project follows Semantic Versioning and keeps a
CHANGELOG.md. Pushing a tag matching v* triggers the release
workflow: test gate → build (checking the tag names the version built) → a strict
docs build as a gate → GitHub release → publish. The release workflow never deploys
the docs site itself: for a final release its last job dispatches the docs workflow
at the tag, which owns every Pages deploy.
License
MIT — see LICENSE.
Metadata
Release files for tftp 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tftp-0.1.1.tar.gz | 484.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tftp-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 757.2 kB
Release files / tftp-0.1.1.tar.gz
| Download URL | tftp-0.1.1.tar.gz |
|---|---|
| Size | 484.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
35ea03813ff809578f28dcd887ac8632e72681bff35e7b16d0cb01f345bfcacb
|
|
BLAKE2b-256 checksum How to use checksums |
e449247c1c642a0ec7f83835423d8f5d973e1639144b2ee0e4011dbca27c2df3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 14, 2026.
Transparency logRelease files / tftp-0.1.1-py3-none-any.whl
| Download URL | tftp-0.1.1-py3-none-any.whl |
|---|---|
| Size | 272.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0968005b8f6cd9ac75dc9704573b2e941a82045713135dcb77a1e55739ca942e
|
|
BLAKE2b-256 checksum How to use checksums |
c0eae3c92a23237399616e202449250e0ec28db5d9de19e9b7c600d5f5a2e428
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 14, 2026.
Transparency log