From 848d17c186023d3e5ebc3c2a1a828545b2400726 Mon Sep 17 00:00:00 2001 From: Michal Grzedzicki Date: Sun, 25 Feb 2024 12:57:01 +0000 Subject: [PATCH] Add support for windowsize option (RFC 7440) --- README.md | 1 + examples/tftpd-dir.rs | 1 + rfcs/rfc7440.txt | 513 +++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/packet.rs | 7 + src/parse.rs | 6 + src/server/builder.rs | 16 ++ src/server/read_req.rs | 184 +++++++++---- src/server/server.rs | 1 + src/tests/external_client.rs | 4 + src/tests/packet.rs | 20 +- src/tests/rrq.rs | 65 +++-- 12 files changed, 733 insertions(+), 87 deletions(-) create mode 100644 rfcs/rfc7440.txt diff --git a/README.md b/README.md index 7aa318b..5a47671 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The following RFCs are implemented: * [RFC 2347] - TFTP Option Extension. * [RFC 2348] - TFTP Blocksize Option. * [RFC 2349] - TFTP Timeout Interval and Transfer Size Options. +* [RFC 7440] - TFTP Windowsize Option. Features: diff --git a/examples/tftpd-dir.rs b/examples/tftpd-dir.rs index 5cd068a..5936b8a 100644 --- a/examples/tftpd-dir.rs +++ b/examples/tftpd-dir.rs @@ -14,6 +14,7 @@ async fn main() -> Result<()> { .bind("0.0.0.0:6969".parse().unwrap()) // Workaround to handle cases where client is behind VPN .block_size_limit(1024) + .window_size(64) .build() .await?; diff --git a/rfcs/rfc7440.txt b/rfcs/rfc7440.txt new file mode 100644 index 0000000..7b33ff4 --- /dev/null +++ b/rfcs/rfc7440.txt @@ -0,0 +1,513 @@ + + + + + + +Internet Engineering Task Force (IETF) P. Masotta +Request for Comments: 7440 Serva +Category: Standards Track January 2015 +ISSN: 2070-1721 + + + TFTP Windowsize Option + +Abstract + + The "Trivial File Transfer Protocol" (RFC 1350) is a simple, + lockstep, file transfer protocol that allows a client to get or put a + file onto a remote host. One of its primary uses is in the early + stages of nodes booting from a Local Area Network (LAN). TFTP has + been used for this application because it is very simple to + implement. The employment of a lockstep scheme limits throughput + when used on a LAN. + + This document describes a TFTP option that allows the client and + server to negotiate a window size of consecutive blocks to send as an + alternative for replacing the single-block lockstep schema. The TFTP + option mechanism employed is described in "TFTP Option Extension" + (RFC 2347). + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc7440. + + + + + + + + + + + + + + +Masotta Standards Track [Page 1] + +RFC 7440 TFTP Windowsize Option January 2015 + + +Copyright Notice + + Copyright (c) 2015 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + +Table of Contents + + 1. Introduction ....................................................2 + 2. Conventions Used in This Document ...............................3 + 3. Windowsize Option Specification .................................3 + 4. Traffic Flow and Error Handling .................................4 + 5. Proof of Concept and Windowsize Evaluation ......................6 + 6. Congestion and Error Control ....................................7 + 7. Security Considerations .........................................8 + 8. References ......................................................9 + 8.1. Normative References .......................................9 + Author's Address ...................................................9 + +1. Introduction + + TFTP is virtually unused for Internet transfers today, TFTP is still + massively used in network boot/installation scenarios including EFI + (Extensible Firmware Interface). TFTP's inherently low transfer rate + has been, so far, partially mitigated by the use of the blocksize + negotiated extension [RFC2348]. Using this method, the original + limitation of 512-byte blocks are, in practice, replaced in Ethernet + environments by blocks no larger than 1468 Bytes to avoid IP block + fragmentation. This strategy produces insufficient results when + transferring big files, for example, the initial ramdisk of Linux + distributions or the PE images used in network installations by + Microsoft WDS/MDT/SCCM. Considering TFTP looks far from extinction + today, this document presents a negotiated extension, under the terms + of the "TFTP Option Extension" [RFC2347], that produces TFTP transfer + rates comparable to those achieved by modern file transfer protocols. + + + + + + + +Masotta Standards Track [Page 2] + +RFC 7440 TFTP Windowsize Option January 2015 + + +2. Conventions Used in This Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + [RFC2119]. + + In this document, these words will appear with that interpretation + only when in ALL CAPS. Lowercase uses of these words are not to be + interpreted as carrying the significance given in RFC 2119. + +3. Windowsize Option Specification + + The TFTP Read Request or Write Request packet is modified to include + the windowsize option as follows. Note that all fields except "opc" + MUST be ASCII strings followed by a single-byte NULL character. + + 2B string 1B string 1B string 1B string 1B + +-------+---~~---+----+---~~---+----+-----~~-----+----+---~~---+----+ + | opc |filename| 0 | mode | 0 | windowsize | 0 | #blocks| 0 | + +-------+---~~---+----+---~~---+----+-----~~-----+----+---~~---+----+ + + opc + The opcode field contains either a 1 for Read Requests or a 2 for + Write Requests, as defined in [RFC1350]. + + filename + The name of the file to be read or written, as defined in + [RFC1350]. + + mode + The mode of the file transfer: "netascii", "octet", or "mail", as + defined in [RFC1350]. + + windowsize + The windowsize option, "windowsize" (case insensitive). + + #blocks + The base-10 ASCII string representation of the number of blocks in + a window. The valid values range MUST be between 1 and 65535 + blocks, inclusive. The windowsize refers to the number of + consecutive blocks transmitted before stopping and waiting for the + reception of the acknowledgment of the last block transmitted. + + + + + + + + +Masotta Standards Track [Page 3] + +RFC 7440 TFTP Windowsize Option January 2015 + + + For example: + + +------+--------+----+-------+----+------------+----+----+----+ + |0x0001| foobar |0x00| octet |0x00| windowsize |0x00| 16 |0x00| + +------+--------+----+-------+----+------------+----+----+----+ + + is a Read Request for the file named "foobar" in octet transfer mode + with a windowsize of 16 blocks (option blocksize is not negotiated in + this example, the default of 512 Bytes per block applies). + + If the server is willing to accept the windowsize option, it sends an + Option Acknowledgment (OACK) to the client. The specified value MUST + be less than or equal to the value specified by the client. The + client MUST then either use the size specified in the OACK or send an + ERROR packet, with error code 8, to terminate the transfer. + + The rules for determining the final packet are unchanged from + [RFC1350] and [RFC2348]. + + The reception of a data window with a number of blocks less than the + negotiated windowsize is the final window. If the windowsize is + greater than the amount of data to be transferred, the first window + is the final window. + +4. Traffic Flow and Error Handling + + The next diagram depicts a section of the traffic flow between the + Data Sender (DSND) and the Data Receiver (DRCV) parties on a generic + windowsize TFTP file transfer. + + The DSND MUST cyclically send to the DRCV the agreed windowsize + consecutive data blocks before normally stopping and waiting for the + ACK of the transferred window. The DRCV MUST send to the DSND the + ACK of the last data block of the window in order to confirm a + successful data block window reception. + + In the case of an expected ACK not timely reaching the DSND + (timeout), the last received ACK SHALL set the beginning of the next + windowsize data block window to be sent. + + In the case of a data block sequence error, the DRCV SHOULD notify + the DSND by sending an ACK corresponding to the last data block + correctly received. The notified DSND SHOULD send a new data block + window whose beginning MUST be set based on the ACK received out of + sequence. + + Traffic with windowsize = 1 MUST be equivalent to traffic specified + by [RFC1350]. + + + +Masotta Standards Track [Page 4] + +RFC 7440 TFTP Windowsize Option January 2015 + +0 1 2 3 4 5 6 7 8 9 + X X + + +window_len=5 + +len 2+5-2 + For normative traffic not specifically addressed in this section, + please refer to [RFC1350] and its updates. + + [ DRCV ] <---traffic---> [ DSND ] + ACK# -> <- Data Block# window block# + ... + <- |DB n+01| 1 + <- |DB n+02| 2 + <- |DB n+03| 3 + <- |DB n+04| 4 + |ACK n+04| -> + <- |DB n+05| 1 + Error |<- |DB n+06| 2 + <- |DB n+07| 3 + |ACK n+05| -> + <- |DB n+06| 1 + <- |DB n+07| 2 + <- |DB n+08| 3 + <- |DB n+09| 4 + |ACK n+09| -> + <- |DB n+10| 1 + Error |<- |DB n+11| 2 + <- |DB n+12| 3 + |ACK n+10| ->| Error + <- |DB n+13| 4 + - timeout - + <- |DB n+10| 1 + <- |DB n+11| 2 + <- |DB n+12| 3 + <- |DB n+13| 4 + |ACK n+13| -> + ... + + Section of a Windowsize = 4 TFTP Transfer + Including Errors and Error Recovery + + + + + + + + + + + + + + + + +Masotta Standards Track [Page 5] + +RFC 7440 TFTP Windowsize Option January 2015 + + +5. Proof of Concept and Windowsize Evaluation + + Performance tests were run on the prototype implementation using a + variety of windowsizes and a fixed blocksize of 1456 bytes. The + tests were run on a lightly loaded Gigabit Ethernet, between two + Toshiba Tecra Core 2 Duo 2.2 Ghz laptops, in "octet" mode, + transferring a 180 MByte file. + + ^ + | + 300 + + Seconds | windowsize | time (s) + | ---------- ------ + | x 1 257 + 250 + 2 131 + | 4 76 + | 8 54 + | 16 42 + 200 + 32 38 + | 64 35 + | + | + 150 + + | + | x + | + 100 + + | + | x + | + 50 + x + | x + | x x + | + 0 +-//--+-----+-----+-----+-----+-----+-----+--> + 1 2 4 8 16 32 64 + + Windowsize (in Blocks of 1456 Bytes) + + Comparatively, the same 180 MB transfer performed over a drive mapped + on Server Message Block (SMB) / Common Internet File System (CIFS) on + the same scenario took 23 seconds. + + + + + + + + + +Masotta Standards Track [Page 6] + +RFC 7440 TFTP Windowsize Option January 2015 + + + The comparison of transfer times (without a gateway) between the + standard lockstep schema and the negotiated windowsizes are: + + Windowsize | Time Reduction (%) + ---------- ----------------- + 1 -0% + 2 -49% + 4 -70% + 8 -79% + 16 -84% + 32 -85% + 64 -86% + + The transfer time decreases with the use of a windowed schema. The + reason for the reduction in time is the reduction in the number of + the required synchronous acknowledgements exchanged. + + The choice of appropriate windowsize values on a particular scenario + depends on the underlying networking technology and topology, and + likely other factors as well. Operators SHOULD test various values + and SHOULD be conservative when selecting a windowsize value because + as the former table and chart shows, there is a point where the + benefit of continuing to increase the windowsize is subject to + diminishing returns. + +6. Congestion and Error Control + + From a congestion control (CC) standpoint, the number of blocks in a + window does not pose an intrinsic threat to the ability of + intermediate devices to signal congestion through drops. The rate at + which TFTP UDP datagrams are sent SHOULD follow the CC guidelines in + Section 3.1 of [RFC5405]. + + From an error control standpoint, while [RFC1350] and subsequent + updates do not specify a circuit breaker (CB), existing + implementations have always chosen to fail under certain + circumstances. Implementations SHOULD always set a maximum number of + retries for datagram retransmissions, imposing an appropriate + threshold on error recovery attempts, after which a transfer SHOULD + always be aborted to prevent pathological retransmission conditions. + + + + + + + + + + + +Masotta Standards Track [Page 7] + +RFC 7440 TFTP Windowsize Option January 2015 + + + An implementation example scaled for an Ethernet environment + (1 Gbit/s, MTU=1500) would be to set: + + windowsize = 8 + blksize = 1456 + maximum retransmission attempts per block/window = 6 + timeout between retransmissions = 1 S + minimum inter-packet delay = 80 uS + + Implementations might well choose other values based on expected + and/or tested operating conditions. + +7. Security Considerations + + TFTP includes no login or access control mechanisms. Care must be + taken when using TFTP for file transfers where authentication, access + control, confidentiality, or integrity checking are needed. Note + that those security services could be supplied above or below the + layer at which TFTP runs. Care must also be taken in the rights + granted to a TFTP server process so as not to violate the security of + the server's file system. TFTP is often installed with controls such + that only files that have public read access are available via TFTP. + Also listing, deleting, renaming, and writing files via TFTP are + typically disallowed. TFTP file transfers are NOT RECOMMENDED where + the inherent protocol limitations could raise insurmountable + liability concerns. + + TFTP includes no protection against an on-path attacker; care must be + taken in controlling windowsize values according to data sender, data + receiver, and network environment capabilities. TFTP service is + frequently associated with bootstrap and initial provisioning + activities; servers in such an environment are in a position to + impose device or network specific throughput limitations as + appropriate. + + This document does not add any security controls to TFTP; however, + the specified extension does not pose additional security risks + either. + + + + + + + + + + + + + +Masotta Standards Track [Page 8] + +RFC 7440 TFTP Windowsize Option January 2015 + + +8. References + +8.1. Normative References + + [RFC1350] Sollins, K., "The TFTP Protocol (Revision 2)", STD 33, + RFC 1350, July 1992, + . + + [RFC2347] Malkin, G. and A. Harkin, "TFTP Option Extension", RFC + 2347, May 1998, . + + [RFC2348] Malkin, G. and A. Harkin, "TFTP Blocksize Option", RFC + 2348, May 1998, . + + [RFC5405] Eggert, L. and G. Fairhurst, "Unicast UDP Usage + Guidelines for Application Designers", BCP 145, RFC 5405, + November 2008, . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997, + . + +Author's Address + + Patrick Masotta + Serva + + EMail: patrick.masotta.ietf@vercot.com + URI: http://www.vercot.com/~serva/ + + + + + + + + + + + + + + + + + + + + + + +Masotta Standards Track [Page 9] + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index eb1e1f4..a57d9a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ //! * [RFC 2347] - TFTP Option Extension. //! * [RFC 2348] - TFTP Blocksize Option. //! * [RFC 2349] - TFTP Timeout Interval and Transfer Size Options. +//! * [RFC 7440] - TFTP Windowsize Option. //! //! Features: //! @@ -55,6 +56,7 @@ //! [RFC 2347]: https://tools.ietf.org/html/rfc2347 //! [RFC 2348]: https://tools.ietf.org/html/rfc2348 //! [RFC 2349]: https://tools.ietf.org/html/rfc2349 +//! [RFC 7440]: https://tools.ietf.org/html/rfc7440 pub mod server; diff --git a/src/packet.rs b/src/packet.rs index 3d679da..fc6cccd 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -63,6 +63,7 @@ pub(crate) struct Opts { pub block_size: Option, pub timeout: Option, pub transfer_size: Option, + pub window_size: Option, } impl PacketType { @@ -161,6 +162,12 @@ impl Opts { buf.put_slice(transfer_size.to_string().as_bytes()); buf.put_u8(0); } + + if let Some(window_size) = self.window_size { + buf.put_slice(&b"windowsize\0"[..]); + buf.put_slice(window_size.to_string().as_bytes()); + buf.put_u8(0); + } } } diff --git a/src/parse.rs b/src/parse.rs index 5056d54..f2427b5 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -76,6 +76,12 @@ pub(crate) fn parse_opts(mut input: &[u8]) -> Option { if let Ok(val) = u64::from_str(val) { opts.transfer_size = Some(val); } + } else if name.eq_ignore_ascii_case("windowsize") { + if let Ok(val) = u16::from_str(val) { + if val >= 1 { + opts.window_size = Some(val); + } + } } input = rest; diff --git a/src/server/builder.rs b/src/server/builder.rs index 171a053..ee7869b 100644 --- a/src/server/builder.rs +++ b/src/server/builder.rs @@ -18,6 +18,7 @@ pub struct TftpServerBuilder { socket: Option>, timeout: Duration, block_size_limit: Option, + window_size: Option, max_send_retries: u32, ignore_client_timeout: bool, ignore_client_block_size: bool, @@ -63,6 +64,7 @@ impl TftpServerBuilder { socket: None, timeout: Duration::from_secs(3), block_size_limit: None, + window_size: None, max_send_retries: 100, ignore_client_timeout: false, ignore_client_block_size: false, @@ -132,6 +134,19 @@ impl TftpServerBuilder { } } + /// Set maximum window size. + /// + /// Client can request a specific window size (RFC7440). Use this option if you + /// want to use it. + /// + /// Default: None, window scaling disabled + pub fn window_size(self, window_size: u16) -> Self { + TftpServerBuilder { + window_size: Some(window_size), + ..self + } + } + /// Set maximum send retries for a data block. /// /// On timeout server will try to send the data block again. When retries are @@ -178,6 +193,7 @@ impl TftpServerBuilder { let config = ServerConfig { timeout: self.timeout, block_size_limit: self.block_size_limit, + window_size: self.window_size, max_send_retries: self.max_send_retries, ignore_client_timeout: self.ignore_client_timeout, ignore_client_block_size: self.ignore_client_block_size, diff --git a/src/server/read_req.rs b/src/server/read_req.rs index 171e10e..dfd3bf6 100644 --- a/src/server/read_req.rs +++ b/src/server/read_req.rs @@ -25,6 +25,7 @@ where timeout: Duration, max_send_retries: u32, oack_opts: Option, + window_size: usize, } impl<'r, R> ReadRequest<'r, R> @@ -47,6 +48,11 @@ where .map(usize::from) .unwrap_or(DEFAULT_BLOCK_SIZE); + // Default window size is 1 as per rfc7440 + let negotiated_window_size: usize = + oack_opts.as_ref().and_then(|o| o.window_size).unwrap_or(1u16) + as usize; + let timeout = oack_opts .as_ref() .and_then(|o| o.timeout) @@ -67,6 +73,7 @@ where timeout, max_send_retries: config.max_send_retries, oack_opts, + window_size: negotiated_window_size, }) } @@ -83,79 +90,120 @@ where } async fn try_handle(&mut self) -> Result<()> { - let mut block_id: u16 = 0; + let mut window: Vec = Vec::with_capacity(self.window_size); + let mut block_id: u16; + let mut window_base: u16 = 1; + let mut buf: Bytes; + let mut is_last_block: bool; + + (buf, is_last_block) = self.fill_data_block(window_base).await?; + + // Send OACK after we manage to read the first block from reader. + // + // We do this because we want to give the developers the option to + // produce an error after they construct a reader. + if let Some(opts) = self.oack_opts.as_ref() { + trace!("RRQ OACK (peer: {}, opts: {:?}", &self.peer, &opts); + let mut buff = BytesMut::new(); + Packet::OAck(opts.to_owned()).encode(&mut buff); + + self.send_window(&[buff.split().freeze()], 0).await?; + } + // push first data packet to the window + window.push(buf); - // Send file to client loop { - let is_last_block; - - // Reclaim buffer - self.buffer.reserve(PACKET_DATA_HEADER_LEN + self.block_size); + // calculate next block_id, window might not be empty + block_id = window_base.wrapping_add(window.len() as u16); + + while !is_last_block && (window.len() < self.window_size) { + // we still have data and window is not full + (buf, is_last_block) = self.fill_data_block(block_id).await?; + window.push(buf); + block_id = block_id.wrapping_add(1); + } - // Encode head of Data packet - block_id = block_id.wrapping_add(1); - Packet::encode_data_head(block_id, &mut self.buffer); + let blocks_acked = self.send_window(&window, window_base).await?; + window_base = window_base.wrapping_add(blocks_acked); - // Read block in self.buffer - let buf = unsafe { - let uninit_buf = self.buffer.chunk_mut(); + // remove acked blocks from window + if blocks_acked == window.len() as u16 { + window.clear() + } else { + for _ in 0..blocks_acked { + window.remove(0); + } + } - let data_buf = slice::from_raw_parts_mut( - uninit_buf.as_mut_ptr(), - uninit_buf.len(), - ); + if is_last_block && window.is_empty() { + // transfer is dome + break; + } + } - let len = self.read_block(data_buf).await?; - is_last_block = len < self.block_size; + trace!("RRQ request served (peer: {})", &self.peer); + Ok(()) + } - self.buffer.advance_mut(len); - self.buffer.split().freeze() - }; + async fn fill_data_block( + &mut self, + block_id: u16, + ) -> Result<(Bytes, bool), Error> { + Packet::encode_data_head(block_id, &mut self.buffer); - // Send OACK after we manage to read the first block from reader. - // - // We do this because we want to give the developers the option to - // produce an error after they construct a reader. - if let Some(opts) = self.oack_opts.take() { - trace!("RRQ OACK (peer: {}, opts: {:?}", &self.peer, &opts); + // Read block in self.buffer + let (buf, len) = unsafe { + let uninit_buf = self.buffer.chunk_mut(); - let mut buf = BytesMut::new(); - Packet::OAck(opts.to_owned()).encode(&mut buf); + let data_buf = slice::from_raw_parts_mut( + uninit_buf.as_mut_ptr(), + uninit_buf.len(), + ); - self.send(buf.split().freeze(), 0).await?; - } + let len = self.read_block(data_buf).await?; - // Send Data packet - self.send(buf, block_id).await?; + self.buffer.advance_mut(len); + (self.buffer.split().freeze(), len) + }; - if is_last_block { - break; - } + if len == self.block_size { + self.buffer.reserve(PACKET_DATA_HEADER_LEN + self.block_size); + Ok((buf, false)) + } else { + // last data block, we won't read anymore + Ok((buf, true)) } - - trace!("RRQ request served (peer: {})", &self.peer); - Ok(()) } - async fn send(&mut self, packet: Bytes, block_id: u16) -> Result<()> { + /// Sends packets contained in a window and waits for client to acknowledge them. Returns amount + /// of packets acknowledged. + async fn send_window( + &mut self, + window: &[Bytes], + window_base: u16, + ) -> Result { // Send packet until we receive an ack for _ in 0..=self.max_send_retries { - self.socket.send_to(&packet[..], self.peer).await?; + for packet in window { + self.socket.send_to(&packet[..], self.peer).await?; + } - match self.recv_ack(block_id).await { - Ok(_) => { + match self.recv_ack(window_base, window.len() as u16).await { + Ok(blocks_acked) => { trace!( - "RRQ (peer: {}, block_id: {}) - Received ACK", + "RRQ (peer: {}, window_base: {}, blocks_acked: {}, window_len: {}) - Received ACK", &self.peer, - block_id + window_base, + blocks_acked, + window.len() ); - return Ok(()); + return Ok(blocks_acked); } Err(ref e) if e.kind() == io::ErrorKind::TimedOut => { trace!( "RRQ (peer: {}, block_id: {}) - Timeout", &self.peer, - block_id + window_base ); continue; } @@ -163,10 +211,15 @@ where } } - Err(Error::MaxSendRetriesReached(self.peer, block_id)) + Err(Error::MaxSendRetriesReached(self.peer, window_base)) } - async fn recv_ack(&mut self, block_id: u16) -> io::Result<()> { + /// Waits for ack packet, returns amount of packets acknowledged. + async fn recv_ack( + &mut self, + window_base: u16, + window_len: u16, + ) -> io::Result { // We can not use `self` within `async_std::io::timeout` because not all // struct members implement `Sync`. So we borrow only what we need. let socket = &mut self.socket; @@ -187,15 +240,31 @@ where if let Ok(Packet::Ack(recved_block_id)) = Packet::decode(&buf[..len]) { - if recved_block_id == block_id { - return Ok(()); + let window_end = window_base.wrapping_add(window_len); + + if window_end > window_base { + // window_end did not wrap + if (recved_block_id >= window_base) && recved_block_id < window_end { + // number of blocks acked + return Ok(recved_block_id-window_base+1u16); + } + else { + trace!("Unexpected ack packet {recved_block_id}, window_base: {window_base}, window_len: {window_len}"); + } + }else { + // window_end wrapped + if recved_block_id >= window_base { + return Ok(1u16+(recved_block_id-window_base)); + } else if recved_block_id < window_end { + return Ok(1u16+recved_block_id+window_len-window_end); + } else { + trace!("Unexpected ack packet {recved_block_id}, window_base: {window_base}, window_len: {window_len}"); + } } } } }) - .await?; - - Ok(()) + .await } async fn read_block(&mut self, buf: &mut [u8]) -> Result { @@ -235,6 +304,13 @@ fn build_oack_opts( opts.transfer_size = Some(file_size); } + if let (Some(client_window_size), Some(server_window_size)) = + (config.window_size, req.opts.window_size) + { + opts.window_size = + Some(cmp::min(client_window_size, server_window_size)) + } + if opts == Opts::default() { None } else { diff --git a/src/server/server.rs b/src/server/server.rs index 24c7fc7..1c06690 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -31,6 +31,7 @@ where pub(crate) struct ServerConfig { pub(crate) timeout: Duration, pub(crate) block_size_limit: Option, + pub(crate) window_size: Option, pub(crate) max_send_retries: u32, pub(crate) ignore_client_timeout: bool, pub(crate) ignore_client_block_size: bool, diff --git a/src/tests/external_client.rs b/src/tests/external_client.rs index 07fbd04..dc8e83b 100644 --- a/src/tests/external_client.rs +++ b/src/tests/external_client.rs @@ -12,6 +12,7 @@ pub fn external_tftp_recv( filename: &str, server: SocketAddr, block_size: Option, + window_size: Option, ) -> io::Result { let tmp = tempdir()?; let path = tmp.path().join("data"); @@ -25,6 +26,9 @@ pub fn external_tftp_recv( if let Some(block_size) = block_size { cmd.arg("--option").arg(format!("blksize {}", block_size)); } + if let Some(window_size) = window_size { + cmd.arg("--option").arg(format!("windowsize {}", window_size)); + } cmd.arg("-g") .arg("-l") diff --git a/src/tests/packet.rs b/src/tests/packet.rs index 07d7f0c..1e04601 100644 --- a/src/tests/packet.rs +++ b/src/tests/packet.rs @@ -62,7 +62,8 @@ fn check_rrq() { opts: Opts { block_size: Some(123), timeout: Some(3), - transfer_size: Some(5556) + transfer_size: Some(5556), + window_size: None, } } )); @@ -123,7 +124,7 @@ fn check_wrq() { assert!(matches!(packet, Err(ref e) if matches!(e, Error::InvalidPacket))); let packet = Packet::decode( - b"\x00\x02abc\0octet\0blksize\0123\0timeout\03\0tsize\05556\0", + b"\x00\x02abc\0octet\0blksize\0123\0timeout\03\0tsize\05556\0windowsize\04\0", ); assert!(matches!(packet, Ok(Packet::Wrq(ref req)) @@ -133,14 +134,15 @@ fn check_wrq() { opts: Opts { block_size: Some(123), timeout: Some(3), - transfer_size: Some(5556) + transfer_size: Some(5556), + window_size: Some(4) } } )); assert_eq!( packet_to_bytes(&packet.unwrap()), - b"\x00\x02abc\0octet\0blksize\0123\0timeout\03\0tsize\05556\0"[..] + b"\x00\x02abc\0octet\0blksize\0123\0timeout\03\0tsize\05556\0windowsize\04\0"[..] ); let packet = Packet::decode(b"\x00\x02abc\0octet\0blksizeX\0123\0"); @@ -221,26 +223,23 @@ fn check_oack() { assert!(matches!(packet, Ok(Packet::OAck(ref opts)) if opts == &Opts { block_size: Some(123), - timeout: None, - transfer_size: None + ..Default::default() } )); let packet = Packet::decode(b"\x00\x06timeout\03\0"); assert!(matches!(packet, Ok(Packet::OAck(ref opts)) if opts == &Opts { - block_size: None, timeout: Some(3), - transfer_size: None + ..Default::default() } )); let packet = Packet::decode(b"\x00\x06tsize\05556\0"); assert!(matches!(packet, Ok(Packet::OAck(ref opts)) if opts == &Opts { - block_size: None, - timeout: None, transfer_size: Some(5556), + ..Default::default() } )); @@ -251,6 +250,7 @@ fn check_oack() { block_size: Some(123), timeout: Some(3), transfer_size: Some(5556), + ..Default::default() } )); } diff --git a/src/tests/rrq.rs b/src/tests/rrq.rs index b51ba41..bfc96af 100644 --- a/src/tests/rrq.rs +++ b/src/tests/rrq.rs @@ -12,7 +12,12 @@ use super::external_client::*; use super::handlers::*; use crate::server::TftpServerBuilder; -fn transfer(file_size: usize, block_size: Option) { +fn transfer( + file_size: usize, + block_size: Option, + server_window_size: Option, + client_window_size: Option, +) { let ex = Arc::new(Executor::new()); let transfered = Rc::new(Cell::new(false)); @@ -27,6 +32,7 @@ fn transfer(file_size: usize, block_size: Option) { // bind let tftpd = TftpServerBuilder::with_handler(handler) .bind("127.0.0.1:0".parse().unwrap()) + .window_size(server_window_size.unwrap_or(1)) .build() .await .unwrap(); @@ -35,7 +41,7 @@ fn transfer(file_size: usize, block_size: Option) { // start client let mut tftp_recv = Unblock::new(()); let tftp_recv = tftp_recv.with_mut(move |_| { - external_tftp_recv("test", addr, block_size) + external_tftp_recv("test", addr, block_size, client_window_size) }); // start server @@ -57,63 +63,76 @@ fn transfer(file_size: usize, block_size: Option) { assert!(transfered.get()); } - #[test] fn transfer_0_bytes() { - transfer(0, None); - transfer(0, Some(1024)); + transfer(0, None, None, None); + transfer(0, Some(1024), None, None); + transfer(0, Some(1024), Some(8), Some(8)); } #[test] fn transfer_less_than_block() { - transfer(1, None); - transfer(123, None); - transfer(511, None); - transfer(1023, Some(1024)); + transfer(1, None, None, None); + transfer(123, None, None, None); + transfer(511, None, None, None); + transfer(1023, Some(1024), None, None); + transfer(1, None, Some(8), Some(8)); + transfer(123, None, Some(8), Some(8)); + transfer(511, None, Some(8), Some(8)); + transfer(1023, Some(1024), Some(8), Some(8)); } #[test] fn transfer_block() { - transfer(512, None); - transfer(1024, Some(1024)); + transfer(512, None, None, None); + transfer(1024, Some(1024), None, None); + transfer(1024, Some(1024), Some(8), Some(8)); } #[test] fn transfer_more_than_block() { - transfer(512 + 1, None); - transfer(512 + 123, None); - transfer(512 + 511, None); - transfer(1024 + 1, Some(1024)); - transfer(1024 + 123, Some(1024)); - transfer(1024 + 1023, Some(1024)); + transfer(512 + 1, None, None, None); + transfer(512 + 123, None, None, None); + transfer(512 + 511, None, None, None); + transfer(1024 + 1, Some(1024), None, None); + transfer(1024 + 123, Some(1024), None, None); + transfer(1024 + 1023, Some(1024), None, None); + transfer(1024 + 1023, Some(1024), Some(8), Some(4)); } #[test] fn transfer_1mb() { - transfer(1024 * 1024, None); - transfer(1024 * 1024, Some(1024)); + transfer(1024 * 1024, None, None, None); + transfer(1024 * 1024, Some(1024), None, None); + transfer(1024 * 1024, Some(1024), Some(16), Some(8)); } #[test] #[ignore] fn transfer_almost_32mb() { - transfer(32 * 1024 * 1024 - 1, None); + transfer(32 * 1024 * 1024 - 1, None, None, None); + + for window_size in + [None, Some(1), Some(2), Some(3), Some(4), Some(5), Some(8), Some(16)] + { + transfer(32 * 1024 * 1024 - 1, None, window_size, window_size); + } } #[test] #[ignore] fn transfer_32mb() { - transfer(32 * 1024 * 1024, None); + transfer(32 * 1024 * 1024, None, None, None); } #[test] #[ignore] fn transfer_more_than_32mb() { - transfer(33 * 1024 * 1024 + 123, None); + transfer(33 * 1024 * 1024 + 123, None, None, None); } #[test] #[ignore] fn transfer_more_than_64mb() { - transfer(65 * 1024 * 1024 + 123, None); + transfer(65 * 1024 * 1024 + 123, None, None, None); }