freebsd-skq/libexec/tftpd/tftpd.c
ambrisko 0b5427f932 Better handle the case with a network that drops packets by retrying
with a back off.  This was discovered when Luigi sent me code to
handle this for Etherboot.  The Etherboot patch worked okay but
FreeBSD's tftpd had trouble handling it and would fail to transfer
the file since it would abort on send and not retry.

Submitted by:	luigi
MFC after:	1 week
2002-04-09 19:13:43 +00:00

822 lines
19 KiB
C

/*
* Copyright (c) 1983, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#ifndef lint
static const char copyright[] =
"@(#) Copyright (c) 1983, 1993\n\
The Regents of the University of California. All rights reserved.\n";
#endif /* not lint */
#ifndef lint
#if 0
static char sccsid[] = "@(#)tftpd.c 8.1 (Berkeley) 6/4/93";
#endif
static const char rcsid[] =
"$FreeBSD$";
#endif /* not lint */
/*
* Trivial file transfer protocol server.
*
* This version includes many modifications by Jim Guyton
* <guyton@rand-unix>.
*/
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/tftp.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <libutil.h>
#include <netdb.h>
#include <pwd.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "tftpsubs.h"
#define TIMEOUT 5
#define MAX_TIMEOUTS 5
int peer;
int rexmtval = TIMEOUT;
int max_rexmtval = 2*TIMEOUT;
#define PKTSIZE SEGSIZE+4
char buf[PKTSIZE];
char ackbuf[PKTSIZE];
struct sockaddr_in from;
int fromlen;
void tftp(struct tftphdr *, int);
/*
* Null-terminated directory prefix list for absolute pathname requests and
* search list for relative pathname requests.
*
* MAXDIRS should be at least as large as the number of arguments that
* inetd allows (currently 20).
*/
#define MAXDIRS 20
static struct dirlist {
char *name;
int len;
} dirs[MAXDIRS+1];
static int suppress_naks;
static int logging;
static int ipchroot;
static char *errtomsg(int);
static void nak(int);
static void oack();
int
main(int argc, char *argv[])
{
struct tftphdr *tp;
int n;
int ch, on;
struct sockaddr_in sin;
char *chroot_dir = NULL;
struct passwd *nobody;
char *chuser = "nobody";
openlog("tftpd", LOG_PID | LOG_NDELAY, LOG_FTP);
while ((ch = getopt(argc, argv, "cClns:u:")) != -1) {
switch (ch) {
case 'c':
ipchroot = 1;
break;
case 'C':
ipchroot = 2;
break;
case 'l':
logging = 1;
break;
case 'n':
suppress_naks = 1;
break;
case 's':
chroot_dir = optarg;
break;
case 'u':
chuser = optarg;
break;
default:
syslog(LOG_WARNING, "ignoring unknown option -%c", ch);
}
}
if (optind < argc) {
struct dirlist *dirp;
/* Get list of directory prefixes. Skip relative pathnames. */
for (dirp = dirs; optind < argc && dirp < &dirs[MAXDIRS];
optind++) {
if (argv[optind][0] == '/') {
dirp->name = argv[optind];
dirp->len = strlen(dirp->name);
dirp++;
}
}
}
else if (chroot_dir) {
dirs->name = "/";
dirs->len = 1;
}
if (ipchroot && chroot_dir == NULL) {
syslog(LOG_ERR, "-c requires -s");
exit(1);
}
on = 1;
if (ioctl(0, FIONBIO, &on) < 0) {
syslog(LOG_ERR, "ioctl(FIONBIO): %m");
exit(1);
}
fromlen = sizeof (from);
n = recvfrom(0, buf, sizeof (buf), 0,
(struct sockaddr *)&from, &fromlen);
if (n < 0) {
syslog(LOG_ERR, "recvfrom: %m");
exit(1);
}
/*
* Now that we have read the message out of the UDP
* socket, we fork and exit. Thus, inetd will go back
* to listening to the tftp port, and the next request
* to come in will start up a new instance of tftpd.
*
* We do this so that inetd can run tftpd in "wait" mode.
* The problem with tftpd running in "nowait" mode is that
* inetd may get one or more successful "selects" on the
* tftp port before we do our receive, so more than one
* instance of tftpd may be started up. Worse, if tftpd
* break before doing the above "recvfrom", inetd would
* spawn endless instances, clogging the system.
*/
{
int pid;
int i, j;
for (i = 1; i < 20; i++) {
pid = fork();
if (pid < 0) {
sleep(i);
/*
* flush out to most recently sent request.
*
* This may drop some request, but those
* will be resent by the clients when
* they timeout. The positive effect of
* this flush is to (try to) prevent more
* than one tftpd being started up to service
* a single request from a single client.
*/
j = sizeof from;
i = recvfrom(0, buf, sizeof (buf), 0,
(struct sockaddr *)&from, &j);
if (i > 0) {
n = i;
fromlen = j;
}
} else {
break;
}
}
if (pid < 0) {
syslog(LOG_ERR, "fork: %m");
exit(1);
} else if (pid != 0) {
exit(0);
}
}
/*
* Since we exit here, we should do that only after the above
* recvfrom to keep inetd from constantly forking should there
* be a problem. See the above comment about system clogging.
*/
if (chroot_dir) {
if (ipchroot) {
char *tempchroot;
struct stat sb;
int statret;
tempchroot = inet_ntoa(from.sin_addr);
asprintf(&tempchroot, "%s/%s", chroot_dir, tempchroot);
statret = stat(tempchroot, &sb);
if ((sb.st_mode & S_IFDIR) &&
(statret == 0 || (statret == -1 && ipchroot == 1)))
chroot_dir = tempchroot;
}
/* Must get this before chroot because /etc might go away */
if ((nobody = getpwnam(chuser)) == NULL) {
syslog(LOG_ERR, "%s: no such user", chuser);
exit(1);
}
if (chroot(chroot_dir)) {
syslog(LOG_ERR, "chroot: %s: %m", chroot_dir);
exit(1);
}
chdir( "/" );
setuid(nobody->pw_uid);
setgroups(1, &nobody->pw_gid);
}
from.sin_family = AF_INET;
alarm(0);
close(0);
close(1);
peer = socket(AF_INET, SOCK_DGRAM, 0);
if (peer < 0) {
syslog(LOG_ERR, "socket: %m");
exit(1);
}
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
if (bind(peer, (struct sockaddr *)&sin, sizeof (sin)) < 0) {
syslog(LOG_ERR, "bind: %m");
exit(1);
}
if (connect(peer, (struct sockaddr *)&from, sizeof(from)) < 0) {
syslog(LOG_ERR, "connect: %m");
exit(1);
}
tp = (struct tftphdr *)buf;
tp->th_opcode = ntohs(tp->th_opcode);
if (tp->th_opcode == RRQ || tp->th_opcode == WRQ)
tftp(tp, n);
exit(1);
}
struct formats;
int validate_access(char **, int);
void xmitfile(struct formats *);
void recvfile(struct formats *);
struct formats {
char *f_mode;
int (*f_validate)(char **, int);
void (*f_send)(struct formats *);
void (*f_recv)(struct formats *);
int f_convert;
} formats[] = {
{ "netascii", validate_access, xmitfile, recvfile, 1 },
{ "octet", validate_access, xmitfile, recvfile, 0 },
#ifdef notdef
{ "mail", validate_user, sendmail, recvmail, 1 },
#endif
{ 0 }
};
struct options {
char *o_type;
char *o_request;
int o_reply; /* turn into union if need be */
} options[] = {
{ "tsize" }, /* OPT_TSIZE */
{ "timeout" }, /* OPT_TIMEOUT */
{ NULL }
};
enum opt_enum {
OPT_TSIZE = 0,
OPT_TIMEOUT,
};
/*
* Handle initial connection protocol.
*/
void
tftp(struct tftphdr *tp, int size)
{
char *cp;
int i, first = 1, has_options = 0, ecode;
struct formats *pf;
char *filename, *mode, *option, *ccp;
filename = cp = tp->th_stuff;
again:
while (cp < buf + size) {
if (*cp == '\0')
break;
cp++;
}
if (*cp != '\0') {
nak(EBADOP);
exit(1);
}
if (first) {
mode = ++cp;
first = 0;
goto again;
}
for (cp = mode; *cp; cp++)
if (isupper(*cp))
*cp = tolower(*cp);
for (pf = formats; pf->f_mode; pf++)
if (strcmp(pf->f_mode, mode) == 0)
break;
if (pf->f_mode == 0) {
nak(EBADOP);
exit(1);
}
while (++cp < buf + size) {
for (i = 2, ccp = cp; i > 0; ccp++) {
if (ccp >= buf + size) {
/*
* Don't reject the request, just stop trying
* to parse the option and get on with it.
* Some Apple OpenFirmware versions have
* trailing garbage on the end of otherwise
* valid requests.
*/
goto option_fail;
} else if (*ccp == '\0')
i--;
}
for (option = cp; *cp; cp++)
if (isupper(*cp))
*cp = tolower(*cp);
for (i = 0; options[i].o_type != NULL; i++)
if (strcmp(option, options[i].o_type) == 0) {
options[i].o_request = ++cp;
has_options = 1;
}
cp = ccp-1;
}
option_fail:
if (options[OPT_TIMEOUT].o_request) {
int to = atoi(options[OPT_TIMEOUT].o_request);
if (to < 1 || to > 255) {
nak(EBADOP);
exit(1);
}
else if (to <= max_rexmtval)
options[OPT_TIMEOUT].o_reply = rexmtval = to;
else
options[OPT_TIMEOUT].o_request = NULL;
}
ecode = (*pf->f_validate)(&filename, tp->th_opcode);
if (has_options)
oack();
if (logging) {
char host[MAXHOSTNAMELEN];
realhostname(host, sizeof(host) - 1, &from.sin_addr);
host[sizeof(host) - 1] = '\0';
syslog(LOG_INFO, "%s: %s request for %s: %s", host,
tp->th_opcode == WRQ ? "write" : "read",
filename, errtomsg(ecode));
}
if (ecode) {
/*
* Avoid storms of naks to a RRQ broadcast for a relative
* bootfile pathname from a diskless Sun.
*/
if (suppress_naks && *filename != '/' && ecode == ENOTFOUND)
exit(0);
nak(ecode);
exit(1);
}
if (tp->th_opcode == WRQ)
(*pf->f_recv)(pf);
else
(*pf->f_send)(pf);
exit(0);
}
FILE *file;
/*
* Validate file access. Since we
* have no uid or gid, for now require
* file to exist and be publicly
* readable/writable.
* If we were invoked with arguments
* from inetd then the file must also be
* in one of the given directory prefixes.
* Note also, full path name must be
* given as we have no login directory.
*/
int
validate_access(char **filep, int mode)
{
struct stat stbuf;
int fd;
struct dirlist *dirp;
static char pathname[MAXPATHLEN];
char *filename = *filep;
/*
* Prevent tricksters from getting around the directory restrictions
*/
if (strstr(filename, "/../"))
return (EACCESS);
if (*filename == '/') {
/*
* Allow the request if it's in one of the approved locations.
* Special case: check the null prefix ("/") by looking
* for length = 1 and relying on the arg. processing that
* it's a /.
*/
for (dirp = dirs; dirp->name != NULL; dirp++) {
if (dirp->len == 1 ||
(!strncmp(filename, dirp->name, dirp->len) &&
filename[dirp->len] == '/'))
break;
}
/* If directory list is empty, allow access to any file */
if (dirp->name == NULL && dirp != dirs)
return (EACCESS);
if (stat(filename, &stbuf) < 0)
return (errno == ENOENT ? ENOTFOUND : EACCESS);
if ((stbuf.st_mode & S_IFMT) != S_IFREG)
return (ENOTFOUND);
if (mode == RRQ) {
if ((stbuf.st_mode & S_IROTH) == 0)
return (EACCESS);
} else {
if ((stbuf.st_mode & S_IWOTH) == 0)
return (EACCESS);
}
} else {
int err;
/*
* Relative file name: search the approved locations for it.
* Don't allow write requests that avoid directory
* restrictions.
*/
if (!strncmp(filename, "../", 3))
return (EACCESS);
/*
* If the file exists in one of the directories and isn't
* readable, continue looking. However, change the error code
* to give an indication that the file exists.
*/
err = ENOTFOUND;
for (dirp = dirs; dirp->name != NULL; dirp++) {
snprintf(pathname, sizeof(pathname), "%s/%s",
dirp->name, filename);
if (stat(pathname, &stbuf) == 0 &&
(stbuf.st_mode & S_IFMT) == S_IFREG) {
if ((stbuf.st_mode & S_IROTH) != 0) {
break;
}
err = EACCESS;
}
}
if (dirp->name == NULL)
return (err);
*filep = filename = pathname;
}
if (options[OPT_TSIZE].o_request) {
if (mode == RRQ)
options[OPT_TSIZE].o_reply = stbuf.st_size;
else
/* XXX Allows writes of all sizes. */
options[OPT_TSIZE].o_reply =
atoi(options[OPT_TSIZE].o_request);
}
fd = open(filename, mode == RRQ ? O_RDONLY : O_WRONLY|O_TRUNC);
if (fd < 0)
return (errno + 100);
file = fdopen(fd, (mode == RRQ)? "r":"w");
if (file == NULL) {
return errno+100;
}
return (0);
}
int timeouts;
jmp_buf timeoutbuf;
void
timer(int sig __unused)
{
if (++timeouts > MAX_TIMEOUTS)
exit(1);
longjmp(timeoutbuf, 1);
}
/*
* Send the requested file.
*/
void
xmitfile(struct formats *pf)
{
struct tftphdr *dp, *r_init();
struct tftphdr *ap; /* ack packet */
int size, n;
volatile unsigned short block;
signal(SIGALRM, timer);
dp = r_init();
ap = (struct tftphdr *)ackbuf;
block = 1;
do {
size = readit(file, &dp, pf->f_convert);
if (size < 0) {
nak(errno + 100);
goto abort;
}
dp->th_opcode = htons((u_short)DATA);
dp->th_block = htons((u_short)block);
timeouts = 0;
(void)setjmp(timeoutbuf);
send_data:
{
int i, t = 1;
for (i = 0; ; i++){
if (send(peer, dp, size + 4, 0) != size + 4) {
sleep(t);
t = (t < 32) ? t<< 1 : t;
if (i >= 12) {
syslog(LOG_ERR, "write: %m");
goto abort;
}
}
break;
}
}
read_ahead(file, pf->f_convert);
for ( ; ; ) {
alarm(rexmtval); /* read the ack */
n = recv(peer, ackbuf, sizeof (ackbuf), 0);
alarm(0);
if (n < 0) {
syslog(LOG_ERR, "read: %m");
goto abort;
}
ap->th_opcode = ntohs((u_short)ap->th_opcode);
ap->th_block = ntohs((u_short)ap->th_block);
if (ap->th_opcode == ERROR)
goto abort;
if (ap->th_opcode == ACK) {
if (ap->th_block == block)
break;
/* Re-synchronize with the other side */
(void) synchnet(peer);
if (ap->th_block == (block -1))
goto send_data;
}
}
block++;
} while (size == SEGSIZE);
abort:
(void) fclose(file);
}
void
justquit(int sig __unused)
{
exit(0);
}
/*
* Receive a file.
*/
void
recvfile(struct formats *pf)
{
struct tftphdr *dp, *w_init();
struct tftphdr *ap; /* ack buffer */
int n, size;
volatile unsigned short block;
signal(SIGALRM, timer);
dp = w_init();
ap = (struct tftphdr *)ackbuf;
block = 0;
do {
timeouts = 0;
ap->th_opcode = htons((u_short)ACK);
ap->th_block = htons((u_short)block);
block++;
(void) setjmp(timeoutbuf);
send_ack:
if (send(peer, ackbuf, 4, 0) != 4) {
syslog(LOG_ERR, "write: %m");
goto abort;
}
write_behind(file, pf->f_convert);
for ( ; ; ) {
alarm(rexmtval);
n = recv(peer, dp, PKTSIZE, 0);
alarm(0);
if (n < 0) { /* really? */
syslog(LOG_ERR, "read: %m");
goto abort;
}
dp->th_opcode = ntohs((u_short)dp->th_opcode);
dp->th_block = ntohs((u_short)dp->th_block);
if (dp->th_opcode == ERROR)
goto abort;
if (dp->th_opcode == DATA) {
if (dp->th_block == block) {
break; /* normal */
}
/* Re-synchronize with the other side */
(void) synchnet(peer);
if (dp->th_block == (block-1))
goto send_ack; /* rexmit */
}
}
/* size = write(file, dp->th_data, n - 4); */
size = writeit(file, &dp, n - 4, pf->f_convert);
if (size != (n-4)) { /* ahem */
if (size < 0) nak(errno + 100);
else nak(ENOSPACE);
goto abort;
}
} while (size == SEGSIZE);
write_behind(file, pf->f_convert);
(void) fclose(file); /* close data file */
ap->th_opcode = htons((u_short)ACK); /* send the "final" ack */
ap->th_block = htons((u_short)(block));
(void) send(peer, ackbuf, 4, 0);
signal(SIGALRM, justquit); /* just quit on timeout */
alarm(rexmtval);
n = recv(peer, buf, sizeof (buf), 0); /* normally times out and quits */
alarm(0);
if (n >= 4 && /* if read some data */
dp->th_opcode == DATA && /* and got a data block */
block == dp->th_block) { /* then my last ack was lost */
(void) send(peer, ackbuf, 4, 0); /* resend final ack */
}
abort:
return;
}
struct errmsg {
int e_code;
char *e_msg;
} errmsgs[] = {
{ EUNDEF, "Undefined error code" },
{ ENOTFOUND, "File not found" },
{ EACCESS, "Access violation" },
{ ENOSPACE, "Disk full or allocation exceeded" },
{ EBADOP, "Illegal TFTP operation" },
{ EBADID, "Unknown transfer ID" },
{ EEXISTS, "File already exists" },
{ ENOUSER, "No such user" },
{ EOPTNEG, "Option negotiation" },
{ -1, 0 }
};
static char *
errtomsg(int error)
{
static char buf[20];
struct errmsg *pe;
if (error == 0)
return "success";
for (pe = errmsgs; pe->e_code >= 0; pe++)
if (pe->e_code == error)
return pe->e_msg;
snprintf(buf, sizeof(buf), "error %d", error);
return buf;
}
/*
* Send a nak packet (error message).
* Error code passed in is one of the
* standard TFTP codes, or a UNIX errno
* offset by 100.
*/
static void
nak(int error)
{
struct tftphdr *tp;
int length;
struct errmsg *pe;
tp = (struct tftphdr *)buf;
tp->th_opcode = htons((u_short)ERROR);
tp->th_code = htons((u_short)error);
for (pe = errmsgs; pe->e_code >= 0; pe++)
if (pe->e_code == error)
break;
if (pe->e_code < 0) {
pe->e_msg = strerror(error - 100);
tp->th_code = EUNDEF; /* set 'undef' errorcode */
}
strcpy(tp->th_msg, pe->e_msg);
length = strlen(pe->e_msg);
tp->th_msg[length] = '\0';
length += 5;
if (send(peer, buf, length, 0) != length)
syslog(LOG_ERR, "nak: %m");
}
/*
* Send an oack packet (option acknowledgement).
*/
static void
oack(void)
{
struct tftphdr *tp, *ap;
int size, i, n;
char *bp;
tp = (struct tftphdr *)buf;
bp = buf + 2;
size = sizeof(buf) - 2;
tp->th_opcode = htons((u_short)OACK);
for (i = 0; options[i].o_type != NULL; i++) {
if (options[i].o_request) {
n = snprintf(bp, size, "%s%c%d", options[i].o_type,
0, options[i].o_reply);
bp += n+1;
size -= n+1;
if (size < 0) {
syslog(LOG_ERR, "oack: buffer overflow");
exit(1);
}
}
}
size = bp - buf;
ap = (struct tftphdr *)ackbuf;
signal(SIGALRM, timer);
timeouts = 0;
(void)setjmp(timeoutbuf);
if (send(peer, buf, size, 0) != size) {
syslog(LOG_INFO, "oack: %m");
exit(1);
}
for (;;) {
alarm(rexmtval);
n = recv(peer, ackbuf, sizeof (ackbuf), 0);
alarm(0);
if (n < 0) {
syslog(LOG_ERR, "recv: %m");
exit(1);
}
ap->th_opcode = ntohs((u_short)ap->th_opcode);
ap->th_block = ntohs((u_short)ap->th_block);
if (ap->th_opcode == ERROR)
exit(1);
if (ap->th_opcode == ACK && ap->th_block == 0)
break;
}
}