Introduction
There are lots of small network devices using TFTP for transferring configurations or firmware. This project was created while trying to GET and PUT some configuration files to a network switch. Because there where only commercial libraries in the web, I decided to publish my sample implementation. Since TFTP uses UDP, it is quite easy to create a simple client. The following paragraph gives a small introduction into the TFTP protocol, which is described in detail in RFC 1350.
Some words on TFTP
TFTP only supports two methods: READ REQUEST (RRQ) and WRITE REQUEST (WRQ). There is neither native support of directories nor any function to list all files. The user has to know what file to be read or write.
The protocol
The first packet sent to the server is always a request (RRQ/WRQ), followed by data as well as acknowledgement packages. If something goes wrong, an error packet will be sent.
Read Session
Client | Server |
---|
RRQ packet (filename and mode) to port 69. Source port becomes CTID. | |
| DATA packet (block number) to port: CTID. Source port becomes STID. |
ACK packet (block number) to port: STID | |
| Next DATA packet ... |
A write request will be handled the same way. Instead of the first data packet, the server will respond to the request with an ACK packet where the block number is zero.
The packets
To identify the different packet types, TFTP defines five Opcodes. Depending on the Opcode, the packet may have a different structure.
Overview of Opcodes
Opcode | Operation |
---|
1 | Read request (RRQ) |
2 | Write request (WRQ) |
3 | Data (DATA) |
4 | Acknowledgment (ACK) |
5 | Error (ERROR) |
RRQ/WRQ request
Each session will start with a request (read / write) packet from the client which will be sent directly to the servers port (e.g., 69). The server will either answer with the first data packet (RRQ) or an acknowledgement packet (WRQ). The source port of the client packet is the client side TID, and the source port of the server side is the server's TID (transfer ID). The next packet from the client will be sent to the server using the server's TID as the destination port and vice versa. The TIDs are constant while the transfer is active.
Each request packet will contain the Opcode, the filename terminated by a zero, and the transfer mode terminated by a zero.
Request Packet |
2 bytes | String | 1 byte | String | 1 byte |
Opcode | Filename | 0 | Mode | 0 |
Depending on the type of request, a data packet for RRQ or an acknowledgement packet for WRQ will follow. If something goes wrong, the server will send an error packet.
DATA and ACK packets
UDP does not provide a streaming functionality by itself. Therefore, the combination of the server and the client TIDs will be used as a "virtual channel". TFTP will use a block number for each data packet, which has to be acknowledged. If a packet is not acknowledged in time (some seconds), the sender will repeat the data packet automatically until it is acknowledged. Acknowledgement packets will be answered with the next data packet. Because each data packet should be 512 (data) bytes long, the last packet will have between 0 and 511 data bytes.
Data Packet |
2 bytes | 2 bytes | Data bytes |
Opcode | Block number | Data |
The acknowledgemet packets have a length of 4 bytes. They only consist of the Opcode and the block number to acknowledge.
ACK Packet |
2 bytes | 2 bytes |
Opode | Block number |
In some cases, the server might send an error packet which consists of the Opcode, the error code, and a message terminated by one or more zeros. Many TFTP servers will stuff the error message with "\0" to equalize the length of all packet types.
Error Packet |
2 bytes | 2 bytes | String | 1 byte |
Opcode | Error code | Error message | 0 |
Implementation
The TFTP client is small enough to fit into one class. For convenience, I decided to define enumerations for the Opcodes, modes, and a special exception class for the TFTP failures. Please keep in mind that this is just a simple example implementation which will work well for the author, but it is not yet complete (see the To Do list).
The TFTPClient
has two different ctors, one with the server name and another with the server name and port. Usually, there will be no need to change the server's port.
public TFTPClient(string server)
: this(server, 69) {
}
public TFTPClient(string server, int port) {
Server = server;
Port = port;
}
The Get
function is the first public function of the TFTPClient
. The function is designed to work thread-safe. Because of this, it needs to create its own new IPEndPoint
s and Socket
s per request.
public void Get(string remoteFile, string localFile, Modes tftpMode) {
int len = 0;
int packetNr = 1;
byte[] sndBuffer = CreateRequestPacket(Opcodes.Read, remoteFile, tftpMode);
byte[] rcvBuffer = new byte[516];
BinaryWriter fileStream = new BinaryWriter(new FileStream(localFile,
FileMode.Create, FileAccess.Write, FileShare.Read));
IPHostEntry hostEntry = Dns.GetHostEntry(tftpServer);
IPEndPoint serverEP = new IPEndPoint(hostEntry.AddressList[0], tftpPort);
EndPoint dataEP = (EndPoint)serverEP;
Socket tftpSocket = new Socket(serverEP.Address.AddressFamily,
SocketType.Dgram, ProtocolType.Udp);
In the next step, the request packet will be created by a specialized function and sent on the socket. After retrieving the answer, it is needs to change the destination port of our EndPoint
because the STID
is to be used. If a packet gets lost, the server will repeat the packet until it is acknowledged This implementation will not wait for the repetition; instead, it will run into a timeout and throw a SocketException
! In this case, the user has to retry.
tftpSocket.SendTo(sndBuffer, sndBuffer.Length, SocketFlags.None, serverEP);
tftpSocket.ReceiveTimeout = 1000 ;
len = tftpSocket.ReceiveFrom(rcvBuffer, ref dataEP);
serverEP.Port = ((IPEndPoint)dataEP).Port;
Now, loop until the received data packet is less than 516 bytes (including the header) long. Check the Block number, it might be a repeated packet. Write the data to the destination file and send the ACK packet using a specialized packet creator function.
while (true) {
if (((Opcodes)rcvBuffer[1]) == Opcodes.Error) {
fileStream.Close();
tftpSocket.Close();
throw new TFTPException(
((rcvBuffer[2] << 8) & 0xff00) | rcvBuffer[3],
Encoding.ASCII.GetString(rcvBuffer, 4, rcvBuffer.Length - 5).Trim('\0'));
}
if ((((rcvBuffer[2] << 8) & 0xff00) | rcvBuffer[3]) == packetNr) {
fileStream.Write(rcvBuffer, 4, len - 4);
sndBuffer = CreateAckPacket(packetNr++);
tftpSocket.SendTo(sndBuffer, sndBuffer.Length, SocketFlags.None, serverEP);
}
if (len < 516) {
break;
} else {
len = tftpSocket.ReceiveFrom(rcvBuffer, ref dataEP);
}
}
If the file is transferred, close all handles and exit the function.
tftpSocket.Close();
fileStream.Close();
}
The same procedure applies for the Put
function. The one and only difference is that we have to send the data and wait for ACK packets. For more details, please have a look at the provided source file.
Please note that this implementation will be able to handle lost data or ACK packets from the server or the client. From my point of view, it is not necessarily required to do so because TFTP is mostly used on a short distance in a LAN. So, there should be no packet loss.
Usage
The usage is as easy as pie. Just create a instance of the TFTPClient
class and use it for reading and writing from and to the server. Keep in mind that there is currently no re-encoding so you should use the transfer mode octet, which is the default.
TFTPClient t = new TFTPClient("127.0.0.1");
t.Put(@"test.zip", @"c:\Temp\MyDemoFileWrite.zip");
t.Get(@"test.zip", @"c:\temp\MyDemoFileRead.zip");
Points of interest
Well, none really. It's just a straightforward solution to solve a coding problem.
To Do
- The client should repeat each unanswered message until it is acknowledged after a timeout.
- Handle the different encodings (netasci, octet) - (not sure if it is really needed).
History
- 2007.06.22 - First cut.
- 2007.06.24 - Link to source file fixed.