usr/src/cmd/cmd-inet/usr.sbin/in.tftpd.c
changeset 0 68f95e015346
child 1926 d83c7a9aec2d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/cmd/cmd-inet/usr.sbin/in.tftpd.c	Tue Jun 14 00:00:00 2005 -0700
@@ -0,0 +1,1376 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ * Copyright 2004 Sun Microsystems, Inc.  All rights reserved.
+ * Use is subject to license terms.
+ */
+
+/*
+ * Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T
+ * All Rights Reserved.
+ */
+
+/*
+ * University Copyright- Copyright (c) 1982, 1986, 1988
+ * The Regents of the University of California.
+ * All Rights Reserved.
+ *
+ * University Acknowledgment- Portions of this document are derived from
+ * software developed by the University of California, Berkeley, and its
+ * contributors.
+ */
+
+#pragma ident	"%Z%%M%	%I%	%E% SMI"
+
+/*
+ * Trivial file transfer protocol server.  A top level process runs in
+ * an infinite loop fielding new TFTP requests.  A child process,
+ * communicating via a pipe with the top level process, sends delayed
+ * NAKs for those that we can't handle.  A new child process is created
+ * to service each request that we can handle.  The top level process
+ * exits after a period of time during which no new requests are
+ * received.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <netinet/in.h>
+
+#include <arpa/inet.h>
+#include <dirent.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <ctype.h>
+#include <netdb.h>
+#include <setjmp.h>
+#include <syslog.h>
+#include <sys/param.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <string.h>
+#include <priv_utils.h>
+#include "tftpcommon.h"
+
+#define	TIMEOUT		5
+#define	DELAY_SECS	3
+#define	DALLYSECS 60
+
+#define	SYSLOG_MSG(message) \
+	(syslog((((errno == ENETUNREACH) || (errno == EHOSTUNREACH) || \
+		(errno == ECONNREFUSED)) ? LOG_WARNING : LOG_ERR), message))
+
+static int			rexmtval = TIMEOUT;
+static int			maxtimeout = 5*TIMEOUT;
+static int			securetftp;
+static int			debug;
+static int			disable_pnp;
+static int			standalone;
+static uid_t			uid_nobody = UID_NOBODY;
+static uid_t			gid_nobody = GID_NOBODY;
+static int			reqsock = -1;
+				/* file descriptor of request socket */
+static socklen_t		fromlen;
+static socklen_t		fromplen;
+static struct sockaddr_storage	client;
+static struct sockaddr_in6 	*sin6_ptr;
+static struct sockaddr_in	*sin_ptr;
+static struct sockaddr_in6	*from6_ptr;
+static struct sockaddr_in	*from_ptr;
+static int			addrfmly;
+static int			peer;
+static off_t			tsize;
+static tftpbuf			ackbuf;
+static struct sockaddr_storage	from;
+static boolean_t		tsize_set;
+static pid_t			child;
+				/* pid of child handling delayed replys */
+static int			delay_fd [2];
+				/* pipe for communicating with child */
+static FILE			*file;
+static char			*filename;
+
+static union {
+	struct tftphdr	hdr;
+	char		data[SEGSIZE + 4];
+} buf;
+
+static union {
+	struct tftphdr	hdr;
+	char		data[SEGSIZE];
+} oackbuf;
+
+struct	delay_info {
+	long	timestamp;		/* time request received */
+	int	ecode;			/* error code to return */
+	struct	sockaddr_storage from;	/* address of client */
+};
+
+int	blocksize = SEGSIZE;	/* Number of data bytes in a DATA packet */
+
+/*
+ * Default directory for unqualified names
+ * Used by TFTP boot procedures
+ */
+static char	*homedir = "/tftpboot";
+
+struct formats {
+	char	*f_mode;
+	int	(*f_validate)(int);
+	void	(*f_send)(struct formats *, int);
+	void	(*f_recv)(struct formats *, int);
+	int	f_convert;
+};
+
+static void	delayed_responder(void);
+static void	tftp(struct tftphdr *, int);
+static int	validate_filename(int);
+static void	tftpd_sendfile(struct formats *, int);
+static void	tftpd_recvfile(struct formats *, int);
+static void	nak(int);
+static char	*blksize_handler(int, char *, int *);
+static char	*timeout_handler(int, char *, int *);
+static char	*tsize_handler(int, char *, int *);
+
+static struct formats formats[] = {
+	{ "netascii",	validate_filename, tftpd_sendfile, tftpd_recvfile, 1 },
+	{ "octet",	validate_filename, tftpd_sendfile, tftpd_recvfile, 0 },
+	{ NULL }
+};
+
+struct options {
+	char	*opt_name;
+	char	*(*opt_handler)(int, char *, int *);
+};
+
+static struct options options[] = {
+	{ "blksize",	blksize_handler },
+	{ "timeout",	timeout_handler },
+	{ "tsize",	tsize_handler },
+	{ NULL }
+};
+
+static char		optbuf[MAX_OPTVAL_LEN];
+static int		timeout;
+static sigjmp_buf	timeoutbuf;
+
+int
+main(int argc, char **argv)
+{
+	struct tftphdr *tp;
+	int n;
+	int c;
+	struct	passwd *pwd;		/* for "nobody" entry */
+	struct in_addr ipv4addr;
+	char abuf[INET6_ADDRSTRLEN];
+	socklen_t addrlen;
+
+	openlog("tftpd", LOG_PID, LOG_DAEMON);
+
+	pwd = getpwnam("nobody");
+	if (pwd != NULL) {
+		uid_nobody = pwd->pw_uid;
+		gid_nobody = pwd->pw_gid;
+	}
+
+	(void) __init_daemon_priv(
+		PU_LIMITPRIVS,
+		uid_nobody, gid_nobody,
+		PRIV_PROC_FORK, PRIV_PROC_CHROOT, NULL);
+
+	/*
+	 *  Limit set is still "all."  Trim it down to just what we need:
+	 *  fork and chroot.
+	 */
+	(void) priv_set(PRIV_SET,
+	    PRIV_ALLSETS, PRIV_PROC_FORK, PRIV_PROC_CHROOT, NULL);
+	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
+	(void) priv_set(PRIV_SET, PRIV_INHERITABLE, NULL);
+
+	while ((c = getopt(argc, argv, "dspS")) != EOF)
+		switch (c) {
+		case 'd':		/* enable debug */
+			debug++;
+			continue;
+		case 's':		/* secure daemon */
+			securetftp = 1;
+			continue;
+		case 'p':		/* disable name pnp mapping */
+			disable_pnp = 1;
+			continue;
+		case 'S':
+			standalone = 1;
+			continue;
+		case '?':
+		default:
+usage:
+			(void) fprintf(stderr,
+			    "usage:  %s [-spd] [home-directory]\n", argv[0]);
+			for (; optind < argc; optind++)
+				syslog(LOG_ERR, "bad argument %s",
+				    argv[optind]);
+			exit(1);
+		}
+
+	if (optind < argc)
+		if (optind == argc - 1 && *argv [optind] == '/')
+			homedir = argv [optind];
+		else
+			goto usage;
+
+	if (pipe(delay_fd) < 0) {
+		syslog(LOG_ERR, "pipe (main): %m");
+		exit(1);
+	}
+
+	(void) sigset(SIGCHLD, SIG_IGN); /* no zombies please */
+
+	if (standalone) {
+		socklen_t clientlen;
+
+		sin6_ptr = (struct sockaddr_in6 *)&client;
+		clientlen = sizeof (struct sockaddr_in6);
+		reqsock = socket(AF_INET6, SOCK_DGRAM, 0);
+		if (reqsock == -1) {
+			perror("socket");
+			exit(1);
+		}
+		(void) memset(&client, 0, clientlen);
+		sin6_ptr->sin6_family = AF_INET6;
+		sin6_ptr->sin6_port = htons(IPPORT_TFTP);
+		if (bind(reqsock, (struct sockaddr *)&client,
+		    clientlen) == -1) {
+			perror("bind");
+			exit(1);
+		}
+		if (debug)
+			(void) puts("running in standalone mode...");
+	} else {
+		/* request socket passed on fd 0 by inetd */
+		reqsock = 0;
+	}
+	if (debug) {
+		int on = 1;
+
+		(void) setsockopt(reqsock, SOL_SOCKET, SO_DEBUG,
+		    (char *)&on, sizeof (on));
+	}
+
+	(void) chdir(homedir);
+
+	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, PRIV_PROC_FORK, NULL);
+	if ((child = fork()) < 0) {
+		syslog(LOG_ERR, "fork (main): %m");
+		exit(1);
+	}
+	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
+
+	if (child == 0) {
+		(void) priv_set(PRIV_SET, PRIV_ALLSETS, NULL);
+		delayed_responder();
+	} /* child */
+
+	/* close read side of pipe */
+	(void) close(delay_fd[0]);
+
+
+	/*
+	 * Top level handling of incomming tftp requests.  Read a request
+	 * and pass it off to be handled.  If request is valid, handling
+	 * forks off and parent returns to this loop.  If no new requests
+	 * are received for DALLYSECS, exit and return to inetd.
+	 */
+
+	for (;;) {
+		fd_set readfds;
+		struct timeval dally;
+
+		FD_ZERO(&readfds);
+		FD_SET(reqsock, &readfds);
+		dally.tv_sec = DALLYSECS;
+		dally.tv_usec = 0;
+
+		n = select(reqsock + 1, &readfds, NULL, NULL, &dally);
+		if (n < 0) {
+			if (errno == EINTR)
+				continue;
+			syslog(LOG_ERR, "select: %m");
+			(void) kill(child, SIGKILL);
+			exit(1);
+		}
+		if (n == 0) {
+			/* Select timed out.  Its time to die. */
+			if (standalone)
+				continue;
+			else {
+				(void) kill(child, SIGKILL);
+				exit(0);
+			}
+		}
+		addrlen = sizeof (from);
+		if (getsockname(reqsock, (struct sockaddr  *)&from,
+		    &addrlen) < 0) {
+			syslog(LOG_ERR, "getsockname: %m");
+			exit(1);
+		}
+
+		switch (from.ss_family) {
+		case AF_INET:
+			fromlen = (socklen_t)sizeof (struct sockaddr_in);
+			break;
+		case AF_INET6:
+			fromlen = (socklen_t)sizeof (struct sockaddr_in6);
+			break;
+		default:
+			syslog(LOG_ERR,
+			    "Unknown address Family on peer connection %d",
+			    from.ss_family);
+			exit(1);
+		}
+
+		n = recvfrom(reqsock, &buf, sizeof (buf), 0,
+			(struct sockaddr *)&from, &fromlen);
+		if (n < 0) {
+			if (errno == EINTR)
+				continue;
+			if (standalone)
+				perror("recvfrom");
+			else
+				syslog(LOG_ERR, "recvfrom: %m");
+			(void) kill(child, SIGKILL);
+			exit(1);
+		}
+
+		(void) alarm(0);
+
+		switch (from.ss_family) {
+		case AF_INET:
+			addrfmly = AF_INET;
+			fromplen = sizeof (struct sockaddr_in);
+			sin_ptr = (struct sockaddr_in *)&client;
+			(void) memset(&client, 0, fromplen);
+			sin_ptr->sin_family = AF_INET;
+			break;
+		case AF_INET6:
+			addrfmly = AF_INET6;
+			fromplen = sizeof (struct sockaddr_in6);
+			sin6_ptr = (struct sockaddr_in6 *)&client;
+			(void) memset(&client, 0, fromplen);
+			sin6_ptr->sin6_family = AF_INET6;
+			break;
+		default:
+			syslog(LOG_ERR,
+			    "Unknown address Family on peer connection");
+			exit(1);
+		}
+		peer = socket(addrfmly, SOCK_DGRAM, 0);
+		if (peer < 0) {
+			if (standalone)
+				perror("socket (main)");
+			else
+				syslog(LOG_ERR, "socket (main): %m");
+			(void) kill(child, SIGKILL);
+			exit(1);
+		}
+		if (debug) {
+			int on = 1;
+
+			(void) setsockopt(peer, SOL_SOCKET, SO_DEBUG,
+			    (char *)&on, sizeof (on));
+		}
+
+		if (bind(peer, (struct sockaddr *)&client, fromplen) < 0) {
+			if (standalone)
+				perror("bind (main)");
+			else
+				syslog(LOG_ERR, "bind (main): %m");
+			(void) kill(child, SIGKILL);
+			exit(1);
+		}
+		if (standalone && debug) {
+			sin6_ptr = (struct sockaddr_in6 *)&client;
+			from6_ptr = (struct sockaddr_in6 *)&from;
+			if (IN6_IS_ADDR_V4MAPPED(&from6_ptr->sin6_addr)) {
+				IN6_V4MAPPED_TO_INADDR(&from6_ptr->sin6_addr,
+				    &ipv4addr);
+				(void) inet_ntop(AF_INET, &ipv4addr, abuf,
+				    sizeof (abuf));
+			} else {
+				(void) inet_ntop(AF_INET6,
+				    &from6_ptr->sin6_addr, abuf,
+				    sizeof (abuf));
+			}
+			/* get local port */
+			if (getsockname(peer, (struct sockaddr *)&client,
+			    &fromplen) < 0)
+				perror("getsockname (main)");
+			(void) fprintf(stderr,
+			    "request from %s port %d; local port %d\n",
+			    abuf, from6_ptr->sin6_port, sin6_ptr->sin6_port);
+		}
+		tp = &buf.hdr;
+		tp->th_opcode = ntohs((ushort_t)tp->th_opcode);
+		if (tp->th_opcode == RRQ || tp->th_opcode == WRQ)
+			tftp(tp, n);
+
+		(void) close(peer);
+		(void) fclose(file);
+	}
+
+	/*NOTREACHED*/
+	return (0);
+}
+
+static void
+delayed_responder(void)
+{
+	struct delay_info dinfo;
+	long now;
+
+	/* we don't use the descriptors passed in to the parent */
+	(void) close(0);
+	(void) close(1);
+	if (standalone)
+		(void) close(reqsock);
+
+	/* close write side of pipe */
+	(void) close(delay_fd[1]);
+
+	for (;;) {
+		int n;
+
+		if ((n = read(delay_fd[0], &dinfo,
+		    sizeof (dinfo))) != sizeof (dinfo)) {
+			if (n < 0) {
+				if (errno == EINTR)
+					continue;
+				if (standalone)
+					perror("read from pipe "
+					    "(delayed responder)");
+				else
+					syslog(LOG_ERR, "read from pipe: %m");
+			}
+			exit(1);
+		}
+		switch (dinfo.from.ss_family) {
+		case AF_INET:
+			addrfmly = AF_INET;
+			fromplen = sizeof (struct sockaddr_in);
+			sin_ptr = (struct sockaddr_in *)&client;
+			(void) memset(&client, 0, fromplen);
+			sin_ptr->sin_family = AF_INET;
+			break;
+		case AF_INET6:
+			addrfmly = AF_INET6;
+			fromplen = sizeof (struct sockaddr_in6);
+			sin6_ptr = (struct sockaddr_in6 *)&client;
+			(void) memset(&client, 0, fromplen);
+			sin6_ptr->sin6_family = AF_INET6;
+			break;
+		}
+		peer = socket(addrfmly, SOCK_DGRAM, 0);
+		if (peer == -1) {
+			if (standalone)
+				perror("socket (delayed responder)");
+			else
+				syslog(LOG_ERR, "socket (delay): %m");
+			exit(1);
+		}
+		if (debug) {
+			int on = 1;
+
+			(void) setsockopt(peer, SOL_SOCKET, SO_DEBUG,
+			    (char *)&on, sizeof (on));
+		}
+
+		if (bind(peer, (struct sockaddr *)&client, fromplen) < 0) {
+			if (standalone)
+				perror("bind (delayed responder)");
+			else
+				syslog(LOG_ERR, "bind (delay): %m");
+			exit(1);
+		}
+		if (client.ss_family == AF_INET) {
+			from_ptr = (struct sockaddr_in *)&dinfo.from;
+			from_ptr->sin_family = AF_INET;
+		} else {
+			from6_ptr = (struct sockaddr_in6 *)&dinfo.from;
+			from6_ptr->sin6_family = AF_INET6;
+		}
+		/*
+		 * Since a request hasn't been received from the client
+		 * before the delayed responder process is forked, the
+		 * from variable is uninitialized.  So set it to contain
+		 * the client address.
+		 */
+		from = dinfo.from;
+
+		/*
+		 * only sleep if DELAY_SECS has not elapsed since
+		 * original request was received.  Ensure that `now'
+		 * is not earlier than `dinfo.timestamp'
+		 */
+		now = time(0);
+		if ((uint_t)(now - dinfo.timestamp) < DELAY_SECS)
+			(void) sleep(DELAY_SECS - (now - dinfo.timestamp));
+		nak(dinfo.ecode);
+		(void) close(peer);
+	} /* for */
+
+	/* NOTREACHED */
+}
+
+/*
+ * Handle the Blocksize option.
+ * Return the blksize option value string to include in the OACK reply.
+ */
+/*ARGSUSED*/
+static char *
+blksize_handler(int opcode, char *optval, int *errcode)
+{
+	char *endp;
+	int value;
+
+	*errcode = -1;
+	errno = 0;
+	value = (int)strtol(optval, &endp, 10);
+	if (errno != 0 || value < MIN_BLKSIZE || *endp != '\0')
+		return (NULL);
+	/*
+	 * As the blksize value in the OACK reply can be less than the value
+	 * requested, to support broken clients if the value requested is larger
+	 * than allowed in the RFC, reply with the maximum value permitted.
+	 */
+	if (value > MAX_BLKSIZE)
+		value = MAX_BLKSIZE;
+
+	blocksize = value;
+	(void) snprintf(optbuf, sizeof (optbuf), "%d", blocksize);
+	return (optbuf);
+}
+
+/*
+ * Handle the Timeout Interval option.
+ * Return the timeout option value string to include in the OACK reply.
+ */
+/*ARGSUSED*/
+static char *
+timeout_handler(int opcode, char *optval, int *errcode)
+{
+	char *endp;
+	int value;
+
+	*errcode = -1;
+	errno = 0;
+	value = (int)strtol(optval, &endp, 10);
+	if (errno != 0 || *endp != '\0')
+		return (NULL);
+	/*
+	 * The timeout value in the OACK reply must match the value specified
+	 * by the client, so if an invalid timeout is requested don't include
+	 * the timeout option in the OACK reply.
+	 */
+	if (value < MIN_TIMEOUT || value > MAX_TIMEOUT)
+		return (NULL);
+
+	rexmtval = value;
+	maxtimeout = 5 * rexmtval;
+	(void) snprintf(optbuf, sizeof (optbuf), "%d", rexmtval);
+	return (optbuf);
+}
+
+/*
+ * Handle the Transfer Size option.
+ * Return the tsize option value string to include in the OACK reply.
+ */
+static char *
+tsize_handler(int opcode, char *optval, int *errcode)
+{
+	char *endp;
+	longlong_t value;
+
+	*errcode = -1;
+	errno = 0;
+	value = strtoll(optval, &endp, 10);
+	if (errno != 0 || value < 0 || *endp != '\0')
+		return (NULL);
+
+	if (opcode == RRQ) {
+		if (tsize_set == B_FALSE)
+			return (NULL);
+		/*
+		 * The tsize value should be 0 for a read request, but to
+		 * support broken clients we don't check that it is.
+		 */
+	} else {
+#if _FILE_OFFSET_BITS == 32
+		if (value > MAXOFF_T) {
+			*errcode = ENOSPACE;
+			return (NULL);
+		}
+#endif
+		tsize = value;
+		tsize_set = B_TRUE;
+	}
+	(void) snprintf(optbuf, sizeof (optbuf), OFF_T_FMT, tsize);
+	return (optbuf);
+}
+
+/*
+ * Process any options included by the client in the request packet.
+ * Return the size of the OACK reply packet built or 0 for no OACK reply.
+ */
+static int
+process_options(int opcode, char *opts, char *endopts)
+{
+	char *cp, *optname, *optval, *ostr, *oackend;
+	struct tftphdr *oackp;
+	int i, errcode;
+
+	/*
+	 * To continue to interoperate with broken TFTP clients, ignore
+	 * null padding appended to requests which don't include options.
+	 */
+	cp = opts;
+	while ((cp < endopts) && (*cp == '\0'))
+		cp++;
+	if (cp == endopts)
+		return (0);
+
+	/*
+	 * Construct an Option ACKnowledgement packet if any requested option
+	 * is recognized.
+	 */
+	oackp = &oackbuf.hdr;
+	oackend = oackbuf.data + sizeof (oackbuf.data);
+	oackp->th_opcode = htons((ushort_t)OACK);
+	cp = (char *)&oackp->th_stuff;
+	while (opts < endopts) {
+		optname = opts;
+		if ((optval = next_field(optname, endopts)) == NULL) {
+			nak(EOPTNEG);
+			exit(1);
+		}
+		if ((opts = next_field(optval, endopts)) == NULL) {
+			nak(EOPTNEG);
+			exit(1);
+		}
+		for (i = 0; options[i].opt_name != NULL; i++) {
+			if (strcasecmp(optname, options[i].opt_name) == 0)
+				break;
+		}
+		if (options[i].opt_name != NULL) {
+			ostr = options[i].opt_handler(opcode, optval, &errcode);
+			if (ostr != NULL) {
+				cp += strlcpy(cp, options[i].opt_name,
+				    oackend - cp) + 1;
+				if (cp <= oackend)
+					cp += strlcpy(cp, ostr, oackend - cp)
+					    + 1;
+
+				if (cp > oackend) {
+					nak(EOPTNEG);
+					exit(1);
+				}
+			} else if (errcode >= 0) {
+				nak(errcode);
+				exit(1);
+			}
+		}
+	}
+	if (cp != (char *)&oackp->th_stuff)
+		return (cp - oackbuf.data);
+	return (0);
+}
+
+/*
+ * Handle access errors caused by client requests.
+ */
+
+static void
+delay_exit(int ecode)
+{
+	struct delay_info dinfo;
+
+	/*
+	 * The most likely cause of an error here is that
+	 * someone has broadcast an RRQ packet because s/he's
+	 * trying to boot and doesn't know who the server is.
+	 * Rather then sending an ERROR packet immediately, we
+	 * wait a while so that the real server has a better chance
+	 * of getting through (in case client has lousy Ethernet
+	 * interface).  We write to a child that handles delayed
+	 * ERROR packets to avoid delaying service to new
+	 * requests.  Of course, we would rather just not answer
+	 * RRQ packets that are broadcasted, but there's no way
+	 * for a user process to determine this.
+	 */
+
+	dinfo.timestamp = time(0);
+
+	/*
+	 * If running in secure mode, we map all errors to EACCESS
+	 * so that the client gets no information about which files
+	 * or directories exist.
+	 */
+	if (securetftp)
+		dinfo.ecode = EACCESS;
+	else
+		dinfo.ecode = ecode;
+
+	dinfo.from = from;
+	if (write(delay_fd[1], &dinfo, sizeof (dinfo)) !=
+	    sizeof (dinfo)) {
+		syslog(LOG_ERR, "delayed write failed.");
+		(void) kill(child, SIGKILL);
+		exit(1);
+	}
+	exit(0);
+}
+
+/*
+ * Handle initial connection protocol.
+ */
+static void
+tftp(struct tftphdr *tp, int size)
+{
+	char *cp;
+	int readmode, ecode;
+	struct formats *pf;
+	char *mode;
+	int fd;
+	static boolean_t firsttime = B_TRUE;
+	int oacklen;
+	struct stat statb;
+
+	readmode = (tp->th_opcode == RRQ);
+	filename = (char *)&tp->th_stuff;
+	mode = next_field(filename, &buf.data[size]);
+	cp = (mode != NULL) ? next_field(mode, &buf.data[size]) : NULL;
+	if (cp == NULL) {
+		nak(EBADOP);
+		exit(1);
+	}
+	if (debug && standalone) {
+		(void) fprintf(stderr, "%s for %s %s ",
+		    readmode ? "RRQ" : "WRQ", filename, mode);
+		print_options(stderr, cp, size + buf.data - cp);
+		(void) putc('\n', stderr);
+	}
+	for (pf = formats; pf->f_mode != NULL; pf++)
+		if (strcasecmp(pf->f_mode, mode) == 0)
+			break;
+	if (pf->f_mode == NULL) {
+		nak(EBADOP);
+		exit(1);
+	}
+
+	/*
+	 * XXX fork a new process to handle this request before
+	 * chroot(), otherwise the parent won't be able to create a
+	 * new socket as that requires library access to system files
+	 * and devices.
+	 */
+	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, PRIV_PROC_FORK, NULL);
+	switch (fork()) {
+	case -1:
+		syslog(LOG_ERR, "fork (tftp): %m");
+		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
+		return;
+	case 0:
+		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
+		break;
+	default:
+		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
+		return;
+	}
+
+	/*
+	 * Try to see if we can access the file.  The access can still
+	 * fail later if we are running in secure mode because of
+	 * the chroot() call.  We only want to execute the chroot()  once.
+	 */
+	if (securetftp && firsttime) {
+		(void) priv_set(
+		    PRIV_SET, PRIV_EFFECTIVE, PRIV_PROC_CHROOT, NULL);
+		if (chroot(homedir) == -1) {
+			syslog(LOG_ERR,
+			    "tftpd: cannot chroot to directory %s: %m\n",
+			    homedir);
+			delay_exit(EACCESS);
+		}
+		else
+		{
+			firsttime = B_FALSE;
+		}
+		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
+		(void) chdir("/");  /* cd to  new root */
+	}
+	(void) priv_set(PRIV_SET, PRIV_ALLSETS, NULL);
+
+	ecode = (*pf->f_validate)(tp->th_opcode);
+	if (ecode != 0)
+		delay_exit(ecode);
+
+	/* we don't use the descriptors passed in to the parent */
+	(void) close(STDIN_FILENO);
+	(void) close(STDOUT_FILENO);
+
+	/*
+	 * Try to open file as low-priv setuid/setgid.  Note that
+	 * a chroot() has already been done.
+	 */
+	fd = open(filename,
+	    (readmode ? O_RDONLY : (O_WRONLY|O_TRUNC)) | O_NONBLOCK);
+	if ((fd < 0) || (fstat(fd, &statb) < 0))
+		delay_exit((errno == ENOENT) ? ENOTFOUND : EACCESS);
+
+	if (((statb.st_mode & ((readmode) ? S_IROTH : S_IWOTH)) == 0) ||
+	    ((statb.st_mode & S_IFMT) != S_IFREG))
+		delay_exit(EACCESS);
+
+	file = fdopen(fd, readmode ? "r" : "w");
+	if (file == NULL)
+		delay_exit(errno + 100);
+
+	/* Don't know the size of transfers which involve conversion */
+	tsize_set = (readmode && (pf->f_convert == 0));
+	if (tsize_set)
+		tsize = statb.st_size;
+
+	/* Deal with any options sent by the client */
+	oacklen = process_options(tp->th_opcode, cp, buf.data + size);
+
+	if (tp->th_opcode == WRQ)
+		(*pf->f_recv)(pf, oacklen);
+	else
+		(*pf->f_send)(pf, oacklen);
+
+	exit(0);
+}
+
+/*
+ *	Maybe map filename into another one.
+ *
+ *	For PNP, we get TFTP boot requests for filenames like
+ *	<Unknown Hex IP Addr>.<Architecture Name>.   We must
+ *	map these to 'pnp.<Architecture Name>'.  Note that
+ *	uppercase is mapped to lowercase in the architecture names.
+ *
+ *	For names <Hex IP Addr> there are two cases.  First,
+ *	it may be a buggy prom that omits the architecture code.
+ *	So first check if <Hex IP Addr>.<arch> is on the filesystem.
+ *	Second, this is how most Sun3s work; assume <arch> is sun3.
+ */
+
+static char *
+pnp_check(char *origname)
+{
+	static char buf [MAXNAMLEN + 1];
+	char *arch, *s, *bufend;
+	in_addr_t ipaddr;
+	int len = (origname ? strlen(origname) : 0);
+	DIR *dir;
+	struct dirent *dp;
+
+	if (securetftp || disable_pnp || len < 8 || len > 14)
+		return (NULL);
+
+	/*
+	 * XXX see if this cable allows pnp; if not, return NULL
+	 * Requires YP support for determining this!
+	 */
+
+	ipaddr = htonl(strtol(origname, &arch, 16));
+	if ((arch == NULL) || (len > 8 && *arch != '.'))
+		return (NULL);
+	if (len == 8)
+		arch = "SUN3";
+	else
+		arch++;
+
+	/*
+	 * Allow <Hex IP Addr>* filename request to to be
+	 * satisfied by <Hex IP Addr><Any Suffix> rather
+	 * than enforcing this to be Sun3 systems.  Also serves
+	 * to make case of suffix a don't-care.
+	 */
+	if ((dir = opendir(homedir)) == NULL)
+		return (NULL);
+	while ((dp = readdir(dir)) != NULL) {
+		if (strncmp(origname, dp->d_name, 8) == 0) {
+			(void) strlcpy(buf, dp->d_name, sizeof (buf));
+			(void) closedir(dir);
+			return (buf);
+		}
+	}
+	(void) closedir(dir);
+
+	/*
+	 * XXX maybe call YP master for most current data iff
+	 * pnp is enabled.
+	 */
+
+	/*
+	 * only do mapping PNP boot file name for machines that
+	 * are not in the hosts database.
+	 */
+	if (gethostbyaddr((char *)&ipaddr, sizeof (ipaddr), AF_INET) != NULL)
+		return (NULL);
+
+	s = buf + strlcpy(buf, "pnp.", sizeof (buf));
+	bufend = &buf[sizeof (buf) - 1];
+	while ((*arch != '\0') && (s < bufend))
+		*s++ = tolower (*arch++);
+	*s = '\0';
+	return (buf);
+}
+
+
+/*
+ * Try to validate filename. If the filename doesn't exist try PNP mapping.
+ */
+static int
+validate_filename(int mode)
+{
+	struct stat stbuf;
+	char *origfile;
+
+	if (stat(filename, &stbuf) < 0) {
+		if (errno != ENOENT)
+			return (EACCESS);
+		if (mode == WRQ)
+			return (ENOTFOUND);
+
+		/* try to map requested filename into a pnp filename */
+		origfile = filename;
+		filename = pnp_check(origfile);
+		if (filename == NULL)
+			return (ENOTFOUND);
+
+		if (stat(filename, &stbuf) < 0)
+			return (errno == ENOENT ? ENOTFOUND : EACCESS);
+		syslog(LOG_NOTICE, "%s -> %s\n", origfile, filename);
+	}
+
+	return (0);
+}
+
+/* ARGSUSED */
+static void
+timer(int signum)
+{
+	timeout += rexmtval;
+	if (timeout >= maxtimeout)
+		exit(1);
+	siglongjmp(timeoutbuf, 1);
+}
+
+/*
+ * Send the requested file.
+ */
+static void
+tftpd_sendfile(struct formats *pf, int oacklen)
+{
+	struct tftphdr *dp;
+	volatile int block = 1;
+	int size, n, serrno;
+
+	if (oacklen != 0) {
+		(void) sigset(SIGALRM, timer);
+		timeout = 0;
+		(void) sigsetjmp(timeoutbuf, 1);
+		if (debug && standalone) {
+			(void) fputs("Sending OACK ", stderr);
+			print_options(stderr, (char *)&oackbuf.hdr.th_stuff,
+			    oacklen - 2);
+			(void) putc('\n', stderr);
+		}
+		if (sendto(peer, &oackbuf, oacklen, 0,
+		    (struct sockaddr *)&from, fromplen) != oacklen) {
+			if (debug && standalone) {
+				serrno = errno;
+				perror("sendto (oack)");
+				errno = serrno;
+			}
+			SYSLOG_MSG("sendto (oack): %m");
+			goto abort;
+		}
+		(void) alarm(rexmtval); /* read the ack */
+		for (;;) {
+			(void) sigrelse(SIGALRM);
+			n = recv(peer, &ackbuf, sizeof (ackbuf), 0);
+			(void) sighold(SIGALRM);
+			if (n < 0) {
+				if (errno == EINTR)
+					continue;
+				serrno = errno;
+				SYSLOG_MSG("recv (ack): %m");
+				if (debug && standalone) {
+					errno = serrno;
+					perror("recv (ack)");
+				}
+				goto abort;
+			}
+			ackbuf.tb_hdr.th_opcode =
+			    ntohs((ushort_t)ackbuf.tb_hdr.th_opcode);
+			ackbuf.tb_hdr.th_block =
+			    ntohs((ushort_t)ackbuf.tb_hdr.th_block);
+
+			if (ackbuf.tb_hdr.th_opcode == ERROR) {
+				if (debug && standalone) {
+					(void) fprintf(stderr,
+					    "received ERROR %d",
+					    ackbuf.tb_hdr.th_code);
+					if (n > 4)
+						(void) fprintf(stderr,
+						    " %.*s", n - 4,
+						    ackbuf.tb_hdr.th_msg);
+					(void) putc('\n', stderr);
+				}
+				goto abort;
+			}
+
+			if (ackbuf.tb_hdr.th_opcode == ACK) {
+				if (debug && standalone)
+					(void) fprintf(stderr,
+					    "received ACK for block %d\n",
+					    ackbuf.tb_hdr.th_block);
+				if (ackbuf.tb_hdr.th_block == 0)
+					break;
+				/*
+				 * Don't resend the OACK, avoids getting stuck
+				 * in an OACK/ACK loop if the client keeps
+				 * replying with a bad ACK. Client will either
+				 * send a good ACK or timeout sending bad ones.
+				 */
+			}
+		}
+		cancel_alarm();
+	}
+	dp = r_init();
+	do {
+		(void) sigset(SIGALRM, timer);
+		size = readit(file, &dp, pf->f_convert);
+		if (size < 0) {
+			nak(errno + 100);
+			goto abort;
+		}
+		dp->th_opcode = htons((ushort_t)DATA);
+		dp->th_block = htons((ushort_t)block);
+		timeout = 0;
+		(void) sigsetjmp(timeoutbuf, 1);
+		if (debug && standalone)
+			(void) fprintf(stderr, "Sending DATA block %d\n",
+			    block);
+		if (sendto(peer, dp, size + 4, 0,
+		    (struct sockaddr *)&from,  fromplen) != size + 4) {
+			if (debug && standalone) {
+				serrno = errno;
+				perror("sendto (data)");
+				errno = serrno;
+			}
+			SYSLOG_MSG("sendto (data): %m");
+			goto abort;
+		}
+		read_ahead(file, pf->f_convert);
+		(void) alarm(rexmtval); /* read the ack */
+		for (;;) {
+			(void) sigrelse(SIGALRM);
+			n = recv(peer, &ackbuf, sizeof (ackbuf), 0);
+			(void) sighold(SIGALRM);
+			if (n < 0) {
+				if (errno == EINTR)
+					continue;
+				serrno = errno;
+				SYSLOG_MSG("recv (ack): %m");
+				if (debug && standalone) {
+					errno = serrno;
+					perror("recv (ack)");
+				}
+				goto abort;
+			}
+			ackbuf.tb_hdr.th_opcode =
+			    ntohs((ushort_t)ackbuf.tb_hdr.th_opcode);
+			ackbuf.tb_hdr.th_block =
+			    ntohs((ushort_t)ackbuf.tb_hdr.th_block);
+
+			if (ackbuf.tb_hdr.th_opcode == ERROR) {
+				if (debug && standalone) {
+					(void) fprintf(stderr,
+					    "received ERROR %d",
+					    ackbuf.tb_hdr.th_code);
+					if (n > 4)
+						(void) fprintf(stderr,
+						    " %.*s", n - 4,
+						    ackbuf.tb_hdr.th_msg);
+					(void) putc('\n', stderr);
+				}
+				goto abort;
+			}
+
+			if (ackbuf.tb_hdr.th_opcode == ACK) {
+				if (debug && standalone)
+					(void) fprintf(stderr,
+						"received ACK for block %d\n",
+						ackbuf.tb_hdr.th_block);
+				if (ackbuf.tb_hdr.th_block == block) {
+					break;
+				}
+				/*
+				 * Never resend the current DATA packet on
+				 * receipt of a duplicate ACK, doing so would
+				 * cause the "Sorcerer's Apprentice Syndrome".
+				 */
+			}
+		}
+		cancel_alarm();
+		block++;
+	} while (size == blocksize);
+
+abort:
+	cancel_alarm();
+	(void) fclose(file);
+}
+
+/* ARGSUSED */
+static void
+justquit(int signum)
+{
+	exit(0);
+}
+
+/*
+ * Receive a file.
+ */
+static void
+tftpd_recvfile(struct formats *pf, int oacklen)
+{
+	struct tftphdr *dp;
+	struct tftphdr *ap;    /* ack buffer */
+	int block = 0, n, size, acklen, serrno;
+
+	dp = w_init();
+	ap = &ackbuf.tb_hdr;
+	do {
+		(void) sigset(SIGALRM, timer);
+		timeout = 0;
+		if (oacklen == 0) {
+			ap->th_opcode = htons((ushort_t)ACK);
+			ap->th_block = htons((ushort_t)block);
+			acklen = 4;
+		} else {
+			/* copy OACK packet to the ack buffer ready to send */
+			(void) memcpy(&ackbuf, &oackbuf, oacklen);
+			acklen = oacklen;
+			oacklen = 0;
+		}
+		block++;
+		(void) sigsetjmp(timeoutbuf, 1);
+send_ack:
+		if (debug && standalone) {
+			if (ap->th_opcode == htons((ushort_t)ACK)) {
+				(void) fprintf(stderr,
+				    "Sending ACK for block %d\n", block - 1);
+			} else {
+				(void) fprintf(stderr, "Sending OACK ");
+				print_options(stderr, (char *)&ap->th_stuff,
+				    acklen - 2);
+				(void) putc('\n', stderr);
+			}
+		}
+		if (sendto(peer, &ackbuf, acklen, 0, (struct sockaddr *)&from,
+		    fromplen) != acklen) {
+			if (ap->th_opcode == htons((ushort_t)ACK)) {
+				if (debug && standalone) {
+					serrno = errno;
+					perror("sendto (ack)");
+					errno = serrno;
+				}
+				syslog(LOG_ERR, "sendto (ack): %m\n");
+			} else {
+				if (debug && standalone) {
+					serrno = errno;
+					perror("sendto (oack)");
+					errno = serrno;
+				}
+				syslog(LOG_ERR, "sendto (oack): %m\n");
+			}
+			goto abort;
+		}
+		if (write_behind(file, pf->f_convert) < 0) {
+			nak(errno + 100);
+			goto abort;
+		}
+		(void) alarm(rexmtval);
+		for (;;) {
+			(void) sigrelse(SIGALRM);
+			n = recv(peer, dp, blocksize + 4, 0);
+			(void) sighold(SIGALRM);
+			if (n < 0) { /* really? */
+				if (errno == EINTR)
+					continue;
+				syslog(LOG_ERR, "recv (data): %m");
+				goto abort;
+			}
+			dp->th_opcode = ntohs((ushort_t)dp->th_opcode);
+			dp->th_block = ntohs((ushort_t)dp->th_block);
+			if (dp->th_opcode == ERROR) {
+				cancel_alarm();
+				if (debug && standalone) {
+					(void) fprintf(stderr,
+					    "received ERROR %d", dp->th_code);
+					if (n > 4)
+						(void) fprintf(stderr,
+						    " %.*s", n - 4, dp->th_msg);
+					(void) putc('\n', stderr);
+				}
+				return;
+			}
+			if (dp->th_opcode == DATA) {
+				if (debug && standalone)
+					(void) fprintf(stderr,
+						"Received DATA block %d\n",
+						dp->th_block);
+				if (dp->th_block == block) {
+					break;   /* normal */
+				}
+				/* Re-synchronize with the other side */
+				if (synchnet(peer) < 0) {
+					nak(errno + 100);
+					goto abort;
+				}
+				if (dp->th_block == (block-1))
+					goto send_ack; /* rexmit */
+			}
+		}
+		cancel_alarm();
+		/*  size = write(file, dp->th_data, n - 4); */
+		size = writeit(file, &dp, n - 4, pf->f_convert);
+		if (size != (n - 4)) {
+			nak((size < 0) ? (errno + 100) : ENOSPACE);
+			goto abort;
+		}
+	} while (size == blocksize);
+	if (write_behind(file, pf->f_convert) < 0) {
+		nak(errno + 100);
+		goto abort;
+	}
+	n = fclose(file);	/* close data file */
+	file = NULL;
+	if (n == EOF) {
+		nak(errno + 100);
+		goto abort;
+	}
+
+	ap->th_opcode = htons((ushort_t)ACK);    /* send the "final" ack */
+	ap->th_block = htons((ushort_t)(block));
+	if (debug && standalone)
+		(void) fprintf(stderr, "Sending ACK for block %d\n", block);
+	if (sendto(peer, &ackbuf, 4, 0, (struct sockaddr *)&from,
+	    fromplen) == -1) {
+		if (debug && standalone)
+			perror("sendto (ack)");
+	}
+	(void) sigset(SIGALRM, justquit); /* just quit on timeout */
+	(void) alarm(rexmtval);
+	/* normally times out and quits */
+	n = recv(peer, dp, blocksize + 4, 0);
+	(void) alarm(0);
+	dp->th_opcode = ntohs((ushort_t)dp->th_opcode);
+	dp->th_block = ntohs((ushort_t)dp->th_block);
+	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 */
+		if (debug && standalone) {
+			(void) fprintf(stderr, "Sending ACK for block %d\n",
+			    block);
+		}
+		/* resend final ack */
+		if (sendto(peer, &ackbuf, 4, 0, (struct sockaddr *)&from,
+		    fromplen) == -1) {
+			if (debug && standalone)
+				perror("sendto (last ack)");
+		}
+	}
+
+abort:
+	cancel_alarm();
+	if (file != NULL)
+		(void) fclose(file);
+}
+
+/*
+ * Send a nak packet (error message).
+ * Error code passed in is one of the
+ * standard TFTP codes, or a UNIX errno
+ * offset by 100.
+ * Handles connected as well as unconnected peer.
+ */
+static void
+nak(int error)
+{
+	struct tftphdr *tp;
+	int length;
+	struct errmsg *pe;
+	int ret;
+
+	tp = &buf.hdr;
+	tp->th_opcode = htons((ushort_t)ERROR);
+	tp->th_code = htons((ushort_t)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 */
+	}
+	(void) strlcpy(tp->th_msg, (pe->e_msg != NULL) ? pe->e_msg : "UNKNOWN",
+	    sizeof (buf) - sizeof (struct tftphdr));
+	length = strlen(tp->th_msg);
+	length += sizeof (struct tftphdr);
+	if (debug && standalone)
+		(void) fprintf(stderr, "Sending NAK: %s\n", tp->th_msg);
+
+	ret = sendto(peer, &buf, length, 0, (struct sockaddr *)&from,
+	    fromplen);
+	if (ret == -1 && errno == EISCONN) {
+		/* Try without an address */
+		ret = send(peer, &buf, length, 0);
+	}
+	if (ret == -1) {
+		if (standalone)
+			perror("sendto (nak)");
+		else
+			syslog(LOG_ERR, "tftpd: nak: %m\n");
+	} else if (ret != length) {
+		if (standalone)
+			perror("sendto (nak) lost data");
+		else
+			syslog(LOG_ERR, "tftpd: nak: %d lost\n", length - ret);
+	}
+}