A TFTP Server in Rust

Rust is Pedantic. I’m Pedantic. We get along wonderfully. Since HTTP is way too overdone, I wanted to try something at the Byte twiddling level. I got a very, very basic TFTP server to run and fetch a larger binary file without corrupting it. Time to celebrate with a bragpost.

The code is on Github, and I went full GPL on it.

Some comments are certainly called for. Here is the main loop, that

  • reads a single packet from a UDP socket
  • extracts the OP code
  • calls the appropriate handler function

fn read_message(socket: &net::UdpSocket) {
    let mut file_streams = HashMap::new();

    let mut buf: [u8; 100] = [0; 100];
    loop{
        let result = socket.recv_from(&mut buf);

        match result {
            Ok((amt, src)) => {
                let data = Vec::from(&buf[0..amt]);
                let connection = Connection{socket: socket, src: &src};
                let mut rdr = Cursor::new(&data);

                if amt < 2{
                    panic!("Not enough data in packet")
                }
                let opcode = rdr.read_u16::<BigEndian>().unwrap();

                match opcode {
                    1 => {
                        file_streams.insert(src, handle_read_request(
                            &mut rdr, &amt, &connection));
                    },
                    2 => println!("Write"),
                    3 => println!("Data"),
                    4 => {
                        let chunk = rdr.read_u16::<BigEndian>().unwrap() + 1;
                        file_streams.get_mut(&src).unwrap().send_chunk(&chunk, &connection);
                    },
                    5 => println!("ERROR"),
                    _ => println!("Illegal Op code"),
                }
            },
            Err(err) => panic!("Read error: {}", err)
        }
    }
}

My first hack at reading the opcode just looked at the second byte as u8, as the big endian network approach means that value was also a valid u8 and matching. However, later on, I need to marshall larger and larger numbers into the outgoing buffer, and the byteorder crate handles that for both reading and writing.

However, reading ascii strings this way was not so clean:

 pub fn new(data: &mut Cursor<&Vec <u8>>, amt: &usize,) -> FileStream {
        let mut index = 2;
        for x in 2..20 {
            if data.get_ref().as_slice()[x] == 0{
                index = x;
                break;
            }
        }
        let mut full_path = String::from("/home/ayoung/tftp/");
        let filename = match str::from_utf8(&data.get_ref().as_slice()[2..index]) {
            Ok(file_name) => file_name,
            Err(why) => panic!("couldn't read filename: {}",
                               Error::description(&why)),
        };
        full_path.push_str(filename);
        println!("filename: {}", filename);

I originally tried using the
read_to_string method on the Cursor, but it did not identify the null, and I ended up with an invalid string. str::from_utf8 Worked properly, once the index is set to skip the start of the buffer.

I really like the form of error handling where the success value is used for assignment, like this:

        let file = match File::open(full_path){
            Err(err) => panic!("Can't open file: {}", err),
            Ok(file) => file,
        };

This Server reads files out of $HOME/tftp. To Test it out, I did:

[ayoung@ayoung541 tmp]$ tftp 127.0.0.1 8888
tftp> binary
tftp> get Minecraft.jar
tftp> quit
[ayoung@ayoung541 tmp]$ diff ~/tftp/Minecraft.jar Minecraft.jar 
[ayoung@ayoung541 tmp]$ 

I think I need a syntax highlighter (for WordPress) that understands Rust.

Special thanks to this post that got me started.

Leave a Reply

Your email address will not be published. Required fields are marked *