#define ERR_ILL    4
 #define ERR_TID    5
 
-void tftp_request(struct listener *listen, time_t now)
+static void tftp_request(struct listener *listen, time_t now)
 {
   ssize_t len;
   char *packet = daemon->packet;
-  char *filename, *mode, *p, *end, *opt;
+  char *filename, *mode, *p, *end;
   union mysockaddr addr, peer;
   struct msghdr msg;
   struct iovec iov;
   /* May reuse struct transfer from abandoned transfer in single port mode. */
   if (!transfer && !(transfer = whine_malloc(sizeof(struct tftp_transfer))))
     return;
-  
+
+  memset(transfer, 0, sizeof(struct tftp_transfer));
+        
   if (option_bool(OPT_SINGLE_PORT))
     transfer->sockfd = listen->tftpfd;
   else if ((transfer->sockfd = socket(family, SOCK_DGRAM, 0)) == -1)
   transfer->peer = peer;
   transfer->source = addra;
   transfer->if_index = if_index;
-  transfer->timeout = now + 2;
+  transfer->timeout = 2;
+  transfer->start = now;
   transfer->backoff = 1;
   transfer->block = 1;
   transfer->blocksize = 512;
-  transfer->offset = 0;
-  transfer->file = NULL;
-  transfer->opt_blocksize = transfer->opt_transize = 0;
-  transfer->netascii = transfer->carrylf = 0;
- 
+  transfer->windowsize = 1;
+  
   (void)prettyprint_addr(&peer, daemon->addrbuff);
   
   /* if we have a nailed-down range, iterate until we find a free one. */
   
   p = packet + 2;
   end = packet + len;
+
+  len = 0;
   
-  if (!(filename = next(&p, end)) ||
-      !(mode = next(&p, end)) ||
-      (strcasecmp(mode, "octet") != 0 && strcasecmp(mode, "netascii") != 0) ||
-      ntohs(*((unsigned short *)packet)) != OP_RRQ)
+  if (ntohs(*((unsigned short *)packet)) == OP_WRQ)
+    len = tftp_err(ERR_ILL, packet, _("unsupported write request from %s"),daemon->addrbuff, NULL);
+  else if (ntohs(*((unsigned short *)packet)) == OP_RRQ)
     {
-      if (!filename)
+      if (!(filename = next(&p, end)))
        len = tftp_err(ERR_ILL, packet, _("empty filename in request from %s"), daemon->addrbuff, NULL);
+      else if (!(mode = next(&p, end)) || (strcasecmp(mode, "octet") != 0 && strcasecmp(mode, "netascii") != 0))
+       len = tftp_err(ERR_ILL, packet, _("unsupported request from %s"),daemon->addrbuff, NULL);
       else
-       len = tftp_err(ERR_ILL, packet, _("unsupported %srequest from %s"),
-                      (ntohs(*((unsigned short *)packet)) == OP_WRQ) ? _("write ") : "", daemon->addrbuff);
-      is_err = 1;
-    }
-  else
-    {
-      if (strcasecmp(mode, "netascii") == 0)
-       transfer->netascii = 1;
-      
-      while ((opt = next(&p, end)))
        {
-         if (strcasecmp(opt, "blksize") == 0)
+         char *opt, *arg;
+         
+         if (strcasecmp(mode, "netascii") == 0)
+           transfer->netascii = 1;
+         
+         while ((opt = next(&p, end)) && (arg = next(&p, end)))
            {
-             if ((opt = next(&p, end)) && !option_bool(OPT_TFTP_NOBLOCK))
+             unsigned int val = atoi(arg);
+             
+             if (strcasecmp(opt, "blksize") == 0 && !option_bool(OPT_TFTP_NOBLOCK))
                {
                  /* 32 bytes for IP, UDP and TFTP headers, 52 bytes for IPv6 */
                  int overhead = (family == AF_INET) ? 32 : 52;
-                 transfer->blocksize = atoi(opt);
-                 if (transfer->blocksize < 1)
-                   transfer->blocksize = 1;
-                 if (transfer->blocksize > (unsigned)daemon->packet_buff_sz - 4)
-                   transfer->blocksize = (unsigned)daemon->packet_buff_sz - 4;
-                 if (mtu != 0 && transfer->blocksize > (unsigned)mtu - overhead)
-                   transfer->blocksize = (unsigned)mtu - overhead;
+                 if (val < 1)
+                   val  = 1;
+                 if (val > (unsigned)daemon->packet_buff_sz - 4)
+                   val  = (unsigned)daemon->packet_buff_sz - 4;
+                 if (mtu != 0 && val > (unsigned)mtu - overhead)
+                   val  = (unsigned)mtu - overhead;
+                 transfer->blocksize = val;
                  transfer->opt_blocksize = 1;
                  transfer->block = 0;
                }
+             else if (strcasecmp(opt, "tsize") == 0 && !transfer->netascii)
+               {
+                 transfer->opt_transize = 1;
+                 transfer->block = 0;
+               }
+             else if (strcasecmp(opt, "timeout") == 0)
+               {
+                 if (val > 255)
+                   val = 255;
+                 transfer->timeout = val;
+                 transfer->opt_timeout = 1;
+                 transfer->block = 0;
+               }
+             else if (strcasecmp(opt, "windowsize") == 0 && !transfer->netascii)
+               {
+                 /* windowsize option only supported for binary transfers. */
+                 if (val < 1)
+                   val = 1;
+                 if (val > TFTP_MAX_WINDOW)
+                   val = TFTP_MAX_WINDOW;
+                 transfer->windowsize = val;
+                 transfer->opt_windowsize = 1;
+                 transfer->block = 0;
+               }
            }
-         else if (strcasecmp(opt, "tsize") == 0 && next(&p, end) && !transfer->netascii)
-           {
-             transfer->opt_transize = 1;
-             transfer->block = 0;
-           }
-       }
-
-      /* cope with backslashes from windows boxen. */
-      for (p = filename; *p; p++)
-       if (*p == '\\')
-         *p = '/';
-       else if (option_bool(OPT_TFTP_LC))
-         *p = tolower((unsigned char)*p);
-               
-      strcpy(daemon->namebuff, "/");
-      if (prefix)
-       {
-         if (prefix[0] == '/')
-           daemon->namebuff[0] = 0;
-         strncat(daemon->namebuff, prefix, (MAXDNAME-1) - strlen(daemon->namebuff));
-         if (prefix[strlen(prefix)-1] != '/')
-           strncat(daemon->namebuff, "/", (MAXDNAME-1) - strlen(daemon->namebuff));
-
-         if (option_bool(OPT_TFTP_APREF_IP))
-           {
-             size_t oldlen = strlen(daemon->namebuff);
-             struct stat statbuf;
-             
-             strncat(daemon->namebuff, daemon->addrbuff, (MAXDNAME-1) - strlen(daemon->namebuff));
-             strncat(daemon->namebuff, "/", (MAXDNAME-1) - strlen(daemon->namebuff));
-             
-             /* remove unique-directory if it doesn't exist */
-             if (stat(daemon->namebuff, &statbuf) == -1 || !S_ISDIR(statbuf.st_mode))
-               daemon->namebuff[oldlen] = 0;
-           }
          
-         if (option_bool(OPT_TFTP_APREF_MAC))
+         /* cope with backslashes from windows boxen. */
+         for (p = filename; *p; p++)
+           if (*p == '\\')
+             *p = '/';
+           else if (option_bool(OPT_TFTP_LC))
+             *p = tolower((unsigned char)*p);
+         
+         strcpy(daemon->namebuff, "/");
+         if (prefix)
            {
-             unsigned char *macaddr = NULL;
-             unsigned char macbuf[DHCP_CHADDR_MAX];
-             
-#ifdef HAVE_DHCP
-             if (daemon->dhcp && peer.sa.sa_family == AF_INET)
-               {
-                 /* Check if the client IP is in our lease database */
-                 struct dhcp_lease *lease = lease_find_by_addr(peer.in.sin_addr);
-                 if (lease && lease->hwaddr_type == ARPHRD_ETHER && lease->hwaddr_len == ETHER_ADDR_LEN)
-                   macaddr = lease->hwaddr;
-               }
-#endif
-             
-             /* If no luck, try to find in ARP table. This only works if client is in same (V)LAN */
-             if (!macaddr && find_mac(&peer, macbuf, 1, now) > 0)
-               macaddr = macbuf;
+             if (prefix[0] == '/')
+               daemon->namebuff[0] = 0;
+             strncat(daemon->namebuff, prefix, (MAXDNAME-1) - strlen(daemon->namebuff));
+             if (prefix[strlen(prefix)-1] != '/')
+               strncat(daemon->namebuff, "/", (MAXDNAME-1) - strlen(daemon->namebuff));
              
-             if (macaddr)
-               {
+             if (option_bool(OPT_TFTP_APREF_IP))
+               {
                  size_t oldlen = strlen(daemon->namebuff);
                  struct stat statbuf;
-
-                 snprintf(daemon->namebuff + oldlen, (MAXDNAME-1) - oldlen, "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x/",
-                          macaddr[0], macaddr[1], macaddr[2], macaddr[3], macaddr[4], macaddr[5]);
+                 
+                 strncat(daemon->namebuff, daemon->addrbuff, (MAXDNAME-1) - strlen(daemon->namebuff));
+                 strncat(daemon->namebuff, "/", (MAXDNAME-1) - strlen(daemon->namebuff));
                  
                  /* remove unique-directory if it doesn't exist */
                  if (stat(daemon->namebuff, &statbuf) == -1 || !S_ISDIR(statbuf.st_mode))
                    daemon->namebuff[oldlen] = 0;
                }
+             
+             if (option_bool(OPT_TFTP_APREF_MAC))
+               {
+                 unsigned char *macaddr = NULL;
+                 unsigned char macbuf[DHCP_CHADDR_MAX];
+                 
+#ifdef HAVE_DHCP
+                 if (daemon->dhcp && peer.sa.sa_family == AF_INET)
+                   {
+                     /* Check if the client IP is in our lease database */
+                     struct dhcp_lease *lease = lease_find_by_addr(peer.in.sin_addr);
+                     if (lease && lease->hwaddr_type == ARPHRD_ETHER && lease->hwaddr_len == ETHER_ADDR_LEN)
+                       macaddr = lease->hwaddr;
+                   }
+#endif
+                 
+                 /* If no luck, try to find in ARP table. This only works if client is in same (V)LAN */
+                 if (!macaddr && find_mac(&peer, macbuf, 1, now) > 0)
+                   macaddr = macbuf;
+                 
+                 if (macaddr)
+                   {
+                     size_t oldlen = strlen(daemon->namebuff);
+                     struct stat statbuf;
+                     
+                     snprintf(daemon->namebuff + oldlen, (MAXDNAME-1) - oldlen, "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x/",
+                              macaddr[0], macaddr[1], macaddr[2], macaddr[3], macaddr[4], macaddr[5]);
+                     
+                     /* remove unique-directory if it doesn't exist */
+                     if (stat(daemon->namebuff, &statbuf) == -1 || !S_ISDIR(statbuf.st_mode))
+                       daemon->namebuff[oldlen] = 0;
+                   }
+               }
+             
+             /* Absolute pathnames OK if they match prefix */
+             if (filename[0] == '/')
+               {
+                 if (strstr(filename, daemon->namebuff) == filename)
+                   daemon->namebuff[0] = 0;
+                 else
+                   filename++;
+               }
            }
+         else if (filename[0] == '/')
+           daemon->namebuff[0] = 0;
+         strncat(daemon->namebuff, filename, (MAXDNAME-1) - strlen(daemon->namebuff));
          
-         /* Absolute pathnames OK if they match prefix */
-         if (filename[0] == '/')
+         /* check permissions and open file */
+         if ((transfer->file = check_tftp_fileperm(&len, prefix, daemon->addrbuff)))
            {
-             if (strstr(filename, daemon->namebuff) == filename)
-               daemon->namebuff[0] = 0;
+             transfer->lastack = transfer->block;
+             transfer->retransmit = now + transfer->timeout;
+             /* This packet is may be the first data packet, but only if windowsize == 1
+                To get windowsize greater then one requires an option negotiation,
+                in which case this packet is the OACK. */
+             if ((len = get_block(packet, transfer)) == -1)
+               len = tftp_err_oops(packet, daemon->namebuff);
              else
-               filename++;
+               is_err = 0;
            }
        }
-      else if (filename[0] == '/')
-       daemon->namebuff[0] = 0;
-      strncat(daemon->namebuff, filename, (MAXDNAME-1) - strlen(daemon->namebuff));
-      
-      /* check permissions and open file */
-      if ((transfer->file = check_tftp_fileperm(&len, prefix, daemon->addrbuff)))
-       {
-         if ((len = get_block(packet, transfer)) == -1)
-           len = tftp_err_oops(packet, daemon->namebuff);
-         else
-           is_err = 0;
-       }
     }
-
-  send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), packet, len, &peer, &addra, if_index);
-
+  
+  if (len)
+    {
+      send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), packet, len, &peer, &addra, if_index);
+      
 #ifdef HAVE_DUMPFILE
-  dump_packet_udp(DUMP_TFTP, (void *)packet, len, NULL, (union mysockaddr *)&peer, transfer->sockfd);
+      dump_packet_udp(DUMP_TFTP, (void *)packet, len, NULL, (union mysockaddr *)&peer, transfer->sockfd);
 #endif
+    }
   
   if (is_err)
     free_transfer(transfer);
 
          if ((len = recvfrom(transfer->sockfd, daemon->packet, daemon->packet_buff_sz, 0, &peer.sa, &addr_len)) > 0)
            {
+#ifdef HAVE_DUMPFILE
+             dump_packet_udp(DUMP_TFTP, (void *)daemon->packet, len, (union mysockaddr *)&peer, NULL, transfer->sockfd);
+#endif       
+
              if (sockaddr_isequal(&peer, &transfer->peer)) 
                handle_tftp(now, transfer, len);
              else
          
   for (transfer = daemon->tftp_trans, up = &daemon->tftp_trans; transfer; transfer = tmp)
     {
+      int endcon = 0, error = 0, timeout = 0;
+      
       tmp = transfer->next;
       
-      if (difftime(now, transfer->timeout) >= 0.0)
+      /* ->start set to zero in handle_tftp() when we recv an error packet. */
+      if (transfer->start == 0)
+       endcon = error = 1;
+      else if (difftime(now, transfer->start) > TFTP_TRANSFER_TIME)  
+       {
+         endcon = 1;
+         /* don't complain about timeout when we're awaiting the last
+            ACK, some clients never send it */
+         if (get_block(daemon->packet, transfer) > 0)
+           error = timeout = 1;
+       }
+      else if (difftime(now, transfer->retransmit) >= 0.0)
        {
-         int endcon = 0;
+         /* Do transmission or re-transmission. When we get an ACK, the call to handle_tftp()
+            bumps transfer->lastack and trips the retransmit timer so that we send the next block(s)
+            here. */
+         unsigned int i, winsize;
          ssize_t len;
+         
+         transfer->retransmit += transfer->timeout + (1<<(transfer->backoff/2));
+         transfer->backoff++;
+         transfer->block = transfer->lastack;
 
-         /* timeout, retransmit */
-         transfer->timeout += 1 + (1<<(transfer->backoff/2));
-                 
-         /* we overwrote the buffer... */
-         daemon->srv_save = NULL;
-
-         if ((len = get_block(daemon->packet, transfer)) == -1)
-           {
-             len = tftp_err_oops(daemon->packet, transfer->file->filename);
-             endcon = 1;
-           }
-         else if (++transfer->backoff > 7)
+         if ((len = get_block(daemon->packet, transfer)) == 0)
+           endcon = 1; /* got last ACK */
+         else
            {
-             /* don't complain about timeout when we're awaiting the last
-                ACK, some clients never send it */
-             if ((unsigned)len == transfer->blocksize + 4)
-               endcon = 1;
-             len = 0;
-           }
+             /* send a window'a worth of blocks unless we're retransmitting OACK */
+             winsize = transfer->block ? transfer->windowsize : 1;
+             
+             /* we overwrote the buffer... */
+             daemon->srv_save = NULL;
+             
+             for (i = 0; i < winsize && !endcon; i++, transfer->block++)
+               {
+                 if (i != 0)
+                   len = get_block(daemon->packet, transfer);
 
-         if (len != 0)
-           {
-             send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), daemon->packet, len,
-                       &transfer->peer, &transfer->source, transfer->if_index);
+                 if (len == 0)
+                   break;
+                 
+                 if (len == -1)
+                   {
+                     len = tftp_err_oops(daemon->packet, transfer->file->filename);
+                     endcon = error = 1;
+                   }
+                 
+                 send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), daemon->packet, len,
+                           &transfer->peer, &transfer->source, transfer->if_index);
 #ifdef HAVE_DUMPFILE
-             dump_packet_udp(DUMP_TFTP, (void *)daemon->packet, len, NULL, (union mysockaddr *)&transfer->peer, transfer->sockfd);
+                 dump_packet_udp(DUMP_TFTP, (void *)daemon->packet, len, NULL, (union mysockaddr *)&transfer->peer, transfer->sockfd);
 #endif
+               }
            }
+       }
+             
+      if (endcon)
+       {
+         strcpy(daemon->namebuff, transfer->file->filename);
+         sanitise(daemon->namebuff);
+         (void)prettyprint_addr(&transfer->peer, daemon->addrbuff);
+         if (timeout)
+           my_syslog(MS_TFTP | LOG_ERR, _("timeout sending %s to %s"), daemon->namebuff, daemon->addrbuff);
+         else if (error)
+           my_syslog(MS_TFTP | LOG_ERR, _("failed sending %s to %s"), daemon->namebuff, daemon->addrbuff);
+         else
+           my_syslog(MS_TFTP | LOG_INFO, _("sent %s to %s"), daemon->namebuff, daemon->addrbuff);
          
-         if (endcon || len == 0)
+         /* unlink */
+         *up = tmp;
+         if (error)
+           free_transfer(transfer);
+         else
            {
-             strcpy(daemon->namebuff, transfer->file->filename);
-             sanitise(daemon->namebuff);
-             (void)prettyprint_addr(&transfer->peer, daemon->addrbuff);
-             my_syslog(MS_TFTP | LOG_INFO, endcon ? _("failed sending %s to %s") : _("sent %s to %s"), daemon->namebuff, daemon->addrbuff);
-             /* unlink */
-             *up = tmp;
-             if (endcon)
-               free_transfer(transfer);
-             else
-               {
-                 /* put on queue to be sent to script and deleted */
-                 transfer->next = daemon->tftp_done_trans;
-                 daemon->tftp_done_trans = transfer;
-               }
-             continue;
+             /* put on queue to be sent to script and deleted */
+             transfer->next = daemon->tftp_done_trans;
+             daemon->tftp_done_trans = transfer;
            }
        }
-
-      up = &transfer->next;
-    }    
+      else
+       up = &transfer->next;
+    }
 }
-         
+
 /* packet in daemon->packet as this is called. */
 static void handle_tftp(time_t now, struct tftp_transfer *transfer, ssize_t len)
 {
   
   if (len >= (ssize_t)sizeof(struct ack))
     {
-      if (ntohs(mess->op) == OP_ACK && ntohs(mess->block) == (unsigned short)transfer->block) 
+      if (ntohs(mess->op) == OP_ACK)
        {
-         /* Got ack, ensure we take the (re)transmit path */
-         transfer->timeout = now;
-         transfer->backoff = 0;
-         if (transfer->block++ != 0)
-           transfer->offset += transfer->blocksize - transfer->expansion;
+         /* try and handle 16-bit blockno wrap-around */
+         unsigned int block = (unsigned short)ntohs(mess->block);
+         if (block < transfer->lastack)
+           block |= transfer->block & 0xffff0000;
+         
+         /* ignore duplicate ACKs and ACKs for blocks we've not yet sent. */
+         if (block >= transfer->lastack &&
+             block <= transfer->block) 
+           {
+             /* Got ack, move forward and ensure we take the (re)transmit path */
+             transfer->retransmit = transfer->start = now;
+             transfer->backoff = 0;
+             transfer->lastack = block + 1;
+             
+             /* We have no easy function from block no. to file offset when
+                expanding line breaks in netascii mode, so we update the offset here
+                as each block is acknowledged. This explains why the window size must be
+                one for a netascii transfer; to avoid  the block no. doing anything
+                other than incrementing by one. */
+             if (transfer->netascii && block != 0)
+               transfer->offset += transfer->blocksize - transfer->expansion;
+           }
        }
       else if (ntohs(mess->op) == OP_ERR)
        {
                    daemon->addrbuff);  
          
          /* Got err, ensure we take abort */
-         transfer->timeout = now;
-         transfer->backoff = 100;
+         transfer->start = 0;
        }
     }
 }
          p += (sprintf(p,"tsize") + 1);
          p += (sprintf(p, "%u", (unsigned int)transfer->file->size) + 1);
        }
-
+      if (transfer->opt_timeout)
+       {
+         p += (sprintf(p,"timeout") + 1);
+         p += (sprintf(p, "%u", transfer->timeout) + 1);
+       }
+      if (transfer->opt_windowsize)
+       {
+         p += (sprintf(p,"windowsize") + 1);
+         p += (sprintf(p, "%u", (unsigned int)transfer->windowsize) + 1);
+       }
+ 
       return p - packet;
     }
   else
        unsigned char data[];
       } *mess = (struct datamess *)packet;
       
-      size_t size = transfer->file->size - transfer->offset; 
+      size_t size;
+      
+      if (!transfer->netascii)
+       transfer->offset = (transfer->block - 1) * transfer->blocksize;
       
       if (transfer->offset > transfer->file->size)
        return 0; /* finished */
       
-      if (size > transfer->blocksize)
+      if ((size = transfer->file->size - transfer->offset) > transfer->blocksize)
        size = transfer->blocksize;
       
       mess->op = htons(OP_DATA);
       mess->block = htons((unsigned short)(transfer->block));
-      
-      if (lseek(transfer->file->fd, transfer->offset, SEEK_SET) == (off_t)-1 ||
-         !read_write(transfer->file->fd, mess->data, size, RW_READ))
+
+      if (size != 0 &&
+         (lseek(transfer->file->fd, transfer->offset, SEEK_SET) == (off_t)-1 ||
+          !read_write(transfer->file->fd, mess->data, size, RW_READ)))
        return -1;
       
-      transfer->expansion = 0;
-      
       /* Map '\n' to CR-LF in netascii mode */
       if (transfer->netascii)
        {
          size_t i;
          int newcarrylf;
-
+         
+         transfer->expansion = 0;
+         
          for (i = 0, newcarrylf = 0; i < size; i++)
-           if (mess->data[i] == '\n' && ( i != 0 || !transfer->carrylf))
+           if (mess->data[i] == '\n' && (i != 0 || !transfer->carrylf))
              {
                transfer->expansion++;
 
                
                i++;
              }
+
          transfer->carrylf = newcarrylf;
-         
        }
 
       return size + 4;