diff --git a/CHANGELOG b/CHANGELOG index 524ca4b..0378682 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,8 @@ version 2.92 Fix failure to cache PTR RRs when a reply contains more than one answer. Thanks to Dmitry for spotting this. + Add TFTP options windowsize (RFC 7440) and timeout (RFC 2349). + version 2.91 Fix spurious "resource limit exceeded messages". Thanks to diff --git a/src/config.h b/src/config.h index 9d0eecf..e5dc967 100644 --- a/src/config.h +++ b/src/config.h @@ -52,6 +52,8 @@ #define CHUSER "nobody" #define CHGRP "dip" #define TFTP_MAX_CONNECTIONS 50 /* max simultaneous connections */ +#define TFTP_MAX_WINDOW 32 /* max window size to negotiate */ +#define TFTP_TRANSFER_TIME 120 /* Abandon TFTP transfers after this long. Two mins. */ #define LOG_MAX 5 /* log-queue length */ #define RANDFILE "/dev/urandom" #define DNSMASQ_SERVICE "uk.org.thekelleys.dnsmasq" /* Default - may be overridden by config */ diff --git a/src/dnsmasq.c b/src/dnsmasq.c index bfdc913..ebb38ad 100644 --- a/src/dnsmasq.c +++ b/src/dnsmasq.c @@ -30,7 +30,9 @@ static volatile pid_t pid = 0; static volatile int pipewrite; static void set_dns_listeners(void); +#ifdef HAVE_TFTP static void set_tftp_listeners(void); +#endif static void check_dns_listeners(time_t now); static void do_tcp_connection(struct listener *listener, time_t now, int slot); static void sig_handler(int sig); diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 83c750d..1bda100 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -1117,14 +1117,13 @@ struct tftp_file { struct tftp_transfer { int sockfd; - time_t timeout; - int backoff; - unsigned int block, blocksize, expansion; + time_t retransmit, start; + unsigned int lastack, block, blocksize, windowsize, timeout, expansion; off_t offset; union mysockaddr peer; union all_addr source; int if_index; - char opt_blocksize, opt_transize, netascii, carrylf; + unsigned char opt_blocksize, opt_transize, opt_windowsize, opt_timeout, netascii, carrylf, backoff; struct tftp_file *file; struct tftp_transfer *next; }; @@ -1768,7 +1767,6 @@ void queue_relay_snoop(struct in6_addr *client, int if_index, struct in6_addr *p /* tftp.c */ #ifdef HAVE_TFTP -void tftp_request(struct listener *listen, time_t now); void check_tftp_listeners(time_t now); int do_tftp_script_run(void); #endif diff --git a/src/tftp.c b/src/tftp.c index 6ed41bc..dfd6e8e 100644 --- a/src/tftp.c +++ b/src/tftp.c @@ -41,11 +41,11 @@ static void sanitise(char *buf); #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; @@ -308,7 +308,9 @@ void tftp_request(struct listener *listen, time_t now) /* 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) @@ -320,15 +322,13 @@ void tftp_request(struct listener *listen, time_t now) 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. */ @@ -361,140 +361,167 @@ void tftp_request(struct listener *listen, time_t now) 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 && 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; + 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; + } } - 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]; + 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)); -#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) - { + 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); @@ -610,6 +637,10 @@ void check_tftp_listeners(time_t now) 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 @@ -628,66 +659,94 @@ void check_tftp_listeners(time_t now) 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) { - int endcon = 0; + 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) + { + /* 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; - - /* 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) - { - /* 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; - } - - if (len != 0) - { - 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); -#endif - } - if (endcon || len == 0) + transfer->retransmit += transfer->timeout + (1<<(transfer->backoff/2)); + transfer->backoff++; + transfer->block = transfer->lastack; + + if ((len = get_block(daemon->packet, transfer)) == 0) + endcon = 1; /* got last ACK */ + 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 + /* 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++) { - /* put on queue to be sent to script and deleted */ - transfer->next = daemon->tftp_done_trans; - daemon->tftp_done_trans = transfer; + if (i != 0) + len = get_block(daemon->packet, transfer); + + 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); +#endif } - continue; } } - - up = &transfer->next; - } -} + + 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); + /* unlink */ + *up = tmp; + if (error) + 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; + } + } + 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) { @@ -697,13 +756,30 @@ 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) { @@ -724,8 +800,7 @@ static void handle_tftp(time_t now, struct tftp_transfer *transfer, ssize_t len) daemon->addrbuff); /* Got err, ensure we take abort */ - transfer->timeout = now; - transfer->backoff = 100; + transfer->start = 0; } } } @@ -835,7 +910,17 @@ static ssize_t get_block(char *packet, struct tftp_transfer *transfer) 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 @@ -846,31 +931,35 @@ static ssize_t get_block(char *packet, struct tftp_transfer *transfer) 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++; @@ -885,8 +974,8 @@ static ssize_t get_block(char *packet, struct tftp_transfer *transfer) i++; } + transfer->carrylf = newcarrylf; - } return size + 4;