TFTP Is Not Trivial
I implemented a Trivial File Transfer Protocol (TFTP) client from the specification, RFC 1350.
I did this for educational purposes. I figured it would be easy.
The protocol sends a block (512 bytes) of data, then waits for that block to be acknowledged before it sends the next block. In that sense, it’s very close to the Xmodem family of serial line file transfer protocols.
Difficulties
- Connected vs unconnected UDP sockets. See below.
- Endianness. The RFC doesn’t talk about “network byte order” or endianness at all, and I implemented it incorrectly at first. Block numbers are 16-bit integers, so they can be in one of two orders. I figured block numbers had to be in network byte order, but I’m not all that well versed in little-endianness. By mistake I chose the wrong order.
- Very finicky servers. Besides being picky about file names, and exceedingly careful about what block numbers they accept, they don’t give much more than “I had a problem” as an error message. Terseness makes debugging difficult, particularly if you’ve gotten the block number byte order wrong.
- Changing server port after the first ACK. The client’s view of a UDP connection (IP address and port number), changes with the first response packet from the server. TCP does something like this as well, but the change in port number is kept in the kernel, not visible to the programmer.
UDP difficulties
I learned about UDP “connected” and “unconnected” sockets.
This appears to be part of UDP itself, or at lest the Linux UDP implementation.
It appears that the TFTP client has to listen for UDP packets
(DATA, ACK and ERR types) with an “unconnected” UDP socket.
In Go programs, this amounts to creating a *net.UDPConn instance
via the net.ListenUDP function.
You can see connected versus unconnected sockets on a Linux machine with the ss command.
1001 % ss -a -l -u # /home/bediger
State Recv-Q Send-Q Local Address:Port Peer Address:Port
UNCONN 0 0 127.0.0.1:323 0.0.0.0:*
UNCONN 0 0 10.0.0.1:cbt 0.0.0.0:*
UNCONN 0 0 10.0.0.1:7896 0.0.0.0:*
UNCONN 0 0 0.0.0.0:domain 0.0.0.0:*
UNCONN 0 0 10.0.40.1:bootps 0.0.0.0:*
UNCONN 0 0 10.0.20.1:bootps 0.0.0.0:*
UNCONN 0 0 10.0.0.1:bootps 0.0.0.0:*
UNCONN 0 0 10.0.0.1:tftp 0.0.0.0:*
UNCONN 0 0 0.0.0.0:ntp 0.0.0.0:*
UNCONN 0 0 [::1]:323 *:*
UNCONN 0 0 *:domain *:*
An “unconnected” UDP socket does not have a default destination.
That lets any other machine or process send a UDP packet
to that socket.
Most of the sockets in the above listing make sense as
a destination for UDP packets.
0.0.0.0:domain is dnsmasq,
which I’m using as a DNS aggregator.
0.0.0.0:ntp is chrony,
an NTP server.
The “0.0.0.0” means the socket can be reached
on any network interface.
Three entries 10.0.40.1:bootps, 10.0.20.1:bootps, 10.0.0.1:bootps,
are sockets on which the kea DHCP4 daemon
listens for DHCP
DISCOVER packets.
This explains how kea-dhcp4 can send an address in the 10.0.20.0/24
range to network entities attached to the CAT-5 cable
plugged in to that machine’s ens5 socket,
but not those plugged in to the eno1 socket.
A TFTP server needs to listen for RRQ (TFTP read request) or WRQ (TFTP write request) packets arriving from anywhere, otherwise, it’s not a server. It must open and listen to an unconnected UDP socket. What’s less obvious is that a TFTP client must open an unconnected UDP socket. A TFTP client does know the IP address of the TFTP server, but it can’t know the UDP port that the first DATA packet a server sends or the first ACK a server uses to send one of those packets to the client. Once the client gets an ACK (for a WRQ) or DATA block no. 1 (for a RRQ), it has to send DATA block no. 2, or an ACK for DATA block no. 1 to the UDP port number of the server.
TFTP clients in the Go programming language:
- Create an unconnected UDP socket with
net.ListenUDP. - Send a WRQ or RRQ request to the TFTP server
using
net.UDPConn.WriteMsgUDPornet.UDPConn.WriteTo, which use the unconnected UDP socket, but set the destination port for that write but no subsequent writes. - Read the first ACK or DATA block using
net.UDPConn.ReadFromUDP, which returns not only the UDP packet’s “payload”, but also the IP address and port number the server sent it from. - Use that IP address and port number to send and receive further ACKs or DATA packets.
Effect of TFTP on server design
TFTP is so loosely specified and so simple,
that implementations of servers in the past have caused problems.
Hypothetically, in the early 1990s, you could easily use TFTP
to get a copy of the company intranet’s main server’s /etc/passwd file.
With that file, and your shiny, new PC, you could hypothetically
run a brute force password guesser.
If the password guesser worked, you could then find out
that the head network engineer’s password was “butterfly”.
That sort of hypothetical occurrence happened quite often. TFTP servers were prone to what we now call directory traversal attacks, password standards were very lax, standard encryption algorithms had gotten weak relative to available computational speeds. It was all hypothetically unsettling.
TFTP servers have evolved to be very picky about what they allow, what they accept as file names, and if they allow WRQ (write request) transfers. All of these guards against TFTP abuse make troubleshooting very difficult.