Network programming in Linux

Trivial File Transfer Protocol (TFTP)


Introduction
Getting started with libpcap

Extracting Ethernet information
Internet Protocol (IP)

Filtering captured datagrams
Capturing datagrams offline

Address Resolution Protocol (ARP)
Internet Control Message Protocol (ICMP)
Transmission Control Protocol (TCP)
User Datagram Protocol (UDP)
Trivial File Transfer Protocol (TFTP)

Injecting datagrams with libnet
Implementing ping
Implementing traceroute

Download source code


Trivial File Transfer Protocol (TFTP) is an application protocol notable for its simplicity. This file exchange is very limited and unsecure, providing no authentication, so it's rarely used interactively by users. It is generally used for automated transfer of configuration or boot files between machines in a secured environment. Due to its lack of security, it is dangerous to use TFTP over the Internet; thus TFTP is generally only used in private, local networks.

TFTP is implemented on top of the User Datagram Protocol (UDP) using port number 69. TFTP is designed to be small and easy to implement, and therefore it lacks most of the features of a regular file transfer protocol. TFTP only reads and writes files (or mail) from/to a remote server. It cannot list directories and has no provisions for user authentication.

In TFTP, a transfer begins with a request to read or write a file, which also serves as connection request. If the server grants the request, the connection is opened and the file is sent in fixed length blocks of 512 bytes. Each datagram contains one block of data, and must be acknowledged by the receiver before the next datagram can be sent. A datagram with payload of less than 512 bytes signals termination of a transfer. If a datagram gets lost in the network, the intended recipient will timeout and may retransmit his last datagram (which may be data or an acknowledgment), thus causing the sender of the lost datagram to retransmit it. Both machines involved in a transfer are considered senders and receivers. One sends data and receives acknowledgments, the other sends acknowledgments and receives data.

TFTP defines three modes of transfer:

  1. Netascii: a modified form of ASCII consisting of an 8-bit extension of the 7-bit ASCII character space from 0x20 to 0x7F (the printable characters and the space) and eight of the control characters. The allowed control characters include Null (0x00), line feed (LF = 0x0A), and carriage return (CR = 0x0D). Netascii also requires that end of line marker on a host be translated to the character pair CR+LF for transmission, and any CR must be followed by either LF or Null.

  2. Octet: allows for the transfer of arbitrary 8-bit bytes, with the received file identical to the sent file (e.g. end of line markers are unaltered).

  3. Mail: uses Netascii transfer, but the file is sent to an email recipient by specifying that recipient's email address as the file name. This mode is considered obsolete.

No security or authentication is provided by the protocol specification. Linux implementations often restrict file transfers to a single configured directory, only allows reading from files with unrestricted readability, and only allows writing to already existing files that have unrestricted write access.

In the early days of TCP/IP, TFTP was often the first protocol implemented in new operating systems because of its simplicity.

Protocol walkthrough

Suppose Host A wants to exchange a file with Host B. The exchange scenario differs according to whether Host A wants to get a file from Host B (called a read request, or RRQ) or Host A wants to write a file to Host B (called a write request, or WRQ):

TFTP protocol walkthrough

When client Host A wants to fetch a file from server Host B (who waits for TFTP requests on its port 69):

  1. Host A opens an ephemeral port (X) and sends a RRQ (encapsulated in a UDP segment) to port 69 of Host B.

  2. Host B opens an ephemeral port (Y) to manage transfers related to this request, and starts sending 512 bytes blocks of data to Host A, each block being acknowledged by Host A.

  3. The last block of data sent by Host B must be of size less than 512 bytes (if the file size is exactly a multiple of 512, the last data block sent will be of size 0). This notifies Host A there is no more data to come.

When client Host A wants to write a file to server Host B (who waits for TFTP requests on its port 69):

  1. Host A opens an ephemeral port (X) and sends a WRQ (encapsulated in a UDP segment) to port 69 of Host B.

  2. Host B opens an ephemeral port (Y) to manage transfers related to this request, and sends an acknowledgment to Host A.

  3. Host A starts sending 512 bytes blocks of data to Host B, each block being acknowledged by Host B.

  4. The last block of data sent by Host A must be of size less than 512 bytes. This notifies Host B there is no more data to come.

If an ACK is not eventually received, a retransmit timer causes the sender to retransmit the lost DATA datagram.

The server's port number changes after the connection request is received(in the example above, Host B switches from port 69 to port Y) so the server doesn't tie up the well-known port for the duration of the file transfer (which could akes seconds or even minutes). Instead, the well-known port 69 remains available to reply to other TFTP requests while file transfers are in progress.

The TFTP datagram format

There are five TFTP message types, for which only the two first header bytes have common format to indicate of which type the TFTP datagram is:

TFTP messages

The first 2 bytes of the TFTP message contain an opcode. For a read request (RRQ) and write request (WRQ) the Filename specifies the server file the client wants to read from or write to. This filename is terminated by a byte of value 0, called a delimiter. The Mode is one of the ASCII strings netascii or octet (in any combination of uppercase or lowercase), again terminated by a delimiter.

Each DATA datagram contains a Block number that is later used in the returned acknowledgment datagram. As an example, when reading a file the client sends a read request (RRQ) specifying the filename and mode. If the file is available, the server responds with a DATA datagram tagged with block number 1; the client responds with an ACK with block number 1. The server sends with the next DATA datagram, tagged block number 2l the client sends an ACK with block number 2. This process continues until the whole file is transferred. Each DATA datagram contains 512 bytes of data, except the final one which contains anything between 0 to 511 bytes of data. When the client receives a DATA datagram with less than 512 bytes of data, it knows it has received the final datagram.

For a write request, the client sends the WRQ specifying the Filename and Mode. If the file can be written by the client, the server responds with an ACK with block number 0. The client then sends the first 512 bytes of file with block number 1. The server responds with an ACK with block number 1. As for RRQ requests, the server knows is has received the last data block when its size is less than 512 bytes.

The last TFTP message type is the error message, with an opcode of 5. This is what the server responds with if a read request or write request can't be processed. Read and write errors during file transmissions also cause this message to be sent, and the faulty transmission is then terminated. The Error number gives a numeric error code, followed by an Error message that might contain additional information on the error.

The TFTPDatagram class

The TFTPDatagram maps raw captured bytes (stored in a Datagram instance) into corresponding TFDP fields; it is therefore derived from the DatagramFragment class.

tftp.h
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
#ifndef TFTP_H
#define TFTP_H

#include <iostream>

#include "datagramfragment.h"   // DatagramFragment

using namespace std;

// Definition de classe representant un datagramme TFTP
class TFTPDatagram : public DatagramFragment {
  public:
    // enumeration des codes d'operation du protocole TFTP
    typedef enum {
      tftp_wrq, tftp_rrq, tftp_data, tftp_ack, tftp_error, tftp_none
    }   TFTPOperation;

    TFTPDatagram(bool = false);                          // default constructor
    TFTPDatagram(bool, unsigned char *, unsigned int);   // parameterized constructor

    unsigned int header_length() const;                  // length of TFTP datagram header in bytes

    TFTPOperation operation() const;                     // operation code

    char * filename() const;                             // filename field in RRQ and WRQ datagrams
    char * mode() const;                                 // mode field in RRQ and WRQ datagrams
  
    unsigned int error_code() const;                     // error code field in ERROR datagrams
    char * error_msg() const;                            // error message field in ERROR datagrams

    unsigned int block() const;                          // block number in DATA datagrams
    unsigned int data_length() const;                    // bytes of data in DATA datagrams

    // Operator overloads
    friend ostream & operator<<(ostream &, const TFTPDatagram &);

  protected:
};

#endif

And here are the member definitions for TFTPDatagram. The implementation is straightforward:

tftp.cpp
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#ifndef TFTP_CPP
#define TFTP_CPP

#include "tftp.h"
#include "exceptions.h"

// Default constructor
TFTPDatagram::TFTPDatagram(bool owned) : DatagramFragment(owned) {
}

// Parameterized constructor
TFTPDatagram::TFTPDatagram(bool owned, unsigned char * s, unsigned int l) : DatagramFragment(owned, s, l) {
}

// Returns the UDP segment header length in bytes
unsigned int TFTPDatagram::header_length() const {
  return 2;
}

// Returns TFTP datagram type according to the opcode field
TFTPDatagram::TFTPOperation TFTPDatagram::operation() const {
  switch (char2word(p_data)) {
    case 1  : return tftp_rrq;
    case 2  : return tftp_wrq;
    case 3  : return tftp_data;
    case 4  : return tftp_ack;
    default : return tftp_none;
  }
}

// Returns the ASCII mode transported by RRQ and WRQ datagrams
char * TFTPDatagram::filename() const {
  if (operation() == tftp_rrq || operation() == tftp_wrq)
    return ((char *)p_data + header_length());
  else
    throw EBadTransportException("TFTP datagram does not contain filename field");
}

// Returns the filename transported by RRQ and WRQ datagrams
char * TFTPDatagram::mode() const {
  if (operation() == tftp_rrq || operation() == tftp_wrq)
    return ((char *)p_data + header_length() + strlen(filename()) + 1);
  else
    throw EBadTransportException("TFTP datagram does not contain filename field");
}

// Returns the error code contained in ERROR datagrams
unsigned int TFTPDatagram::error_code() const {
  if (operation() == tftp_error)
    return (char2word(p_data + header_length()));
  else
    throw EBadTransportException("TFTP datagram does not contain error code field");
}

// Returns the error message contained in ERROR datagrams
char * TFTPDatagram::error_msg() const {
  if (operation() == tftp_error)
    return ((char *)p_data + header_length() + 2);
  else
    throw EBadTransportException("TFTP datagram does not contain error message field");
}

// Returns the block number transported in DATA and ACK datagrams
unsigned int TFTPDatagram::block() const {
  if (operation() == tftp_data || operation() == tftp_ack)
    return (char2word(p_data + header_length()));
  else
    throw EBadTransportException("TFTP datagram does not contain block field");
}

// Returns the size (in bytes) of data transported by DATA datagrams
unsigned int TFTPDatagram::data_length() const {
  if (operation() == tftp_data)
    return (p_len - 4);
  else
    throw EBadTransportException("TFTP datagram does not contain data");
}

// Returns a string textually identifying some common standard ports
ostream & operator<<(ostream & ostr, const TFTPDatagram & tftp) {
  if (tftp.p_data) {
    ostr << "operation = ";
    switch (tftp.operation()) {
      case TFTPDatagram::tftp_rrq  : ostr << "READ" << endl;
                                     break;
      case TFTPDatagram::tftp_wrq  : ostr << "WRITE" << endl;
                                     break;
      case TFTPDatagram::tftp_data : ostr << "DATA" << endl;
                                     break;
      case TFTPDatagram::tftp_ack  : ostr << "ACK" << endl;
                                     break;
      case TFTPDatagram::tftp_error: ostr << "ERROR" << endl;
                                     break;
      default                      : ostr << "unknown" << endl;
                                     break;
    }
  }

  if (tftp.operation() == TFTPDatagram::tftp_rrq || tftp.operation() == TFTPDatagram::tftp_wrq) {
    ostr << "filename = " << tftp.filename() << endl;
    ostr << "mode = "     << tftp.mode() << endl;
  }

  if (tftp.operation() == TFTPDatagram::tftp_data || tftp.operation() == TFTPDatagram::tftp_ack) 
    ostr << "block number = " << tftp.block() << endl;

  if (tftp.operation() == TFTPDatagram::tftp_data)
    ostr << "data size = " << tftp.data_length() << endl;

  if (tftp.operation() == TFTPDatagram::tftp_error) {
    ostr << "error code = " << tftp.error_code() << endl;
    ostr << "error message = " << tftp.error_msg() << endl;
  }

  ostr << flush;

  return ostr;
}

#endif

Since TFTP is transported by UDP, we add a new method to the UDPSegment class for mapping its payload into a TFTPDatagram instance:

udpsegment.cpp (partial)
056
057
058
059
// Returns TFTP datagram transported in payload
TFTPDatagram UDPSegment::tftp() {
  return TFTPDatagram(false, data(), length() - header_length());
}

Finally, we update the callback function to map the captured bytes of the UDPSegment's payload into a TFTPDatagram instance for display:

sniff16.cpp (partial)
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
305
036
037
038
039
040



086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#include <iostream>
#include <sstream>             // ostringstream

#include <cstring>             // memset
#include <cstdlib>             // exit
#include <unistd.h>            // getopt()
#include <signal.h>            // Ctrl+C handling
#include <arpa/inet.h>         // struct in_addr
#include <string>              // string

#include <set>                 // STL set
#include <map>                 // STL dictionary

#include <pcap.h>              // libpcap

#include "datagram.h"          // Datagram
#include "ethernetframe.h"     // EthernetFrame
#include "ippacket.h"          // IPPacket
#include "arppacket.h"         // ARPPacket
#include "icmppacket.h"        // ICMPPacket
#include "tcpsegment.h"        // TCPSegment
#include "udpsegment.h"        // UDPSegment

#include "pingflood.h"         // PingFloodDetection
#include "tcpsession.h"        // TCPSession

#include "tftp.h"              // TFTPDatagram

using namespace std;

pcap_t        *pcap_session = NULL;   // libpcap session handle

char          *strfilter = NULL;      // textual BPF filter
bpf_program    binfilter;             // compiled BPF filter program

pcap_dumper_t *logfile = NULL;        // file descriptor for datagram logging

unsigned int capture_count = 0;       // count of captured datagrams

char * tftpserver = NULL;             // supervised TFTP server

...

bool show_raw   = false;          // deactivate raw display of data captured
bool quiet_mode = false;          // controls whether the callback display captured datagrams or not
int  security_tool = 0;           // security tool to apply

#define ARPSPOOF   1
#define PINGFLOOD  2
#define TCPTRACK   3
#define TFTPTRACK  4

// Macro replacing cout to apply conditional display in callback
#define COUT if (!quiet_mode) cout

// Callback given to pcap_loop() for processing captured datagrams
void process_packet(u_char *user, const struct pcap_pkthdr * h, const u_char * packet) {
  static set<IPAddress>             arpRequests;
  static PingFloodDetection         pingFloods;  
  static map<string, TCPSession>    tcpSessions;
  static map<string, unsigned int>  tftpClients;

  IPPacket     ip;
  ARPPacket    arp;
  ICMPPacket   icmp;
  TCPSegment   tcp;
  UDPSegment   udp;
  TFTPDatagram tftp;
    
  COUT << "Grabbed " << h->caplen << " bytes (" << static_cast<int>(100.0 * h->caplen / h->len) 
       << "%) of datagram received on " << ctime((const time_t*)&h->ts.tv_sec);
       
  Datagram pkt(packet, h->caplen);        // initialized Datagram instance
  if (show_raw) COUT << "---------------- Raw data -----------------" << pkt << endl;

  EthernetFrame ether = pkt.ethernet();   // get EthernetFrame instance from transported data
  COUT << "---------- Ethernet frame header ----------" << endl << ether;
  
  // Display payload content according to EtherType
  switch (ether.ether_type()) {
    case EthernetFrame::et_IPv4 :         // get IPPacket instance from transported data
      ip = ether.ip4();
      COUT << "-------- IP packet header --------" << endl << ip;

      // If it transports an ICMP packet, display its attributes
      if (ip.protocol() == IPPacket::ipp_icmp) {
        icmp = ip.icmp();
        COUT << "------ ICMP packet header ------" << endl << icmp;
        
        // Apply ping flood detection if required
        if (security_tool == PINGFLOOD && pingFloods.process_ping(ip.destination_ip(), icmp))
          cout << "**** ALERT - Potential Ping flood detected ****" << endl
               << "     numerous echo requests with large payload targeting" << endl
               << "     host " << ip.destination_ip() << endl << endl;
      }

      // If it transports a TCP segment, display its attributes
      else if (ip.protocol() == IPPacket::ipp_tcp) {
        tcp = ip.tcp();
        COUT << "------ TCP segment header ------" << endl << tcp;

        // Apply TCP session tracking if required
        if (security_tool == TCPTRACK) {
          // Compute keys to uniquely identify the session
          string src, dst;
          TCPSession::getKeys(ip, src, dst);
          
          // Is the segment part of a tracked session or not? We need to search 
          // for two keys since segments within a TCP session travel in both
          // directions
          map<string, TCPSession>::iterator it = tcpSessions.find(src + dst);
          if (it == tcpSessions.end()) it = tcpSessions.find(dst + src);
          
          // If it's a new session then start tracking it
          if (it == tcpSessions.end() && (tcp.flag_syn() && !tcp.flag_ack())) {
            tcpSessions[src + dst] = TCPSession(ip);
            it = tcpSessions.find(src + dst);
          }
          
          if (it != tcpSessions.end())
            // Now we track the session to which is associated the TCP segment
            if (it->second.getState() != it->second.trackState(ip, false))
              // If the session has just been closed, display total number of bytes 
              // exchanged since we started tracking it, and destroy it
              if (it->second.terminated()) {
                cout << "Total data exchanged between " << src << " and " << dst << " = " 
                     << it->second.getBytes() << " bytes" << endl;
                     
                // Destroy TCP session tracker
                tcpSessions.erase(it);
              }
        }
      }          

      // If it transports a UDP segment, display its attributes
      else if (ip.protocol() == IPPacket::ipp_udp) {
        udp = ip.udp();
        COUT << "------ UDP segment header ------" << endl << udp;
        
        // Apply TFTP session tracking if required
        if (security_tool == TFTPTRACK && tftpserver) {
          ostringstream ostr;   // to build map key for tracking client
          
          // Is it a new TFTP connection request to the supervised server?
          ostr << ip.source_ip();
          if (ostr.str() == string(tftpserver) && udp.destination_port() == 69) {
            // Build a key for tracking the TFTP session
            ostr << '/' << udp.source_port();
            
            // Initialize data counter for new client
            tftpClients[ostr.str()] = 0;
          }
          
          // Is the source or destination a TFTP client being tracked? We must check in
          // both directions since the client may be reading from or writing to the server
          ostr.str("");    // clear stream content
          ostr << ip.source_ip() << '/' << udp.source_port();            
          map<string, unsigned int>::iterator it = tftpClients.find(ostr.str());
          if (it == tftpClients.end()) {
            ostr.str("");    // clear stream content
            ostr << ip.destination_ip() << '/' << udp.destination_port();
            it = tftpClients.find(ostr.str());
          }
          
          // If its a TFTP client being tracked, show TFTP datagram and update 
          // tracking info
          if (it != tftpClients.end()) {
            tftp = udp.tftp();

            COUT << "-- tftp packet --" << endl;
            COUT << tftp << flush;
            
            // If it's a DATA datagram, update the client counter
            if (tftp.operation() == TFTPDatagram::tftp_data) {
              it->second += tftp.data_length();
              
              // If it's the last DATA datagram, display results and stop tracking
              // the connection
              if (tftp.data_length() < 512) {
                cout << "Total TFTP data exchanged over " << it->first << " = " 
                     << it->second << " bytes" << endl;
                     
                tftpClients.erase(it);
              }
            }
          }
        }
      }
      
      break;

      ...

Key points in sniff16.cpp are:

  • A global variable, tftpserver, is added to store the TFTP server's IP address given through command line arguments.

  • Line #103 declares STL map to store the IP+Port of clients connecting to the TFTP server, along with a counter for data bytes exchanged.

  • The code handling TFTP tracking starts at line #183. The first step is to determine if the TFTP datagram is RRQ or WRQ connecting to port 69 of the TFTP server. If so, the source's IP+Port are added to the map, along with its data counter initialized to 0.

  • Code at line #198 checks whether the TFTP datagram is part of a tracked exchange. If so, lines #209 through #225 build the TFTPDatagram instance, update the tracked exchange's data counter, and stops tracking the exchange if its the last DATA datagram.

Here is the resulting display of reading twice the file essai.txt from a TFTP server installed locally (i.e. both the TFTP server and the TFTP client are on the same host). The size of the downloaded file is 10885 bytes:

%root> ./sniff16 -s tftptrack -S 172.16.179.137 -q
device = eth0
network ip = 172.16.179.0
network mask = 255.255.255.0
Total TFTP data exchanged over 172.16.179.137/36174 = 10885 bytes
^C
*** Capture process interrupted by user...
*** 45 datagrams captured
%root> ./sniff16 -s tftptrack -S 172.16.179.137 -n 3
device = eth0
network ip = 172.16.179.0
network mask = 255.255.255.0
Grabbed 63 bytes (100%) of datagram received on Fri Oct  4 11:56:48 2013
---------- Ethernet frame header ----------
destination MAC address = 00.00.00.00.00.00
source MAC address = 00.00.00.00.00.00
ether type = IPv4 [0x0800]
-------- IP packet header --------
version = IPv4
header length = 20 (IHL = 5)
type of service = 0:
total length = 49
fragment ID = 0x0000
  don't fragment = 2
  more fragments = 0
  fragment position = 0
protocol = UDP [0x11]
time to live = 64
checksum = 0x3cba
destination IP address = 172.16.179.137
source IP address = 172.16.179.137
------ UDP segment header ------
source port = 47906 [ephemeral]
destination port = 69 [TFTP]
length = 29
checksum = 0xfe30
-- tftp packet --
operation = READ
filename = essai.txt
mode = netascii

Grabbed 558 bytes (100%) of datagram received on Fri Oct  4 11:56:48 2013
---------- Ethernet frame header ----------
destination MAC address = 00.00.00.00.00.00
source MAC address = 00.00.00.00.00.00
ether type = IPv4 [0x0800]
-------- IP packet header --------
version = IPv4
header length = 20 (IHL = 5)
type of service = 0:
total length = 544
fragment ID = 0x4bfe
  don't fragment = 2
  more fragments = 0
  fragment position = 0
protocol = UDP [0x11]
time to live = 64
checksum = 0xeecc
destination IP address = 172.16.179.137
source IP address = 172.16.179.137
------ UDP segment header ------
source port = 39602 [ephemeral]
destination port = 47906 [ephemeral]
length = 524
checksum = 0x0020
-- tftp packet --
operation = DATA
block number = 1
data size = 512

Grabbed 46 bytes (100%) of datagram received on Fri Oct  4 11:56:48 2013
---------- Ethernet frame header ----------
destination MAC address = 00.00.00.00.00.00
source MAC address = 00.00.00.00.00.00
ether type = IPv4 [0x0800]
-------- IP packet header --------
version = IPv4
header length = 20 (IHL = 5)
type of service = 0:
total length = 32
fragment ID = 0x0000
  don't fragment = 2
  more fragments = 0
  fragment position = 0
protocol = UDP [0x11]
time to live = 64
checksum = 0x3ccb
destination IP address = 172.16.179.137
source IP address = 172.16.179.137
------ UDP segment header ------
source port = 47906 [ephemeral]
destination port = 39602 [ephemeral]
length = 12
checksum = 0xfe1f
-- tftp packet --
operation = ACK
block number = 1

*** 3 datagrams captured
%root> 

The first download is done in quiet mode with TFTP tracking enabled. It clearly shows 10885 bytes of data were transferred through the TFTP exchange.

The second download is done while openly sniffing the first three datagrams of the TFTP exchange. It shows:

  1. a RRQ datagram from client to server,

  2. a DATA datagram back to the client, and

  3. the ACK datagram acknowledging it back to the server.

Note that the DATA datagram contains 512 bytes of data, which means its not the last one; it actually required 22 TFTP datagrams to complete this file transfer.


Home  |  Previous  |  Next

 
Copyright © 2014 Marco Lavoie