Skip to content

Backends

Besides a directory (FilesystemBackend), tftp.backends has:

Memory

from tftp.backends import MemoryBackend

files = MemoryBackend({"pxelinux.cfg/default": b"DEFAULT linux\n"}, writable=True)
tftp.TFTPServer(files).serve_forever()       # uploads land in files.files when complete

Uploads are bounded: max_upload (16 MiB by default) is the most one upload may hold and max_entries (1024, the names already in files included) the most names; a client that crosses either gets ERROR 3, and a name that exists can still be replaced. Pass None to remove a bound.

HTTP(S) gateway

Boot ROMs speak TFTP; images often live behind HTTP.

from tftp.backends import HTTPBackend

tftp.TFTPServer(HTTPBackend("https://images.example.com/pxe/")).serve_forever()

boot/vmlinuz is fetched from https://images.example.com/pxe/boot/vmlinuz and streamed: Content-Length answers tsize, a 404 becomes ERROR 1, a 403 ERROR 2. With writable=True uploads become HTTP PUTs, and the client's last block is acknowledged only once the HTTP server has answered. Pass url_for= to route by client or filename, headers= for authentication, or an opener for proxies and TLS settings. Only the standard library is used.

The gateway fetches http and https only: a base_url of another scheme is a ValueError, and the default opener refuses a redirect to ftp:, file: or data: (an opener of your own is yours to restrict or widen). An upload's PUT carries the announced size, so a client that sends more or fewer octets than it announced fails with an ERROR and the origin stores nothing. Run a gateway that anonymous peers can reach with max_sessions set: each request costs the origin one connection.

Another TFTP server (terminating proxy)

from tftp.backends import UpstreamBackend

proxy = UpstreamBackend("10.0.0.20", client_options={"blksize": 8192, "windowsize": 16})
tftp.TFTPServer(proxy, options=tftp.Profile.LEGACY.server).serve_forever()

Each side negotiates on its own, and they are joined by a bounded buffer: a slow client slows the upstream rather than the proxy holding the file. The upstream's errors reach the client with the same code and message, and its tsize passes through. upstream may be a callable choosing the server per request. For forwarding packets unchanged instead, see Relaying.

Handlers that wrap a directory

Three handlers compose with each other and with FilesystemBackend; none can take a request out of the directory it serves.

from tftp.backends import CaseInsensitive, FilesystemBackend, PerClient, Remap

# Names rewritten by the first matching regular expression, then served.
Remap(FilesystemBackend("/srv/tftp"), [r"^/?pxelinux/=boot/", (r"[.]BIN$", ".bin")])

# /srv/clients/<client address>/ for a client that has one (an IPv6 ':' is written '-').
PerClient("/srv/clients", lambda d: FilesystemBackend(d, writable=True), fallback=False)

# Firmware asking for \Boot\BCD finds boot/bcd; the exact name wins.
CaseInsensitive("/srv/tftp", writable=True)

PerClient is a convenience, not isolation: with the default fallback=True a client that has no directory of its own is served the whole root, every other client's directory included. With fallback=False it is answered "file not found" to everything, and a client can reach only its own directory. Remap takes "REGEX=REPLACEMENT" text or (pattern, replacement) pairs, and a replacement may name a group (\1).

Writing your own

A handler returns a reader or writer. If it can be slow, return a tftp.backends.Pipe and fill (or drain) it from a thread: the transfer pauses while the pipe is empty (or full) and resumes when it is woken, without blocking anything else. Handlers that block while opening are run in worker threads automatically; mark fast ones with opens_fast = True.