TFTP Client & Server

with Option Extension over UDP

 

 

Trivial File Transfer Protocol

for POSIX compatible platform: Solaris

 

 

Documentation Report Presented to

x

By :

x

x

December 2000

x

x

X

x

Table of Contents

Introduction *

Trivial file transfer protocol. 3

Sockets. 3

User Datagram Protocol. 3

Client/Server model. 4

TFTP Design 5

Program command line example. 8

TFTP Sliding window design. 9

TFTP Blocksize option 9

TFTP Timeout Interval 9

TFTP Option Negociation 10

Overview of the TFTP Protocol 11

Connection Phase *1

Data Transfer *1

Disconnection Phase *2

Structure and overall architecture of the class model *

Assumptions for the TFTP Project *1

TFTP program performance and limitation *4

CONCLUSION 26

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Introduction

 

Trivial File Transfer Protocol

Trivial File Transfer Protocol (TFTP) is a simple protocol for transferring files between systems. TFTP is implemented on top of User Datagram Protocol (UDP) and provides no security features and it lacks most of the features of a regular FTP. TFTP may be used to move files between machines on different networks implementing UDP. TFTP uses go-back-N protocol. In other words, data packets are transferred in one direction only and each data packet is acknowledged separately. TFTP can read and write files from/to a remote server. The only supported transfer mode for this project is octet. This means that our TFTP program should send or receive data in binary format without any interpretation. The data is sent or received as is, there is no translation of CR/LF ASCII characters or any other ASCII characters.

Sockets

A socket is one end point of a two-way communication link between two programs connected to the Internet. Sockets are seen as communication interfaces on top of TCP or UDP. TCP is connection-oriented socket and UDP is connectionless datagram socket. A socket is bound to a port number that represents a communication end-point, so that the TCP or UDP layer can identify the application for data to be sent.

User Datagram Protocol

User Datagram Protocol (UDP) is a standard, low-overhead, connectionless, host-to-host protocol that is used over packet-switched computer communications networks, and that allows an application program on one computer to send a datagram to an application program on another computer. Connectionless protocol refers to network protocols in which a host can send a message without establishing a connection with the recipient. That is, the host simply puts the message onto the network with the destination address and hopes that it arrives. UDP is a simple protocol that exchanges datagrams without acknowledgments or guaranteed delivery, requiring that error processing and retransmission be handled by the other protocols. UDP provides very few error recovery services, offering instead a direct way to send and receive datagrams over an IP network. It is used primarily for broadcasting messages over a network or to distribute information that changes every few seconds.

Client/Server model

TFTP is using the client/server model, which consist of two distinct programs, usually running on different machines at different locations. The two different socket classes: UDP_SocketServer and UDP_SocketClient, are used to represent the connection between a client program and a server program. A server program contains the main method and performs the work of listening to the port, establishing connections, reading from and writing to the socket to answer requests from client programs. Given the server address and socket number, the client program can find the server and requests a connection from the server. Once a connection is been established, both the client and the server are ready to transfer information. Once the transfer is completed between both parties, the client and the server will disconnect from each other. However, multiple client requests can come into the server socket. Client connection requests can be accepted or rejected. The server can service many clients simultaneously through the use of threads, where one thread is used for each client connection.

 

 

 

 

 

 

 

 

 

 

 

 

 

TFTP Design

Our main objectives were to have a multi-thread environment with queues, mutex and read-write locks to protect shared data and queues integrity. We also decided to use real-time clock timers for accurate wake up time to verify if a timeout as occur or not.

In order, to have a fully working multi-thread environment, that is fast, efficient and reliable, where data integrity is guaranteed, we needed to use some protection mechanism that will insure that no global shared object will ever get corrupted. We used global shared object, so that we can split the task to be accomplished by many threads that are working together to get the job done. Those mechanisms are known as mutex and read-write locks. Mutex ensure that the only owner of the lock will be able to modify the data at a given time. Read-write locks ensure that only a single writer can modify the data or that many readers can read the data at a given time. In order, to achieved some degree of portability, we used the commonly known Posix Threads library, that we wrapped up to make it easy to used in a class named Basic_PThread. We did the same for Mutex and RWLock classes. Those classes permit us to forget about the Posix Thread library complexity and to make it as easy to use as it is in Java. Having those tools at hands, we divided the program in many sub tasks, where each sub task is a class, which inherit from Basic_PThread. We can therefore say that we used mainly the bottom-up approach, mainly by first designing the required tools needed by the program and then use those fully debugged and working classes to build up more complex classes.

For instance, we wrote down a brief list of needed classes for the program and decided to start designing and coding those classes, since everything else used those classes by after. For example, we cannot design multi-thread TFTP classes without having threads, mutex, RW locks and UDP sockets working.

It was pretty easy by after to create as many sub task as needed as the following list illustrates:

 

Program command line examples:

Run server on port 2000

./tftp445 -server 2000

Run a client to write a file call test.txt on the server DEA,

port 2000 with a blocksize of 600 bytes and a windowsize of 3.

./tftp445 --blocksize=600 --windowsize=3 -client dea.ece.concordia.ca:2000

-put test.txt

 

 

 

TFTP Sliding Window Design

As mentioned, when we send a data packet, we take it out from data_queue and send it through the socket and decrement the windowsizeLeft counter by one. We keep on sending data packet until the windowsizeLeft counter reaches zero. When we get an ACK packet, the ACK_driver function in TFTP_Parser will search in the queue and increment the counter by one for each element found.

 

TFTP Block Size Option

This is fairly simple, we read from disk in binary read-only mode up to the given blocksize, if that much data is provided. We construct data packet from it using DataFactory, label them and queue them into data_queue. After each reading, a file seek of blocksize bytes is done if the data length is equal to the blocksize option, so that the next read to file will give us the next data packet, else we close the file. The current data transfer session is ended when a packet of less than the blocksize is received or get acknowledged. If the file size is a multiple of the blocksize number, we send a dummy packet, with no data attach to it, which acts as a flag to end the session.

 

TFTP Timeout Interval

As mentioned, when we send a data packet, we take it out from data_queue and send it through the socket, we keep a local copy that we timestamp with an issue time of currentTime + timeout and append it to the waitingAck_queue. Once the timer wakes up, we increment the currentTime value and scan the list for expired timestamp data packet. For instance, if the new current time is lower then or equal to the expired timestamp then this data packet timeouts and its resent. We send only one packet at a time, if by chance, we receive an acknowledgement before the next data packet expire then we know that everything was received. In the worst case, the next data packet will timeout and be resent and so on, until an acknowledgement for up to the last packet is received.

 

TFTP Option Negotiation

When we parse the command line in TFTP_Option, we call a function named build_OptionPacket() and build_RRQ_WRQ_TemplatePacket(). The first one creates a template packet that will be used by RRQ, WRQ and OACK statements. It simply insures that all initialized variables are appended to Option_template through memcpy(), the length is saved in Option_templateLength and the two templates are protected by the read-write lock named rwlock3_templates. The number 3 is here to avoid any deadlocks, by simply making sure that any sequence of RWLock or mutex requests are in ascending order. RRQ_WRQ_template is a template with a default RRQ opcode, the filename requested and the transfer mode. Once a RRQ or WRQ has to be sent, the function create_RRQ_WRQ() locks the RWLock for reading and checks the length and if itís bigger than zero, it appends it to the RRQ, after copying the RRQ_WRQ_template and opcode in the appropriate byte location.

On the server side, when we receive RRQ or WRQ, we parse the raw packet and identify every null-terminated string using the strlen() function and by doing some pointer arithmetic. We copy all those null-terminated string to an array and store the size of the array for further processing. We then scan the array for keywords like "blksize", "window", "octet", "timeout", etc. knowing that the next parameter, except for "octet" shall be the value of that variable. Using strlwr(), strcmp(), atol() and few if statements, we can know exactly what is what and even know if some garbage was added and ignore it. We identify each variable in consequence and an Option_template is created. If OACK_arg is set to true, then we know that we must respond with an OACK, else we send DATA 1 or ACK 0. Due to some unknown bus error, we had to comment OACK_arg out, but the templates are still created and decoded accurately on both sides.

 

 

 

 

Overview of the TFTP Protocol

  1. Connection Phase

  1. Data Transfer

OACK Acknowledge the Read Request and the options.

DATA 1 Acknowledge the Read Request but not the options.

ERROR The read request has been denied.

OACK Acknowledge the Write Request and the options

ACK 0 Acknowledge the Write Request but not the options

ERROR The write request has been denied

or by an acknowledgement packet for a data packet with a bigger block number.

  1. Disconnection Phase

 

 

 

Structure and overall architecture of the class model

 

The class Mutex contains the following POSIX C structures:

 

The class Mutex is used when one user wants to access a shared variable, (generic shared data inside the program) to prevent two users from accessing the same shared variable at the same time, to guaranteed data integrity and to avoid data corruption. By definition, Mutex works on a FIFO (first in first out) basis, which is the first user to request the Mutex, obtain the Mutex.

The class RWLock is used when one user wants to access a shared variable, (generic shared data inside the program) that might be read by many readers at a given time. It is implemented using Mutex and a bunch of counters. The first reader gets the mutex and grant access to every other read request but denying every write requests. The last reader to notify a release the RWLock releases the mutex. The first writer gets the mutex and prohibits any other action on the protected object until heís done with the modifications.

The class UDP_SocketServer will listen at a well-known port for service requests. The UDP_SocketServer::ListenThread() process remains dormant until a connection is requested by a client's connection to the server's address. When a connection is requested to the server's address, the server process wakes up and services the client requests. The server address consists of its IP address and the service port where the request was made. Once a server has established its environment, it creates a socket and begins accepting service requests. The bind() function is required to ensure that the server listens for request at the expected location. In order to listen to a port number under 1024, the server must be run with root user ID.

The class UDP_SocketClient is an active entity process that initiates a connection when invoked. The client knows where to find the server by using the getservbyname() function. Next the destination host is looked up with a gethostbyname() call. At this point the address buffer is cleared, then filled in with the Internet address of the remote host and with the port number where the login process resides on the remote host. At this point a socket is created and a connection is initiated. The process UDP_SocketClient::ListenThread() is created to wait constantly for a reply from the server.

The class FileFactory will take care of writing the raw packets into the file, by parsing the data and the length and store it into the file at the appropriate location by calling the appropriate JFile functions. We currently use directWrite() instead of our first design which was to enqueue packet to be written into data_queue and then let the FileFactory thread take care of writing the packets to the file once it get some CPU cycles. The problem was that the FileFactory didnít get enough CPU cycles and it was therefore simpler to just write the data packet to the file as soon as we receive them.

The class DList_pPacket is simply a double linked list with queue features where each element stored is a pointer to a TFTP_Packet object.

The class DataFactory will take care of reading the file and produce data packets from the content stored inside the file. After that, it will take the data packet and enqueue it at the end of the double linked list named data_queue. This class use JFile and TFTP_Option to read from the file.

The class TFTP_Options will take care of every option and mostly all global variables that we are using inside this program. Most of those variables are initialized by default and reinitialized later on as needed through the command line, the shell, a client request or a server acknowledgement. Few RWLock objects protect all the data stored inside this class. Each of them is assigned an ID number, so that any cascaded request to a RWLock must be in ascending order to avoid any deadlocks. For instance, you can lock 1 and 3, but you cannot ask for 2 before unlocking 3, else a deadlock will probably occur.

This way all the class users can simultaneously access the data as needed in parallel, but only one of them can write to it, but many of them can read it, at a given time. As seen in the demo, we often print the current program status by calling showOptions() to display the following global data and their length for debugging purposes:

the last data packet )

we donít exceed the current window size maximum )

Each of those data follows the RFC documentation, the range are respected and for the access mode any value other that octet is considered as non implemented and result in the termination of the program with an error code 2.

To make the implementation of the class TFTP_Packet easier, we added two special functions in TFTP_Option which are to build the option extension template using given defined parameters and to build the read or write request template without the option extension. This way, the TFTP_Packet simply have to take care of initializing packets with the default-established templates that we decided to use. The first operation is obviously to create those templates.

The class PThread and Basic_PThread are wrappers for the Posix Thread set of functions part of the POSIX Thread library. PThread is more evolved than Basic_PThread, since it contains the setting for scheduling and other pthread advanced features. This class is wonderfully design and very easy to used. It follows the Java idea, which is order to get a working PThread, you simply have to inherit from that class, implement the function virtual void run() and start() it to make the pthread running. The function start() will take care of initializing the pthread and create it with default scheduling parameters, it will pass a static function wrapper, since a callback cannot be done on virtual functions and a pointer to the class, since a static function cannot access members of a class. The wrapper takes a void* argument which will be cast back to PThread*, so that we can call the run function from it, as simple as that. If you ever programmed threads in Java, you will get the same easy to use API to make your life simple. Inherit from PThread, implement run() and start() it !

The class Timer uses signals and timer_create() to implement a real-time clock timer, it uses SIGUSR1 that will be catch by a timer handler. Since that timer_create() cannot pass a void* parameter and that only signal SIGUSR1 and SIGUSR2 can be used, we are forced to have a maximum of two timer for the entire program, unless we fork a timer for each packet, but that could slow down everything. This class use the same trick as in PThread, which is to call a static function that will call a virtual, unimplemented one that shall be implemented by the children class. The Timer class constructor takes as a parameter the number of nanoseconds and seconds which is the clock tick length of the timer square wave. The clock low duration and the clock high duration are set as equal, to get a real square wave at the specified frequency.

The class JFile can handle any kind of files, text or binary. The main role is to read data from it or to write to data into it. The class knows itself (the FILE* pointer), itís size and itís current position. The filename included in the path, access mode and the record size are given as parameters to the constructor. The JFile is able to parse the given path to retrieve the short filename, so that the TFTP_Option user can ask JFile what is the short filename, so RRQ and WRQ parameters can be established. It can also ask the file size for the option extension, if needed. Therefore, the class is able to append data or to read data as a stream, since it updates the pointer as it read or write into the file by incrementing the pointer by the specified block size stored internally.

JErrorNo is an enumeration of supported errors that are used in C programming. It follows the errno definition as declared in <sys/errno.h>. Since we used a lot of C functions inside this TFTP program, it was much more convenient, to use the returned errno value and simply match that value with a set of predefined error handlers. The error code returned by the function is converted to a JErrorNo type and passed to the JErrorController. Since different platforms have different error number, we standardized it, so that we can have a uniform array of function pointer. It converts incompatible error number to the one we defined as standard and then pass it to the call() function, which will do a look up in the array using the JErrorNo as an index. Each element of the array are a pointer to the head of a single linked list of JErrorHandler* elements.

The class JErrorController manages and formats the error received from the TFTP program. The JErrorController will find in the array the error received from the TFTP program and deliver the error to the error handler. The main task of the JErrorController is to react accordingly to that specific error by using the JErrorHandler for that error number. The call() function inside the JErrorController class will simply call the JErrorHandler* head, which will call a static function which will deal with the error. The default error handler is simply a text display, specifying the origin of the error and a short description of the possible reason to explain why this error happened. The line number and source code file name are displayed and also the theoretical JErrorLevel, which is a value out of five, which tell us how important this error is and if the program can be resumed, aborted or if the error can be solved or not.

The class JErrorHandler handles errors and try to resolve the error according to the description of the error and the error number. If the error can be recovered then JErrorHandler will resolve the current error. Otherwise, JErrorHandler will display by default the following error message on the standard error output ( stderr ):

***********************************************

An error of type #X of level Y/5 has occurred

at line L of file XYZ.cpp,

Here's a brief description of the error:

[ÖÖÖ]

***********************************************

By default the JErrorHandler will point on the default error handler. New error handlers can be added to perform specific error handling management, i.e. different from the defaultErrorHandler() function, simply by adding a new JErrorHandler that points to the new error handler static function and then add this handler on top of the stack for that specific JErrorNo number. JErrorHandler is simply a container, so that we can do a link list of it. The JErrorHandler is used, for example, to inform us that the JFile couldn't be opened properly or that a Mutex, RWLock or PThread operation was not successful.

The class TFTP_Timeout scan the double link-list waitingAck_queue from the head and search for an expired timestamp stored inside a TFTP_Packet, if this is the case then a timeout occurs and the packet will be resent if less than three retries were attempts. We detect a timeout by storing the current time added to the timeout value to get a timestamp value. When a clock tick occurs, we update the current time and look for any timestamp lower then or equal to the current time. If such a thing happen, we resent the packet, update the timestamp and enqueue it back into the waitingAck_queue, just in case another timeout occurs.

The class TFTP_DataAck will acknowledge each packet in the queue during the TFTP transfer and will be called every time a valid ACK packet is received with a block number greater than zero.

The file TFTP_header is design to define all states, enumerations and C type structures used in the TFTP program, such as:

// List of all OpCode accepted by TFTP.

enum OpCode_t

{

RRQ = 1,

WRQ = 2,

DATA = 3,

ACK = 4,

ERROR = 5,

OACK = 6

};

 

const char* TFTP_errmsg[ 8 ] = {

/* 0 */ "Not defined.",

/* 1 */ "File not found.",

/* 2 */ "Access violation.",

/* 3 */ "Disk full or allocation exceeded.",

/* 4 */ "Illegal TFTP operation.",

/* 5 */ "Unknown transfer ID.",

/* 6 */ "File already exist.",

/* 7 */ "No such user.",

};

 

enum TFTP_errcode_t

{

TFTP_NotDefined = 0,

TFTP_FileNotFound = 1,

TFTP_AccessViolation = 2,

TFTP_DiskFull = 3,

TFTP_IllegalOp = 4,

TFTP_FileAlreadyExist = 5,

TFTP_NoSuchUser = 6 // only use for mail (Not supported)

};

 

 

 

 

 

/**************************************************************************

**************************************************************************

*** bit 0 Init

*** bit 1 Connected

*** bit 2 Ack for RRQ/WRQ

*** bit 3 Reading

*** bit 4 Writing

*** bit 5 End of Read/Write

*** bit 6 Error

*** bit 7 Finished, program exit signal

**************************************************************************

*** Disconnect means that we are not connected and that

*** all the variables in TFTP_Option are still initialized.

**************************************************************************

**************************************************************************/

enum TFTP_Status_t

{

TFTP_Uninitialized = 0x00, // %0000,0000

TFTP_Disconnected = 0x01, // %0000,0001

TFTP_Connected = 0x03, // %0000,0011

TFTP_ReadingNotAck = 0x0B, // %0000,1011

TFTP_WritingNotAck = 0x13, // %0001,0011

TFTP_Reading = 0x0F, // %0000,1111

TFTP_Writing = 0x17, // %0001,0111

TFTP_EndofReadNotAck = 0x2B, // %0010,1011

TFTP_EndofWriteNotAck = 0x33, // %0011,0011

TFTP_EndofRead = 0x2F, // %0010,1111

TFTP_EndofWrite = 0x37, // %0011,0111

TFTP_Error = 0x43, // %0100,0011

TFTP_Finished = 0x81 // %1000,0001

};

/**************************************************************************

**************************************************************************

*** bit 0 Server

*** bit 1 Client

*** bit 2 Reading Command Line request

*** bit 3 Writing Command Line request

**************************************************************************

**************************************************************************/

enum TFTP_Type_t

{

TFTP_Type_Undefined = 0x00, // %0000

TFTP_Server = 0x01, // %0001

TFTP_ServerRead = 0x05, // %0101

TFTP_ServerWrite = 0x09, // %1001

TFTP_Client = 0x02, // %0010

TFTP_ClientRead = 0x06, // %0110

TFTP_ClientWrite = 0x0A, // %1010

};

 

The class TFTP_Sender sends data packet created by the Data_Factory until the queue is empty or that the windowsizeLeft reached zero.

The functions in TFTP_Error send a TFTP error packet for the assigned errno values. The following error functions are defined:

The last error should not occur because we are not using Mail option.

 

 

Assumptions for the TFTP Project

and the program is aborted.

such as Solaris.

with the inclusion of the -lmalloc library in the Makefile.

packet, if connected, and display a meaningful message before exiting as long as the

error is covered by the Unix errno values, if not the program execution

is undetermined.

e.g. Bus error, segmentation fault are not covered by JErrorController.

 

 

 

 

 

 

 

 

 

 

TFTP program performance and limitation

Experiments:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Conclusion

 

The project was a great learning experience for us. We encountered many problems during this project. Segmentation Faults were a naturally incurring error. This off course made the use of gdb essential for solving those errors. The decision to use threads and mutex proved to be wise decision since scheduling was left to the operating system. The use of Mutex caused some problems in the beginning but only because we had not included the proper libraries.

The design of project went through many changes throughout the course of the semester since we used a bottom up approach to the project. But that proved to be a good idea since most of our time was spent on making the low-level code work without flaws.

Working in a team caused some friction at some times but was dealt with like any team project. But the TFTP project proved to be an overall success at the end.