Skip to content

Relaying

There is no standard TFTP relay (nothing like DHCP's), so tftp.relay.TFTPRelay is an application-level one. The request is forwarded byte for byte to an upstream server from a fresh socket; the upstream's transfer ID is learned from its first answer; then datagrams cross unchanged:

client:C  --RRQ-->  relay:69                (request, forwarded untouched)
relay:U   --RRQ-->  upstream:69
upstream:S  <->  relay:U                    (the upstream's TID, learned)
relay:D     <->  client:C                   (a fresh client-side TID)

Because nothing is re-encoded, options and vendor extensions this library has never heard of still work end to end, and the client negotiates directly with the upstream.

from tftp.relay import TFTPRelay, RouteTable, by_prefix, by_subnet

route = RouteTable(
    [
        by_prefix({"windows/": "wds.lan"}),
        by_subnet({"10.1.0.0/16": "10.1.0.5", "10.2.0.0/16": "10.2.0.5"}),
    ],
    default="10.0.0.20",
)
TFTPRelay(route, on_session_end=print).serve_forever()

A route is any plain callable route(request, context) returning an upstream ("host", "host:port", (host, port)) or None to refuse (ERROR 2). context has the client's address, and the address and interface the request arrived on.

On an event loop

AsyncTFTPRelay is the same relay on asyncio: the same arguments, texts, counters and bounds, and a route that is a coroutine, so it can ask a database or an inventory service. A plain function is a TypeError when the relay is built, and a coroutine is one for TFTPRelay. The helpers above are plain functions: call one inside the coroutine.

import asyncio
from tftp.relay import AsyncTFTPRelay, by_prefix

windows = by_prefix({"windows/": "wds.lan"})

async def route(request, context):
    return windows(request, context) or "10.0.0.20"

async def main():
    async with AsyncTFTPRelay(route, on_session_end=print) as relay:
        await relay.serve_forever()

asyncio.run(main())

The route and the lookup of an upstream's name run in a task of the relay's, bounded together by idle_timeout, and count against max_sessions while they wait: a request that outlasts the bound is answered ERROR 0 and a request that finds the relay full is refused at once. Stopping the relay ends every transfer in flight, both ends getting ERROR 0 "relay shutting down".

When does a relayed transfer end?

A transparent relay cannot understand every extension, so it uses several signals: an ERROR in either direction, the final DATA/ACK exchange (then a short linger for retransmissions), no traffic for idle_timeout, or max_duration. Each ends with a RelaySummary (bytes each way, reason, error).

Relay or proxy?

TFTPRelay UpstreamBackend + TFTPServer
packets forwarded unchanged each side is its own transfer
options client and upstream negotiate directly each side negotiates its own
unknown extensions pass through not understood, so not forwarded
different block/window sizes per side no yes
backpressure end to end through a bounded buffer

Use the proxy when an old boot ROM at 512-byte lock-step should still get the upstream at 8 KiB windows, or to put policy (and caching) in between; use the relay to reach servers on another network without touching the protocol.

Firewall/NAT "TFTP helpers" are a third, different thing: they open connection-tracking state for the server's new port. This library does not touch OS firewall state.