Files
hpr-knowledge-base/hpr_transcripts/hpr0862.txt
Lee Hanken 7c8efd2228 Initial commit: HPR Knowledge Base MCP Server
- MCP server with stdio transport for local use
- Search episodes, transcripts, hosts, and series
- 4,511 episodes with metadata and transcripts
- Data loader with in-memory JSON storage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 10:54:13 +00:00

338 lines
21 KiB
Plaintext

Episode: 862
Title: HPR0862: Breaking Down TFTP
Source: https://hub.hackerpublicradio.org/ccdn.php?filename=/eps/hpr0862/hpr0862.mp3
Transcribed: 2025-10-08 03:43:45
---
Hi, this is Kevin Grenade with the first installment of my series Breaking Down Protocols.
I was inspired to do this by Steve Gibson's How the Internet Work Series on Security
Now and Klatu's Networking Basic Series here on HPR.
Not so much inspired that it could be done, but that it could be interesting.
So what I'll be trying to do is describe different protocols and pretty much all the nitty-gritty
detail except I'll at the same time be trying to describe why they do the different things
they do with the trade-offs they make, etc. to the best of my ability.
So even though this is going to be very technical, I'm hoping that it'll be pretty accessible
to everyone.
In this episode, I'll be describing TFTP trivial file transfer protocol.
Before getting to the technical details, I think the most important things are why would
you want to use it.
Well, obviously for a file transfer protocol, you need to transfer files, but why use TFTP
instead of some other file transfer protocol.
Well, it's that first word trivial.
It's very simple.
It's very simple to implement.
It takes up a very small memory footprint, etc.
What it doesn't do is provide a lot of robustness, a lot of features, or a lot of speed.
TFTP derives much of its simplicity from its assumptions about the underlying transport
protocols, or rather the lack of assumptions about the underlying transport protocols.
All it requires are machine-level addressing, application-level addressing, and fixed-length
packets.
It was originally designed on top of UDP-IP, which provide these, but it can be implemented
on top of any other protocol that provides these features.
Two protocols I'm aware of that make use of TFTP are PXC and A-Rink 615A.
PXC, sometimes called Pixie Boot, is a protocol that is used to bootstrap a computer off
of the network.
So you embed PXC, which includes TFTP and DHCP and UDP and IP, into the networking
card itself, and it will query for a Pixie Boot server, give the network card an address,
and then download files from the Pixie server over TFTP in order to bootstrap the system.
So with the Linux system, it will download usually a kernel and a initial RAM disk file,
and then it will bootstrap off those.
TFTP is a really good match for this scenario because it's very simple and it only relies
on UDP.
DHCP also relies on UDP, so that has a synergy going where you don't have to implement
one transport layer for one protocol and a different transport layer protocol for the
other.
Not to mention UDP is very simple in the first place compared to, for example, TCP.
A-Rink 615A also uses TFTP to provide file transfer services.
It's not a bootstrap protocol like PXC, it instead is used more for generic file transfer,
it can be used to upload new firmware for remote targets, and it can also be used to retrieve
configuration and log data from those targets.
In this case, the various targets are actually avionics modules, and they generally have
a very small embedded system on them, and in this case, the simple implementation of TFTP
is crucial because the resources on these systems are so constrained.
The common thread between these two applications is that the resources available are very constrained,
and they're also secondary functions of the hardware that they're implemented on.
In the case of PXC, the primary function is a network card, and the PXC booting system
is an add-on feature, I mean, it's just a bullet point, in most cases.
In the case of A-Rink 615A, it also doesn't have anything to do with what the module is actually
supposed to be doing.
Some of these are monitoring landing gear, some of them are monitoring fuel tanks, things
like that, and the ability to upgrade them and retrieve data from them is really a secondary
function, so you don't want to spend a lot of time on it.
It's not their core competency, to say.
The simplicity of TFTP and the simplicity of its requirements really shines here.
A side note is that A-Rink 615A is an example of a TFTP implementation that is not built
on top of IPUDP.
There's actually a special protocol called AFDX that is used on aircraft, and it fulfills
all of the same requirements that UDP does.
But due to the way that TFTP was designed, you can actually move it on top of another
protocol.
Now that I've talked a bit about what TFTP is good for, let's dive into how it's actually
implemented.
The first thing that you want to do is to open a connection to a TFTP server.
So the client will format a packet that requests a particular file, I'll go over the format
of the packet later, and it will open a port locally with a random port number.
It actually doesn't matter what it is.
And it will then send that packet to port 69 of the target machine.
This is what's called a well-known port.
There's actually a registry, a global registry of well-known port numbers that are used
and different protocols reserve certain ports, mostly in the 0 to 1000 range, for their
sold use.
HTTP, for example, has port 80 reserved, it also has other ports reserved for secure communications
etc.
But anyway, TFTP uses port 69.
So you get your packet, you open a local port, and then you send your packet to port 69
on the remote machine.
And it receives that packet, if everything's okay, it will then open its own random
port and respond back to the port that you sent your packet from.
So just for example, you open up port 1027 and send a packet to port 69, and then they
will open port 1024 and send it back to port 1027 on your computer.
So from then on, every message that gets sent back and forth will be addressed to that
IP port pair.
So on your computer, you're going to use your IP address and port 1027, and on the server
they're going to use their IP and whatever port I just said, I actually forgot what it
was.
But anyway, so that's how you know which packets arriving at the computer are intended
for that TFTP conversation.
An important point about this setup is that you can have one process running as the server
on your computer listening to port 69.
And what happens is every time it receives a request, it will actually spawn a new process
that will finish the conversation.
And what that means is that server will then be free to start more TFTP transactions
by continuing to listen to port 69.
So your server doesn't get bogged down with trying to start new sessions and handle
those sessions at the same time.
Now that we know how to start a TFTP file transfer, we can take a closer look at the layout
of the packets.
The first two bytes of each packet is a number that says what type of packet it is.
And actually there's only five different packet types in normal TFTP.
There's actually an additional one, but we'll be getting to that at the very end.
The packet types are read request, write request, data, act, and error.
The read request and write request packets are almost exactly the same.
That is that initial message that you send from the client to port 69 on the server.
If it's a read request, which means the opcode is one, you want to download a file from
the server.
It's a write request, which has an opcode of two.
It means you want to upload a file to the server.
The rest of the message is just made of two strings.
The first of which is the name of the file that the client wants to transfer, either as
a read or a write.
The second string indicates the transfer mode of the request.
There are three default options for this, but one of them is not even used anymore.
That ASCII indicates that the sender should transmit bytes as defined by the document USASIX3.4-1968 and RFC764.
I'm not going to get into the details here, but it's a standard for data interchange between
different CPU architectures.
The most commonly used mode is octet.
This indicates that the sender should transmit bytes in its native representation.
This is less portable, but faster since no translation has to happen.
It's up to the client to know whether it's safe to use octet mode.
Mail mode was part of the original specification as a forwarding method for email, but email
ended up being forwarded over more advanced protocols, and it's deprecated for TFTP.
Nobody does this.
Custom servers are also allowed to implement any additional modes that they want.
For example, they could have a UTF-8 mode, but there is no guarantee that other TFTP clients
or servers will support these additional modes, so that's basically only going to be used
within some kind of a closed system where the implementer is in control of both the client
and the server, and then they can do whatever they want.
The rest of the packets are just as simple.
A data packet has an opcode of 3, a 2-byte block number, and up to 512 bytes of payload.
I'll explain what this means later.
An app packet has an opcode of 4, and a 2-byte block number, which matches the 2-byte block
number in the data packet, and then the last message is an error packet.
It has an opcode of 5, a 2-byte error code, and optionally a null-terminated string that
should be a human-readable indication of what went wrong.
TFTP defines seven error codes to cover the most common errors, such as found, not found,
access, violation, and disk full, and provides a catch-all error code for use when none
of the common errors apply.
The catch-all error code should be supplemented with an ASCII string indicating the cause
of the error.
That string isn't necessary for the other errors.
That's it for the packets themselves, what you might call the syntax of the protocol.
Now I'll move on to what is called the control-flow of the protocol, which is a set of rules
for how these messages are used, and what they mean.
I've covered some of this already, for example, that a write or read request message is used
to start a transfer.
TFTP is what's called a lockstep protocol.
This means that one side sends a message, then listens for a reply before sending the
next message.
This makes it very simple, but it has some drawbacks.
Since only one packet is in flight on the network at a time, it's quite difficult to get
very high through put out of TFTP, and this only gets worse at the latency of the connection
is high.
In normal operation, each message has one other message that can be used to reply to it.
A client starts a read by sending a read request, which is replied to with a data packet,
which is in turn replied to with an AC.
Then the client and server alternate sending data and AC packets until the transfer is done.
You can think of a ping pong game or a pendulum of a clock swinging back and forth, it just
alternates.
To write a file, the client first sends a write request, which is replied to with an AC,
which is replied to with a data packet, and so on.
The exception to this rigid back and forth is the error packet, which can be used to reply
to anything.
This principle goes a long way towards making the implementation of the TFTP client or
server simple, since there's only one message that they have to expect during the bulk
of the transfer.
Since TFTP is layered on top of UDP, which provides no delivery guarantees, TFTP has
to handle retransmission itself.
As you would expect, it does so in the simplest way possible.
After sending a message, the center starts a timeout and rescinds the message if the timeout
expires.
Since retransmission is happening, the packets have to be marked so the client and server
can tell them apart.
This is done with block counters.
Each data and AC packet has a block counter field.
The first data packet has a field with the value of 1.
Each subsequent data packet has a field 1 higher, and each AC has the same value as the
data packet it is acknowledging.
The exception is the AC of a right request, which has a value of 0.
There are three ways for a transfer to terminate, completing successfully, explicitly airing
out, and timing out.
The successful end of the transfer is signaled by a short data packet.
All data packets except for the last one have a 512 byte payload.
The last data packet has either whatever is left of the data, or if the data was a multiple
of 512 bytes, an empty data packet is sent.
This lets the receiver know the transmission is done, but to let the sender know it was
received, the receiver sends one last AC.
If either side encounters an error that renders them unable to complete the transaction, they
can halt the transfer.
They should send an error packet to let the other side know why.
Reasons can include user intervention, no disk space, access violation, illegal operation,
unknown TID, this one's special, and file exists.
Systems are also always coming up with new and exciting ways to make an operation fail,
like printer on fire.
Regardless of the reason, if the side encountering the error is feeling nice, they can send an
error so the other side isn't left hanging.
This leads to the third failure mode timeout.
If the side encountering an error isn't feeling nice, or if the network connection is interrupted
or a powers cut or if there are too many sunspots, one side will just stop responding and the
other side should probably give up eventually, or at least ask the user what to do.
All RFC 1350 says about this is timeouts must also be used to detect errors.
Thanks guys.
A TFTP implementation will also retransmit the latest packet if it receives a duplicate
of the latest packet received.
So for example, if it receives an AC of block 5, it will send a data packet containing
block 6.
If it later receives another AC with a block counter 5, it assumes block 6 was lost and
rescinds it.
This can be faster than waiting for the sender's timeout to expire.
This actually leads to a serious problem called the Sorcerer's Apprentice Syndrome.
Imagine what will happen if a TFTP packet is delayed instead of lost.
The sender will timeout and rescind, and the receiver will get the same message twice.
The receiver will reply to both messages, and then the original sender will get both
replies and in term reply to both of them.
This can continue indefinitely doubling the bandwidth used by the transfer.
But even worse, it can happen again and again.
Which is the reason for calling the bug the Sorcerer's Apprentice Syndrome.
The simple TFTP automaton just keeps mindlessly cloning itself which could possibly bring
down a network.
The fix for this bug is simple, break the chain of retransmitts.
TFTP is required to not reply to duplicate AC messages.
In other words, it replies to the first AC with a given block counter number, but ignores
any subsequent ACs with the same block counter number.
This bug was originally addressed by RFC1123 and later the main TFTP RFC was updated to
contain the fix.
That's the TFTP protocol as defined in RFC1350.
It works, but there are a few shortcomings to the protocol which have been addressed
by later IFCs.
First, the block size of 512 bytes keeps throughput quite low.
Second, the receiver of a file can't determine if it has room for the file and can't give
feedback to the user about progress since it doesn't know how big the file is.
Third, there is no standard timeout period or any way to adjust it.
The means used to address these shortcomings is called the TFTP option extension.
During initialization, the client specifies options in its reader write request and the
server replies with a new message, the OAC, or option acknowledgment which echoes the
options back to the client.
The transaction then continues as usual, but possibly modified by the options used.
The format of the option extension is simple.
Each option used adds two strings to the end of a reader write request.
The first string identifies the type of option being requested.
The second string provides a value associated with the option.
An important point is that the extension method is backwards compatible with vanilla TFTP.
A TFTP server that doesn't recognize options will just ignore the extra data at the end
of the reader write request.
And by replying with a data or ACC instead of an OACC packet, we'll signal to the client
that it cannot or will not use options and the transfer can proceed as usual.
In order to be backwards compatible with servers that may only allocate a 512 byte buffer
for receiving messages, read and write requests are limited to 512 bytes.
The OAC packet contains just the opcode identifying it as an OAC, which is 6, and any options
being acknowledged.
Depending on the particular option, the value associated with that option may be different
in the OAC than in the reader write request.
One last adjustment to the protocol is the addition of a new error code that is used
to indicate that a transfer should be terminated due to option negotiation.
For example, if the server indicates that it cannot support an option and the client does
not wish to continue the transmission unless the option is used.
The block size option spelled BLKSIZE allows the client to request that the file being transferred
be broken into chunks that aren't 512 bytes in length.
The valid range that can be requested is between 8 and 65,464 bytes inclusive.
While it allows the client to request a block size smaller than 512 bytes, the usual goal
of the block size option is to request a larger block size.
Ideally the block size will result in the largest packet that will not be fragmented by intervening
routers, but selecting the block size to make this happen is left as an exercise for the
implementer.
A common choice is 1,428 since this matches the M2U of Ethernet after accounting for
the various packet headers, but it may be desirable to adjust this based on system design or
even local network conditions.
The server may echo a smaller value in its OAC, for example if it has statically sized
buffers or special knowledge about the M2U.
The timeout option allows the client to request a particular timeout duration before the server
retransments TFTP packets.
The valid range is between 1 and 255 seconds inclusive.
If the server is willing to accept this option, it must reply with an OAC containing a matching
timeout value.
Generally, the timeout duration should be slightly higher than the round trip time for a packet
reaching its destination and the reply returning to the sender.
Increasing the value is important if the link being used has a very high latency, and
decreasing the value can be helpful when the link being used is somewhat unreliable since
retries will be attempted more quickly.
The transfer size option, spelled TSIZE, allows the client to provide or request the size
of the file being transferred.
In a write request, the client sets the transfer size option value to the link to the file
in octets, which is echoed back by the server in an OAC.
In a read request, the client sets the transfer size option value to 0, and the server sets
the transfer size option in the OAC to the size of the requested file.
This is primarily intended to allow the client or server to terminate the operation early
if the file is too large, but it can also be used by the client to provide progress information
to the user.
There is one other issue related to the block counter, which is roll over.
The block counter is a two-byte unsigned integer, meaning the largest number it can represent
is 65,536.
The problem is that the TFTP protocol doesn't specify what should happen if the block
counter value exceeds this number.
It is very likely that most implementations will represent the block counter internally
as a 16-bit unsigned integer, and only modify this integer by incrementing it.
If so, the counter will reset to 0 after reaching its maximum value, and everything will
work smoothly.
However, if either implementation uses a different representation of the counter, they may disagree
on what the current value for the block counter should be, and therefore be unable to transfer
files with a size exceeding 65,536 blocks.
That comes out to just a bit under 32 megabytes, so if you don't support roll over that's
compatible with the other end, that's the size of the file you'll be limited to.
A short note indicating that the block counter should roll over to 0 upon reaching its maximum
size would have prevented this problem and allowed the TFTP implementations to confidently
transfer files of completely arbitrary sizes.
But since 32 megabytes was seen as big enough when TFTP was originally written, this wasn't
considered a problem.
This is an example of how underspecifying a protocol can lead to problems in the future
when unanticipated situations can arise.
And there you have it, the TFTP protocol as I know it, while I was doing research for
this podcast, I actually discovered an additional TFTPRFC290 for TFTP multicast option.
I am not familiar with it, and it looks somewhat complicated, so I'm going to be skipping
that one.
I'd like to take this opportunity to thank everyone involved in producing HPR for providing
this forum for audio casts.
And with that, this is Kevin Grenade signing off, and hoping to hear from you.
You have been listening to Hacker Public Radio, or Hacker Public Radio does our.
We are a community podcast network that releases shows every weekday Monday through Friday.
Today's show, like all our shows, was contributed by an HPR listener like yourself.
If you ever consider recording a podcast, then visit our website to find out how easy
it really is.
Hacker Public Radio was founded by the Digital.Pound and the Infonomicom Computer Club.
HPR is funded by the binary revolution at binref.com.
All binref projects are crowd-responsive by linear pages.
From shared hosting to custom private clouds, go to lunarpages.com for all your hosting
needs.
Oneless otherwise stasis, today's show is released under a creative commons, attribution,
share alike, lead us our license.