From 66f62650c353e901264a4cf0729d35dbc0ae284d Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 5 Jan 2020 16:21:24 +0000 Subject: [PATCH] Add --tftp-single-port option. --- CHANGELOG | 2 + man/dnsmasq.8 | 7 ++- src/dnsmasq.c | 13 ++--- src/dnsmasq.h | 5 +- src/option.c | 3 ++ src/tftp.c | 147 +++++++++++++++++++++++++++++++------------------- 6 files changed, 113 insertions(+), 64 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 795cbd2..570eb46 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,6 +60,8 @@ version 2.81 Fix bug which caused very rarely caused zero-length DHCPv6 packets. Thanks to Dereck Higgins for spotting this. + Add --tftp-single-port option. + version 2.80 Add support for RFC 4039 DHCP rapid commit. Thanks to Ashram Method diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index 8f0ffdb..197d9de 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -1943,7 +1943,12 @@ specifies a range of ports for use by TFTP transfers. This can be useful when TFTP has to traverse a firewall. The start of the range cannot be lower than 1025 unless dnsmasq is running as root. The number of concurrent TFTP connections is limited by the size of the port range. -.TP +.TP +.B --tftp-single-port +Run in a mode where the TFTP server uses ONLY the well-known port (69) for its end +of the TFTP transfer. This allows TFTP to work when there in NAT is the path between client and server. Note that +this is not strictly compliant with the RFCs specifying the TFTP protocol: use at your own risk. +.TP .B \-C, --conf-file= Specify a configuration file. The presence of this option stops dnsmasq from reading the default configuration file (normally /etc/dnsmasq.conf). Multiple files may be specified by repeating the option diff --git a/src/dnsmasq.c b/src/dnsmasq.c index 7842538..4a733ee 100644 --- a/src/dnsmasq.c +++ b/src/dnsmasq.c @@ -1107,7 +1107,7 @@ int main (int argc, char **argv) #endif - /* must do this just before select(), when we know no + /* must do this just before do_poll(), when we know no more calls to my_syslog() can occur */ set_log_writer(); @@ -1670,11 +1670,12 @@ static int set_dns_listeners(time_t now) #ifdef HAVE_TFTP int tftp = 0; struct tftp_transfer *transfer; - for (transfer = daemon->tftp_trans; transfer; transfer = transfer->next) - { - tftp++; - poll_listen(transfer->sockfd, POLLIN); - } + if (!option_bool(OPT_SINGLE_PORT)) + for (transfer = daemon->tftp_trans; transfer; transfer = transfer->next) + { + tftp++; + poll_listen(transfer->sockfd, POLLIN); + } #endif /* will we be able to get memory? */ diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 8e047fc..ce8fbd6 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -262,7 +262,8 @@ struct event_desc { #define OPT_RAPID_COMMIT 57 #define OPT_UBUS 58 #define OPT_IGNORE_CLID 59 -#define OPT_LAST 60 +#define OPT_SINGLE_PORT 60 +#define OPT_LAST 61 #define OPTION_BITS (sizeof(unsigned int)*8) #define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) ) @@ -960,6 +961,8 @@ struct tftp_transfer { unsigned int block, blocksize, expansion; off_t offset; union mysockaddr peer; + union all_addr source; + int if_index; char opt_blocksize, opt_transize, netascii, carrylf; struct tftp_file *file; struct tftp_transfer *next; diff --git a/src/option.c b/src/option.c index 70a6517..6038fa5 100644 --- a/src/option.c +++ b/src/option.c @@ -168,6 +168,7 @@ struct myoption { #define LOPT_CAA 356 #define LOPT_SHARED_NET 357 #define LOPT_IGNORE_CLID 358 +#define LOPT_SINGLE_PORT 359 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -258,6 +259,7 @@ static const struct myoption opts[] = { "tftp-max", 1, 0, LOPT_TFTP_MAX }, { "tftp-mtu", 1, 0, LOPT_TFTP_MTU }, { "tftp-lowercase", 0, 0, LOPT_TFTP_LC }, + { "tftp-single-port", 0, 0, LOPT_SINGLE_PORT }, { "ptr-record", 1, 0, LOPT_PTR }, { "naptr-record", 1, 0, LOPT_NAPTR }, { "bridge-interface", 1, 0 , LOPT_BRIDGE }, @@ -458,6 +460,7 @@ static struct { { LOPT_NOBLOCK, OPT_TFTP_NOBLOCK, NULL, gettext_noop("Disable the TFTP blocksize extension."), NULL }, { LOPT_TFTP_LC, OPT_TFTP_LC, NULL, gettext_noop("Convert TFTP filenames to lowercase"), NULL }, { LOPT_TFTPPORTS, ARG_ONE, ",", gettext_noop("Ephemeral port range for use by TFTP transfers."), NULL }, + { LOPT_SINGLE_PORT, OPT_SINGLE_PORT, NULL, gettext_noop("Use only one port for TFTP server."), NULL }, { LOPT_LOG_OPTS, OPT_LOG_OPTS, NULL, gettext_noop("Extra logging for DHCP."), NULL }, { LOPT_MAX_LOGS, ARG_ONE, "[=]", gettext_noop("Enable async. logging; optionally set queue length."), NULL }, { LOPT_REBIND, OPT_NO_REBIND, NULL, gettext_noop("Stop DNS rebinding. Filter private IP ranges when resolving."), NULL }, diff --git a/src/tftp.c b/src/tftp.c index c618a2c..3af2937 100644 --- a/src/tftp.c +++ b/src/tftp.c @@ -18,6 +18,7 @@ #ifdef HAVE_TFTP +static void handle_tftp(time_t now, struct tftp_transfer *transfer, ssize_t len); static struct tftp_file *check_tftp_fileperm(ssize_t *len, char *prefix); static void free_transfer(struct tftp_transfer *transfer); static ssize_t tftp_err(int err, char *packet, char *message, char *file); @@ -50,7 +51,7 @@ void tftp_request(struct listener *listen, time_t now) struct ifreq ifr; int is_err = 1, if_index = 0, mtu = 0; struct iname *tmp; - struct tftp_transfer *transfer; + struct tftp_transfer *transfer = NULL, **up; int port = daemon->start_tftp_port; /* may be zero to use ephemeral port */ #if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT) int mtuflag = IP_PMTUDISC_DONT; @@ -239,6 +240,25 @@ void tftp_request(struct listener *listen, time_t now) if (mtu == 0) mtu = daemon->tftp_mtu; + /* data transfer via server listening socket */ + if (option_bool(OPT_SINGLE_PORT)) + for (transfer = daemon->tftp_trans, up = &daemon->tftp_trans; transfer; up = &transfer->next, transfer = transfer->next) + if (sockaddr_isequal(&peer, &transfer->peer)) + { + if (ntohs(*((unsigned short *)packet)) == OP_RRQ) + { + /* Handle repeated RRQ or abandoned transfer from same host and port + by unlinking and reusing the struct transfer. */ + *up = transfer->next; + break; + } + else + { + handle_tftp(now, transfer, len); + return; + } + } + if (name) { /* check for per-interface prefix */ @@ -264,16 +284,21 @@ void tftp_request(struct listener *listen, time_t now) #endif } - if (!(transfer = whine_malloc(sizeof(struct tftp_transfer)))) + /* May reuse struct transfer from abandoned transfer in single port mode. */ + if (!transfer && !(transfer = whine_malloc(sizeof(struct tftp_transfer)))) return; - if ((transfer->sockfd = socket(listen->family, SOCK_DGRAM, 0)) == -1) + if (option_bool(OPT_SINGLE_PORT)) + transfer->sockfd = listen->tftpfd; + else if ((transfer->sockfd = socket(listen->family, SOCK_DGRAM, 0)) == -1) { free(transfer); return; } transfer->peer = peer; + transfer->source = addra; + transfer->if_index = if_index; transfer->timeout = now + 2; transfer->backoff = 1; transfer->block = 1; @@ -286,7 +311,7 @@ void tftp_request(struct listener *listen, time_t now) prettyprint_addr(&peer, daemon->addrbuff); /* if we have a nailed-down range, iterate until we find a free one. */ - while (1) + while (!option_bool(OPT_SINGLE_PORT)) { if (bind(transfer->sockfd, &addr.sa, sa_len(&addr)) == -1 || #if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT) @@ -315,7 +340,7 @@ void tftp_request(struct listener *listen, time_t now) p = packet + 2; end = packet + len; - + if (ntohs(*((unsigned short *)packet)) != OP_RRQ || !(filename = next(&p, end)) || !(mode = next(&p, end)) || @@ -439,9 +464,8 @@ void tftp_request(struct listener *listen, time_t now) is_err = 0; } } - - while (sendto(transfer->sockfd, packet, len, 0, - (struct sockaddr *)&peer, sa_len(&peer)) == -1 && errno == EINTR); + + send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), packet, len, &peer, &addra, if_index); if (is_err) free_transfer(transfer); @@ -538,60 +562,25 @@ static struct tftp_file *check_tftp_fileperm(ssize_t *len, char *prefix) void check_tftp_listeners(time_t now) { struct tftp_transfer *transfer, *tmp, **up; - ssize_t len; - struct ack { - unsigned short op, block; - } *mess = (struct ack *)daemon->packet; - - /* Check for activity on any existing transfers */ - for (transfer = daemon->tftp_trans, up = &daemon->tftp_trans; transfer; transfer = tmp) - { - tmp = transfer->next; - - prettyprint_addr(&transfer->peer, daemon->addrbuff); - + /* In single port mode, all packets come via port 69 and tftp_request() */ + if (!option_bool(OPT_SINGLE_PORT)) + for (transfer = daemon->tftp_trans; transfer; transfer = transfer->next) if (poll_check(transfer->sockfd, POLLIN)) { /* we overwrote the buffer... */ daemon->srv_save = NULL; - - if ((len = recv(transfer->sockfd, daemon->packet, daemon->packet_buff_sz, 0)) >= (ssize_t)sizeof(struct ack)) - { - if (ntohs(mess->op) == OP_ACK && ntohs(mess->block) == (unsigned short)transfer->block) - { - /* 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; - } - else if (ntohs(mess->op) == OP_ERR) - { - char *p = daemon->packet + sizeof(struct ack); - char *end = daemon->packet + len; - char *err = next(&p, end); - - /* Sanitise error message */ - if (!err) - err = ""; - else - sanitise(err); - - my_syslog(MS_TFTP | LOG_ERR, _("error %d %s received from %s"), - (int)ntohs(mess->block), err, - daemon->addrbuff); - - /* Got err, ensure we take abort */ - transfer->timeout = now; - transfer->backoff = 100; - } - } + handle_tftp(now, transfer, recv(transfer->sockfd, daemon->packet, daemon->packet_buff_sz, 0)); } + + for (transfer = daemon->tftp_trans, up = &daemon->tftp_trans; transfer; transfer = tmp) + { + tmp = transfer->next; if (difftime(now, transfer->timeout) >= 0.0) { int endcon = 0; + ssize_t len; /* timeout, retransmit */ transfer->timeout += 1 + (1<backoff); @@ -613,13 +602,14 @@ void check_tftp_listeners(time_t now) } if (len != 0) - while(sendto(transfer->sockfd, daemon->packet, len, 0, - (struct sockaddr *)&transfer->peer, sa_len(&transfer->peer)) == -1 && errno == EINTR); - + send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), daemon->packet, len, + &transfer->peer, &transfer->source, transfer->if_index); + if (endcon || len == 0) { strcpy(daemon->namebuff, transfer->file->filename); sanitise(daemon->namebuff); + 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; @@ -638,15 +628,60 @@ void check_tftp_listeners(time_t now) 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) +{ + struct ack { + unsigned short op, block; + } *mess = (struct ack *)daemon->packet; + + if (len >= (ssize_t)sizeof(struct ack)) + { + if (ntohs(mess->op) == OP_ACK && ntohs(mess->block) == (unsigned short)transfer->block) + { + /* 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; + } + else if (ntohs(mess->op) == OP_ERR) + { + char *p = daemon->packet + sizeof(struct ack); + char *end = daemon->packet + len; + char *err = next(&p, end); + + prettyprint_addr(&transfer->peer, daemon->addrbuff); + + /* Sanitise error message */ + if (!err) + err = ""; + else + sanitise(err); + + my_syslog(MS_TFTP | LOG_ERR, _("error %d %s received from %s"), + (int)ntohs(mess->block), err, + daemon->addrbuff); + + /* Got err, ensure we take abort */ + transfer->timeout = now; + transfer->backoff = 100; + } + } +} static void free_transfer(struct tftp_transfer *transfer) { - close(transfer->sockfd); + if (!option_bool(OPT_SINGLE_PORT)) + close(transfer->sockfd); + if (transfer->file && (--transfer->file->refcount) == 0) { close(transfer->file->fd); free(transfer->file); } + free(transfer); }