Skip to content

MaicolAntali/fromUDPtoTFTP

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

From UDP to TFTP: RFC 1350 Implementation

A native Go implementation of the Trivial File Transfer Protocol (TFTP), specifically focusing on the RFC 1350.

How I implemented it?

Step 1 ~ Let's Start

I have implemented the code to create a UDP connection on port 69 (default port for TFTP) to be able to read data (packet) from the connection with a buffer size of 516 bytes. The buffer size is set to 516 bytes to accommodate the maximum standard TFTP DATA packet (4 bytes for the header + 512 bytes of payload):

2 bytes    2 bytes      up to 512 bytes
------------------------------------------
| 03    |   Block #  |        Data       |
------------------------------------------

Step 2 ~ My first successful TFTP connection

The goal is parse the first 2 bytes (the Op.Code) of the packet. The operation code represent which operation the packet wants to do and the format. The server must be able to support all 5 op. code defined in the RFC 1350.

Each op. code is represented in the code as a constant of type OpCode. The OpCode is defined as a uint16 to match the 2-byte field specified in the RFC. I had used the OpCode constant in a switch so we can decide how to treat each packet.

switch extractTheOpCode(rawPacket[:n]) {
case OpRRQ:
    log.Printf("Received RRQ packet from %s\n", clientAddr)
case OpWRQ:
    log.Printf("Received WRQ packet from %s\n", clientAddr)
default:
    log.Printf("Unknow or unsupported op.code!")
}

After reviewing what I was done until now, I realize that probably I should create struct to hold the packet information. Each packet type has a unique structure and specific logic requirements. I started by implementing the RRQ (Read Request) struct:

type RRQ struct {
	Filename string
	Mode     string
}

Associated with this function I had create a ParseRRQ(b []byte) (*RRQ, error) that take a raw byte slice and extract an RRQ struct. Once that was in place, I write the server function HandleRRQ(conn *net.UDPConn, addr *net.UDPAddr, rrq *RRQ) that effectively handles the RRQ request by sending data. After putting all together this how it looks my case of RRQ:

case OpRRQ:
    log.Printf("Received RRQ packet from %s\n", clientAddr)
    rrq, err := ParseRRQ(rawPacket[:n])
    if err != nil {
        log.Printf("Malformed RRQ request from %s, error: %v\n", clientAddr, err)
        continue
    }

    s.HandleRRQ(conn, clientAddr, rrq)

Now I was ready to send the first few bytes! I implement the DATA struct and the (pd DATA) Marshal() ([]byte, error) function that create a ready to send, byte slice (packet) from the DATA struct. After marshaling a hard code DATA struct I have sent the first few bytes through the TFTP protocol!!

func (s UdpServer) HandleRRQ(conn *net.UDPConn, addr *net.UDPAddr, rrq *RRQ) {
	log.Printf("Client wants to read file: %s in mode: %s\n", rrq.Filename, rrq.Mode)

	bytes, _ := DATA{
		BlockId: 1,
		Payload: []byte("Hello! This is my first TFTP packet.\n"),
	}.Marshal()

	_, _ = conn.WriteToUDP(bytes, addr)
}

FirstSuccessfulConnection.png

Step 3 ~ Go routines

The issue: UDP is stateless, so the server must keep track of the current state for each connection. TFTP offers a solution to this problem: it listens for every request on port 69 and hands the client off to a new ephemeral port. With this solution, I can take advantage of goroutines. After the initial request on port 69, I can hand off the request to a dedicated worker.

I have refactored the function HandleRRQ(addr *net.UDPAddr, rrq *RRQ) to receive only the client address and the RRQ packet as arguments. The function immediately creates a new UDP socket with an ephemeral port. All communication will go through it. The function starts by sending a DATA packet with the first 512 bytes and waits for an ACK. The ACK must be received within 3 seconds; otherwise, the function times out and sends the same block again. The function will try to send the same block 3 times before giving up and closing the connection. If the ACK is received correctly, the function sends the next block. This cycle repeats until the final block (less than 512 bytes) is sent correctly.

Step 4: File and Error Packets

Implementing the logic to read a file from the file system was a straightforward task. Inside the HandleRRQ function, the file is opened and read using a 512-byte buffer. At this stage, HandleRRQ had become quite long and was handling too many responsibilities, which prompted me to perform a refactoring.

I created a new Go file (transfer.go) dedicated specifically to packet transfer logic. From HandleRRQ, I extracted the function: streamFileToClient(conn *net.UDPConn, addr *net.UDPAddr, file *os.File) The primary goal of this function is to transfer the provided file by dividing it into DATA packets. It utilizes sendDataWithRetries(conn *net.UDPConn, clientAddr *net.UDPAddr, data DATA), which manages the network I/O, timeouts, and retransmission logic.

After completing the transfer logic, I implemented ERROR packets to notify the client about any error.

       2 bytes     2 bytes      string    1 byte
      -------------------------------------------
ERROR |  05     |  ErrorCode |  ErrMsg  |   0   |
      -------------------------------------------

I defined a specific ERROR struct with a Marshal() method to ensure the structure is encoded correctly. In transfer.go, I also created a helper function to simplify sending these packets: sendErrorPacket(conn *net.UDPConn, addr *net.UDPAddr, code ErrorCode, message string). Using this function, I can easily return error messages to the client. For example, handling a missing file looks like this:

if errors.Is(err, os.ErrNotExist) {
        if err := sendErrorPacket(conn, addr, ErrorFileNotFount, "File not found!"); err != nil {
            log.Printf("Error sending the error packet: %v\n", err)
        }
    }

Step 5: Implementing WRQ

Implementing the logic for WRITE request was pretty straight forward. I follow the same code structure used in the RDD requests in the revers order. I successful implement the full WRQ logic:

FirstSuccessfulConnection.png

Project Status

  • UDP Server on Port 69
  • RRQ (Read Request) Parsing
  • DATA Packet Marshaling
  • Open a new UDP socket (ephemeral port) to transfer file.
  • ACK (Acknowledgment) Handling
  • Read and send a file on the file system.
  • Create and send ERROR packets
  • Implements WRQ logic
  • Implements modes

About

[WIP] A native Go implementation of the Trivial File Transfer Protocol (TFTP)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages