Build a TFTP Server and Client in Python

Trivial File Transfer Protocol, or TFTP, is a small file-transfer protocol built for environments that value simple implementations over authentication and rich features. It is associated with tasks such as network boot, firmware distribution and tightly controlled equipment provisioning. A Python client and server make its packet exchange easier to understand because the whole protocol starts with just a few message types and a simple acknowledgment loop.

That simplicity has an important cost. Standard TFTP does not authenticate users, encrypt files or provide the protections of SFTP. An internet-facing or unrestricted server is therefore a poor way to distribute sensitive content. The example below is deliberately limited to the local machine only, a single approved test file, read-only operation and one request at a time. It is a teaching implementation, not a deployment-ready file service.

How TFTP differs from ordinary file transfer

TFTP normally uses UDP rather than TCP. A client initiates a request to the server’s well-known UDP port 69; the actual file transfer then proceeds using a transfer-specific UDP port. Each default-size data block contains up to 512 bytes, and the receiver acknowledges blocks so the sender knows when to advance. Because UDP does not itself guarantee delivery or ordering, TFTP implements a small stop-and-wait transfer mechanism inside the application protocol.

The five classic packet types defined in RFC 1350 are read request (RRQ), write request (WRQ), data (DATA), acknowledgment (ACK) and error (ERROR). A read request asks a server to send a named file. The server answers with the first data block from its transfer socket. The client acknowledges that block, receives the next one, and repeats. A data packet shorter than the negotiated block size marks the end; if a file happens to be an exact multiple of 512 bytes, a zero-byte terminal data block is required in the basic protocol.

Real TFTP implementations need to handle retransmissions, client timeouts, duplicate packets, illegal modes, errors, option negotiation and potentially many concurrent transfers. Later protocol extensions allow negotiated block size and timeout behavior. The laboratory version keeps the defaults and supports one client to expose the essential flow rather than concealing it behind a general-purpose package.

For network-layer context, the site’s explanation of IP transport and OSI-layer troubleshooting helps distinguish UDP datagrams from the application protocol handling the file transfer. TFTP’s use of UDP does not make its own acknowledgments equivalent to TCP’s reliable byte stream.

Build a safe laboratory directory before running code

Create a new empty folder on a computer where you are allowed to run test networking software. Put both Python scripts below into that folder along with a text file named demo.txt. The server will make only this exact file available and will not accept uploads, arbitrary path names or directory traversal. The client writes its downloaded copy to downloaded-demo.txt using an exclusive-create file mode so an existing file cannot be silently overwritten.

Port 69 generally requires privileged access and is not needed for this exercise. The scripts instead listen on UDP port 1069 bound to 127.0.0.1, the IPv4 loopback address. Local processes can exchange datagrams there without exposing the example to other machines on the LAN. If another service already uses 1069, choose a different high UDP port in both scripts. There is no reason to change firewall policy, disable security software or expose the example to the public Internet.

Use a currently maintained Python 3 installation. The code relies on the standard libraries socket, struct and pathlib. No external package is required. Launching from the same working directory as demo.txt matters because the server uses a simple relative filename. A service supervisor, sandbox or systemd unit would change those assumptions; that is one reason this program should remain a local exercise.

Write the read-only server

The server checks that the datagram is a TFTP read request for the one approved filename in binary (octet) mode. It binds a separate UDP socket for the response, demonstrating TFTP’s use of a transfer identifier rather than continuing to send data from the original listener. Each block begins with the DATA opcode and a 16-bit block number. A matching ACK allows the next block to be sent. Timeouts lead to a limited resend attempt, and the server exits after one file.

Save this script as tftp_server.py:

"""One-request, read-only TFTP lab server. LOOPBACK ONLY. RFC 1350 subset."""
import socket
import struct
from pathlib import Path

HOST, PORT = "127.0.0.1", 1069
FILENAME = b"demo.txt"
BLOCK_SIZE = 512

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as listener:
    listener.bind((HOST, PORT))
    listener.settimeout(15)
    request, client = listener.recvfrom(1024)
    parts = request[2:].split(b"\x00")
    if request[:2] != b"\x00\x01" or len(parts) < 3:
        raise ValueError("Expected a TFTP read request")
    if parts[0] != FILENAME or parts[1].lower() != b"octet":
        raise ValueError("This lab serves only demo.txt in octet mode")

    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as session:
        session.bind((HOST, 0))  # Fresh transfer identifier (UDP port).
        session.settimeout(1.5)
        with Path("demo.txt").open("rb") as source:
            for block in range(1, 65536):
                payload = source.read(BLOCK_SIZE)
                packet = struct.pack("!HH", 3, block) + payload
                for attempt in range(4):
                    session.sendto(packet, client)
                    try:
                        ack, peer = session.recvfrom(1024)
                    except socket.timeout:
                        continue
                    if peer == client and ack == struct.pack("!HH", 4, block):
                        break
                else:
                    raise TimeoutError("Client did not acknowledge block")
                if len(payload) < BLOCK_SIZE:
                    print("Transfer complete")
                    break

The deliberate filename restriction is the main safety property of this laboratory server. It does not construct arbitrary paths from data supplied by the client. Even so, there is no authentication and no transport encryption. The bound loopback address, fixed file, small test size and limited lifetime reduce exposure for learning; they would not be enough for a production device-provisioning system.

Write the client and observe block acknowledgments

The client sends an RRQ for demo.txt in octet mode. After the first data packet arrives, it remembers the responding server address and UDP port, because a compliant TFTP transfer does not necessarily return packets from the initial request port. For every expected data block, it writes the payload and sends an ACK containing that block number. A short final block completes the transfer. The example recognizes an immediately repeated data block and resends its acknowledgment, but does not implement a full timeout and duplicate-request state machine.

Save the following as tftp_client.py:

"""Loopback TFTP read-only demonstration client. No external network hosts."""
import socket
import struct
from pathlib import Path

SERVER = ("127.0.0.1", 1069)
RRQ = struct.pack("!H", 1) + b"demo.txt\x00octet\x00"

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
    client.settimeout(2)
    peer = None
    next_block = 1
    # Exclusive create prevents accidentally replacing an existing file.
    with Path("downloaded-demo.txt").open("xb") as output:
        for attempt in range(3):
            client.sendto(RRQ, SERVER)
            try:
                packet, sender = client.recvfrom(1024)
                if sender[0] == SERVER[0] and packet[:2] == b"\x00\x03":
                    peer = sender
                    break
            except socket.timeout:
                continue
        else:
            raise TimeoutError("No TFTP response on loopback")

        while True:
            opcode, block = struct.unpack("!HH", packet[:4])
            if opcode != 3 or sender != peer:
                raise ValueError("Unexpected TFTP packet")
            if block == next_block:
                data = packet[4:]
                output.write(data)
                client.sendto(struct.pack("!HH", 4, block), peer)
                next_block += 1
                if len(data) < 512:
                    break
            elif block == next_block - 1:
                client.sendto(struct.pack("!HH", 4, block), peer)
            try:
                packet, sender = client.recvfrom(1024)
            except socket.timeout:
                raise TimeoutError("Transfer stalled; lab has limited retry logic")
print("Saved downloaded-demo.txt")

Notice the client stores received bytes rather than decoding the payload as text. That means the transfer works for the small binary file format supported here as well as plain text. It does not verify content integrity cryptographically, negotiate block-size extensions or resume interrupted downloads. A complete implementation would need tests for concurrent clients, duplicate final packets, rollover behavior and robust timeout management.

Run the test and verify the output instead of assuming success

Open one Terminal window in the laboratory folder and start python3 tftp_server.py. The process waits for one client request and then returns. In a second Terminal window, from the same folder, start python3 tftp_client.py. If the expected file exists and port 1069 is available, the client should print that it saved downloaded-demo.txt; the server should report a completed transfer. Do not change the address to a public host for a test.

Compare the downloaded file with the original using a cryptographic digest or a byte-for-byte comparison. A command such as shasum -a 256 demo.txt downloaded-demo.txt on macOS produces two hashes; they should match for identical content. On another platform, use an available SHA-256 tool. The important property is equality of actual content, not just that a file with the expected name appears.

A useful small experiment is to make demo.txt larger than 512 bytes and observe that several DATA/ACK exchanges occur. The code also sends the required terminal zero-length block for files whose size is exactly divisible by 512. That protocol detail explains why a transfer cannot be considered finished merely because the last observed block happened to be full-sized. You can repeat the exercise by deleting the generated downloaded file and restarting the one-request server; do not overwrite unknown files.

Understand the limitations before adapting the example

The server does not implement WRQ uploads, options such as blksize, netascii conversion, error packets for rejected requests, concurrent sessions, durable audit logs or all recovery behaviors prescribed by modern TFTP deployments. The client is also intentionally conservative about unexpected server responses. Those omissions are acceptable in a controlled tutorial but should prevent anyone from presenting it as an interoperable enterprise TFTP service.

TFTP has no built-in login mechanism. A standard TFTP server that allows arbitrary file reads, or write access to a sensitive directory, can expose confidential information or damage its own host. An unrestricted UDP service may also be abused through misconfiguration or unexpected network requests. On a real provisioning network, protect it through segmentation, narrow bind addresses, authorization outside the protocol, hardened filesystem permissions, logging and careful service lifecycle management—or prefer a modern authenticated encrypted transfer method where the use case allows.

Errors are another difference between a demonstration and operational software. A production implementation should reject unexpected transfer identifiers, report invalid requests using the defined ERROR packet, avoid uncontrolled retries, guard against resource exhaustion and define what happens when a peer disappears after receiving the last block. Designing and testing those behaviors requires treating the RFC as a specification rather than simply adding more socket calls.

When to choose TFTP—and when to choose SFTP instead

Some network appliances and boot firmware still support TFTP because a tiny client is easy to implement before a full operating system is available. In that specific role, an isolated provisioning network can make a narrow TFTP service reasonable. Even there, the operator should restrict which images are served, maintain an approved artifact source and verify that the recipient applies only trusted firmware or configuration files. A transfer completing successfully does not authenticate the content by itself.

For routine administrative file exchange, account-based access, encrypted transport, signed artifacts and observable transfer logs are usually more important than minimizing protocol complexity. SFTP, carried over SSH, is often a better starting point. The existing article on setting up SFTP on Ubuntu addresses that authenticated, encrypted file-transfer role; it is a separate use case, not an alternate TFTP configuration setting.

This demonstration should leave readers with the right mental model: a TFTP RRQ begins at a well-known UDP port, the server responds from a transfer-specific port, DATA and ACK packets advance in blocks, and file size determines the terminal exchange. Building and testing that exchange locally makes the protocol understandable. Choosing whether to deploy TFTP at all requires a much stricter security and operations decision.

img