I have tried to look into why the TFTP servers that are typically available on Linux (namely tftp-hpa and atftp) would behave differently than Tftpd32 or Tftpd64 which is configured to use the "anticipation window" option.
The "anticipation window" behavior
On the Tftpd32's website, there is a FAQ section which contains the question "How do i tune the anticipation window ?". The answer mentions that:
Tftpd32 will send data packets without waiting for acknowledgements
I assume that it refers to the data packets within the anticipation window.
Also, on the Tftpd64's Wiki, there is a Settings section which contains the following note next to the description of the "anticipation window" option:
Tftpd32 is able to send packets before receiving acknowledgements.
Since the default size of the TFTP data packet is 512 bytes, when setting the anticipation window to 1000 bytes, it instructs Tftpd32/Tftpd64 to extend the number of 512 byte data packets that are sent to clients without waiting for their acknowledgement by 1 (1000 / 512 is ~1.95). So, in effect, instead of a typical operation where only one data packet is sent to a client and then the server waits for its acknowledgement, Tftpd32/Tftpd64 would send two data packets and then wait for the client's acknowledgements. The code of Tftpd64 seems to confirm that the anticipation window's size is used to determine the number of extra data packets to send before waiting for their acknowledgement.
Such a behavior of the TFTP server resembles the behavior introduced in RFC 7440: TFTP Windowsize Option. However, if I understand correctly, RFC 7440 specifies that a larger window size needs to be asked for by the client. The TFTP download procedure implemented within the U-Boot bootloader of the TP-Link Archer C2 devices does not specify the windowsize
option when performing the TFTP Read Request. It only specifies the timeout
option, as captured by tcpdump
:
192.168.0.1.2439 > 192.168.0.100.69: [no cksum] TFTP, length 27, RRQ "test.bin" octet timeout 1
According to the RFC 1350: THE TFTP PROTOCOL (REVISION 2), the data packets must be acknowledged by the client before the server can send more data packets:
Each data packet contains one block of data, and must be acknowledged by an acknowledgment packet before the next packet can be sent.
So, the behavior of Tftpd32/Tftpd64 with the "anticipation window" of at least 512 bytes does not follow the original TFTP specification. It resembles the newer RFC 7440 specification with the windowsize
option but it only implements a part of it while omitting e.g. the support for clients specifying the desired TFTP window size.
That being said, such a behavior is able to work-around the apparent issues with some TP-Link Archer C2 devices where their U-Boot bootloader is unable to successfully download the firmware update over TFTP. I have tried using Tftpd64 with "anticipation window" of 1000 that was running in a Windows virtual machine. I can confirm that the TP-Link Archer C2 device that has TFTP flashing issues when used with a regular TFTP server (serial number 2161374002651) was able to use Tftpd64 configured as described above to successfully download the firmware.
However, the speed was rather low, only around 10 kB/s. I have tried experimenting with different values of the "anticipation window" option but I was unable to find a value which would significantly improve the speed, i.e. by more than ~30 %. Larger "anticipation window" typically caused speed to decrease and the number of timeouts to increase. Without the "anticipation window" option enabled, the behavior was the same as with any other regular TFTP server, i.e. the transfer would typically end after a few hundred data packets because of the timeouts.
Linux alternatives
I have also looked into the options of using a Linux TFTP server that would be capable of sending multiple data packets without acknowledgement. The two most commonly used TFTP servers on Linux, namely tftp-hpa and atftp, do not support the RFC 7440 windowsize
option. The author of tftp-hpa has been asked to add support for it but there does not seem to be any activity since then. However, there are forks of tftp-hpa, namely ClausKlein/tftp-hpa and mellowcandle/tftp-hpa-rfc7440. which support RFC 7440. There are also several TFTP servers written in Python that claim to support RFC 7440 windowsize
option, in particular: matkaczmarek/TFTP-RFC7440_1350, sirMackk/py3tftp and apardyl/PyTFTPd.
While there are multiple implementations of TFTP servers with RFC 7440 support available for Linux, that alone is insufficient for the purpose of sending a firmware to a TP-Link Archer C2 device with TFTP flashing issues. The reason is that, as mentioned above, the TFTP client within the U-Boot bootloader of the TP-Link Archer C2 device does not specify the windowsize
option. As a result, even the TFTP servers with full support of RFC 7440 would keep using the window size of 1.
In order to make the TFTP download work even on the TP-Link Archer C2 devices with TFTP flashing issues, it is necessary to instruct the TFTP server with RFC 7440 windowsize
support to always use a window of size at least 2, regardless of the client's requirement. Since the above mentioned TFTP servers do not offer an option to customize the window size manually, it is necessary to update their code.
I have tried to do that with apardyl/PyTFTPd because it is written in Python, has no dependencies on external packages and its code seems reasonably short and simple. I have made the following two changes:
--- a/tftp_common.py
+++ b/tftp_common.py
@@ -190,7 +190,7 @@ class RQPacket(TFTPPacket):
@staticmethod
def parse_rq(packet: bytes, constructor):
block_size = 512
- window_size = 1
+ window_size = 2 # temporarily changed
bad_opts = False
packet = packet[2:].split(b'\0')
for i in range(2, len(packet), 2):
@@ -198,6 +198,8 @@ class RQPacket(TFTPPacket):
block_size = int(packet[i + 1])
elif packet[i].lower() == b'windowsize':
window_size = int(packet[i + 1])
+ elif packet[i].lower() == b'timeout':
+ pass # not implemented, temporarily ignoring
elif packet[i] != b'':
bad_opts = True
return constructor(packet[0], TransferModes.get_mode(packet[1]), block_size, window_size, bad_opts)
The first change adjusts the TFTP window size to 2. The second change skips the timeout
option, which is not implemented by PyTFTPd
but is part of the TFTP Read Request packet sent by the TFTP client within the U-Boot bootloader of TP-Link Archer C2.
I have then tried to run the PyTFTPd
TFTP server with the above mentioned updates:
# ./tftpd.py 69 /srv/tftp
and initiate the TFTP download from the device's bootloader. There were no timeouts, the transfer was successful and it was also reasonably fast. The transfer speed was about 200 kB/s, i.e. roughly 20 times faster than with Tftpd64 during my experiments.
Summary
I can confirm that a customized TFTP server that sends two data packets at once before waiting for acknowledgements regardless of whether the client specifies the RFC 7440 windowsize
option or not, can successfully be used to send firmware to a TP-Link Archer C2 device that experiences TFTP flashing issues with standards-compliant TFTP servers.
I think that it would be interesting to know why this kind of work-around in particular helps. It might also be interesting to investigate the root cause of why some devices are able to download firmware correctly from standards-compliant TFTP servers while some others are not.