rust-embedded-community / embedded-nal

An Embedded Network Abstraction Layer
Apache License 2.0
177 stars 25 forks source link

#use of connect() for tftp protocol #75

Open sor-ca opened 2 years ago

sor-ca commented 2 years ago

Hello! I'm trying to write a tftp-client on the basis of embedded-nal UdpClientStack According to tftp protocol, the client (randomly) chooses its own port, e.g. client:8080. Then the client sends a request to the server on port server:69. The server (randomly) chooses a port as well, e.g. server:8081, and sends an answer to the client on port client:8080. The whole transfer (read or write) will now use the two ports client:8080 and server:8081. The problem is that according to embedded-nal (and its implementation std-embedded-nal) fn connect() has not the same functions as in std::UdpSocket. It not just connect the socket with remote address (as in std::UdpSocket), but creates a socket by binding with unspecified port and then connect with remote address. So, each time when we use embedded-nal::connect, we create a new socket which is connected with specified remote address. It seems impossible to connect the existing socket with another remote address according to embedded-nal... or i don't understand something :(((

Maybe for tftp-client it is necessary to use create socket without connection with the help of UdpFullStack, then to bind it with specified local port and send messages with the help of send_to()? Or maybe you may advice me a way to change the remote address of the socket according to UdpClientStack? Thank you for answers

ryan-summers commented 2 years ago

Question: Are you trying to write a TFTP client or a TFTP server? A TFTP client in an embedded context is usually some PC where someone is trying to write data to or read data from an embedded device (which is operating as the TFTP server, e.g. serving a file, such as the application image, to the client for modification over the network). It seems odd that the embedded device would be the TFTP client, since that implies it would be requesting data from a PC.

So, each time when we use embedded-nal::connect, we create a new socket which is connected with specified remote address. It seems impossible to connect the existing socket with another remote address according to embedded-nal... or i don't understand something :(((

UdpClientStack::connect takes a &mut self reference, so you can call stack.connect(&mut socket) as many times as you want with the same socket to change the address that the socket is connected to.

You mention that you want to write a TFTP client. In this case, you need to:

  1. Send a message to the server's port 69 to initiate the request
  2. Receive a response from the server from some other ephemeral port
  3. Redirect your future communications to the port from step (2)

With this data flow, it seems like you would:

// Send a TFTP request to a server
let mut socket = stack.socket().unwrap();
stack.connect(&mut socket, "server:69").unwrap();
server.send(&mut socket, TFTP_REQUEST).unwrap();

// Read the TFTP response from the server.
let (resp_size, server_addr) = server.recv(&mut socket, &mut TFTP_RESPONSE).unwrap();

// Reconnect to the port that the server is using for the transfer now that we have initiated the transaction.
server.connect(&mut socket, server_addr).unwrap();
sor-ca commented 2 years ago

Thank you for reply!

Urhengulas commented 2 years ago

@ryan-summers said:

// Send a TFTP request to a server
let mut socket = stack.socket().unwrap();
stack.connect(&mut socket, "server:69").unwrap();
server.send(&mut socket, TFTP_REQUEST).unwrap();

// Read the TFTP response from the server.
let (resp_size, server_addr) = server.recv(&mut socket, &mut TFTP_RESPONSE).unwrap();

// Reconnect to the port that the server is using for the transfer now that we have initiated the transaction.
server.connect(&mut socket, server_addr).unwrap();

We've tried this flow, but (at least with std-embedded-nal) this will only receive responses from server:69, but not from, e.g., server:8081.

But replacing UdpFullStack::bind with UdpClientStack::connect will probably help.

ryan-summers commented 2 years ago

This may come down to the implementation of std-embedded-nal as well, but my reading shows that the duplicate connect() should result in a new bind call to the updated port: https://gitlab.com/chrysn/std-embedded-nal/-/blob/master/src/udp.rs#L60

I'd recommend taking a look at Wireshark captures to see what's happening as well for debugging.

Urhengulas commented 2 years ago

my reading shows that the duplicate connect() should result in a new bind call to the updated port: https://gitlab.com/chrysn/std-embedded-nal/-/blob/master/src/udp.rs#L60

It would, but the server.recv before that will never return, because it will never get an answer from server:69 (but from, e.g., server:8081).

ryan-summers commented 2 years ago

Ah indeed I was mistaken.

When trying to implement a "TFTP Client", you need to (confusingly) implement UDP server traits.

// Send a TFTP request to a server
let mut socket = stack.socket().unwrap();
stack.bind(&mut socket, 8081).unwrap();
stack.send_to(&mut socket, "server:69", TFTP_REQUEST).unwrap();

// Read the TFTP response from the server. This will be an ACK (if writing) or a DAT (if reading).
let (resp_size, server_addr) = stack.receive(&mut socket, &mut TFTP_RESPONSE).unwrap();

// TODO: Service the incoming frames if receiving, produce the outgoing frames if writing

That being said, generally an embedded "TFTP Client" is not what a user would want, but rather a "TFTP Server". The TFTP server would make files (on the embedded device) readable and writable by a TFTP client (e.g. a PC).

That way, a user on a standard PC could e.g. bootload an embedded device by writing the application.bin file on the embedded device, or update images on some e.g. embedded gui by writing to logo.png from a PC.

Urhengulas commented 2 years ago

That being said, generally an embedded "TFTP Client" is not what a user would want, but rather a "TFTP Server". The TFTP server would make files (on the embedded device) readable and writable by a TFTP client (e.g. a PC).

That way, a user on a standard PC could e.g. bootload an embedded device by writing the application.bin file on the embedded device, or update images on some e.g. embedded gui by writing to logo.png from a PC.

That confusion was created by me. I understood that in the over the air update usecase (which you are describing, i think)the embedded device would download the file (firmware, image etc.) from the PC, therefore act as a client. But it makes more sense as you describe it, since otherwise the embedded device would need to initiate the transfer.

chrysn commented 1 year ago

It seems to me that part of the trouble is that it's not clear from the trait definitions whether it'd even be allowed for a socket to be connect()ed multiple times, and/or how that interacts with binding. #33 tracks a language based approach for clarifying that, but documentation might suffice.