diff options
Diffstat (limited to 'libexec/tftpd')
-rw-r--r-- | libexec/tftpd/Makefile | 18 | ||||
-rw-r--r-- | libexec/tftpd/Makefile.depend | 16 | ||||
-rw-r--r-- | libexec/tftpd/Makefile.depend.options | 5 | ||||
-rw-r--r-- | libexec/tftpd/tests/Makefile | 8 | ||||
-rw-r--r-- | libexec/tftpd/tests/functional.c | 1283 | ||||
-rw-r--r-- | libexec/tftpd/tftp-file.c | 299 | ||||
-rw-r--r-- | libexec/tftpd/tftp-file.h | 39 | ||||
-rw-r--r-- | libexec/tftpd/tftp-io.c | 446 | ||||
-rw-r--r-- | libexec/tftpd/tftp-io.h | 46 | ||||
-rw-r--r-- | libexec/tftpd/tftp-options.c | 477 | ||||
-rw-r--r-- | libexec/tftpd/tftp-options.h | 68 | ||||
-rw-r--r-- | libexec/tftpd/tftp-transfer.c | 446 | ||||
-rw-r--r-- | libexec/tftpd/tftp-transfer.h | 30 | ||||
-rw-r--r-- | libexec/tftpd/tftp-utils.c | 328 | ||||
-rw-r--r-- | libexec/tftpd/tftp-utils.h | 129 | ||||
-rw-r--r-- | libexec/tftpd/tftpd.8 | 333 | ||||
-rw-r--r-- | libexec/tftpd/tftpd.c | 849 |
17 files changed, 4820 insertions, 0 deletions
diff --git a/libexec/tftpd/Makefile b/libexec/tftpd/Makefile new file mode 100644 index 000000000000..1cea8178ae07 --- /dev/null +++ b/libexec/tftpd/Makefile @@ -0,0 +1,18 @@ +.include <src.opts.mk> + +PROG= tftpd +MAN= tftpd.8 +SRCS= tftp-file.c tftp-io.c tftp-options.c tftp-transfer.c tftp-utils.c +SRCS+= tftpd.c + +.if ${MK_TCP_WRAPPERS} != "no" +CFLAGS+= -DLIBWRAP +LIBADD= wrap +.endif + +CWARNFLAGS.gcc+= -Wno-format-nonliteral + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include <bsd.prog.mk> diff --git a/libexec/tftpd/Makefile.depend b/libexec/tftpd/Makefile.depend new file mode 100644 index 000000000000..344a5d0e9310 --- /dev/null +++ b/libexec/tftpd/Makefile.depend @@ -0,0 +1,16 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + include \ + include/arpa \ + include/xlocale \ + lib/${CSU_DIR} \ + lib/libc \ + lib/libcompiler_rt \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/libexec/tftpd/Makefile.depend.options b/libexec/tftpd/Makefile.depend.options new file mode 100644 index 000000000000..fbc21670804e --- /dev/null +++ b/libexec/tftpd/Makefile.depend.options @@ -0,0 +1,5 @@ +# This file is not autogenerated - take care! + +DIRDEPS_OPTIONS= TCP_WRAPPERS + +.include <dirdeps-options.mk> diff --git a/libexec/tftpd/tests/Makefile b/libexec/tftpd/tests/Makefile new file mode 100644 index 000000000000..d1faca03331e --- /dev/null +++ b/libexec/tftpd/tests/Makefile @@ -0,0 +1,8 @@ +.include <bsd.own.mk> + +ATF_TESTS_C= functional +TEST_METADATA.functional+= timeout=15 + +LIBADD= util + +.include <bsd.test.mk> diff --git a/libexec/tftpd/tests/functional.c b/libexec/tftpd/tests/functional.c new file mode 100644 index 000000000000..791aa9190a2f --- /dev/null +++ b/libexec/tftpd/tests/functional.c @@ -0,0 +1,1283 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2018 Alan Somers. + * + * 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +#include <sys/param.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/wait.h> + +#include <netinet/in.h> + +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdalign.h> +#include <stdio.h> +#include <unistd.h> + +#include <atf-c.h> +#include <libutil.h> + +static const uint16_t BASEPORT = 6969; +static const char pidfile[] = "tftpd.pid"; +static int protocol = PF_UNSPEC; +static int s = -1; /* tftp client socket */ +static struct sockaddr_storage addr; /* Destination address for the client */ +static bool s_flag = false; /* Pass -s to tftpd */ +static bool w_flag = false; /* Pass -w to tftpd */ + +/* Helper functions*/ +static void require_bufeq(const char *expected, size_t expected_len, + const char *actual, size_t len); + +/* + * Receive a response from tftpd + * @param hdr The reply's expected header, as a char array + * @param contents The reply's expected contents, as a char array + * @param contents_len Length of contents + */ +#define RECV(hdr, contents, contents_len) do { \ + char buffer[1024]; \ + struct sockaddr_storage from; \ + socklen_t fromlen = sizeof(from); \ + ssize_t r = recvfrom(s, buffer, sizeof(buffer), 0, \ + (struct sockaddr *)&from, &fromlen); \ + ATF_REQUIRE(r > 0); \ + require_bufeq((hdr), sizeof(hdr), buffer, \ + MIN((size_t)r, sizeof(hdr))); \ + require_bufeq((const char *) (contents), (contents_len), \ + &buffer[sizeof(hdr)], r - sizeof(hdr)); \ + if (protocol == PF_INET) { \ + ((struct sockaddr_in *)&addr)->sin_port = \ + ((struct sockaddr_in *)&from)->sin_port; \ + } else { \ + ((struct sockaddr_in6 *)&addr)->sin6_port = \ + ((struct sockaddr_in6 *)&from)->sin6_port; \ + } \ +} while(0) + +static void +recv_ack(uint16_t blocknum) +{ + char hdr[] = {0, 4, blocknum >> 8, blocknum & 0xFF}; + RECV(hdr, NULL, 0); +} + +static void +recv_oack(const char *options, size_t options_len) +{ + char hdr[] = {0, 6}; + RECV(hdr, options, options_len); +} + +/* + * Receive a data packet from tftpd + * @param blocknum Expected block number to be received + * @param contents Pointer to expected contents + * @param contents_len Length of contents expected to receive + */ +static void +recv_data(uint16_t blocknum, const char *contents, size_t contents_len) +{ + char hdr[] = {0, 3, blocknum >> 8, blocknum & 0xFF}; + RECV(hdr, contents, contents_len); +} + +#define RECV_ERROR(code, msg) do { \ + char hdr[] = {0, 5, code >> 8, code & 0xFF}; \ + RECV(hdr, msg, sizeof(msg)); \ +} while (0) + +/* + * send a command to tftpd. + * @param cmd Command to send, as a char array + */ +static void +send_bytes(const void *cmd, size_t len) +{ + ssize_t r; + + r = sendto(s, cmd, len, 0, (struct sockaddr *)(&addr), addr.ss_len); + ATF_REQUIRE(r >= 0); + ATF_REQUIRE_EQ(len, (size_t)r); +} + +static void +send_data(uint16_t blocknum, const char *contents, size_t contents_len) +{ + char buffer[1024]; + + buffer[0] = 0; /* DATA opcode high byte */ + buffer[1] = 3; /* DATA opcode low byte */ + buffer[2] = blocknum >> 8; + buffer[3] = blocknum & 0xFF; + memmove(&buffer[4], contents, contents_len); + send_bytes(buffer, 4 + contents_len); +} + +/* + * send a command to tftpd. + * @param cmd Command to send, as a const string + * (terminating NUL will be ignored) + */ +#define SEND_STR(cmd) \ + ATF_REQUIRE_EQ(sizeof(cmd) - 1, \ + sendto(s, (cmd), sizeof(cmd) - 1, 0, \ + (struct sockaddr *)(&addr), addr.ss_len)) + +/* + * Acknowledge block blocknum + */ +static void +send_ack(uint16_t blocknum) +{ + char packet[] = { + 0, 4, /* ACK opcode in BE */ + blocknum >> 8, + blocknum & 0xFF + }; + + send_bytes(packet, sizeof(packet)); +} + +/* + * build an option string + */ +#define OPTION_STR(name, value) name "\000" value "\000" + +/* + * send a read request to tftpd. + * @param filename filename as a string, absolute or relative + * @param mode either "octet" or "netascii" + */ +#define SEND_RRQ(filename, mode) \ + SEND_STR("\0\001" filename "\0" mode "\0") + +/* + * send a read request with options + */ +#define SEND_RRQ_OPT(filename, mode, options) \ + SEND_STR("\0\001" filename "\0" mode "\000" options) + +/* + * send a write request to tftpd. + * @param filename filename as a string, absolute or relative + * @param mode either "octet" or "netascii" + */ +#define SEND_WRQ(filename, mode) \ + SEND_STR("\0\002" filename "\0" mode "\0") + +/* + * send a write request with options + */ +#define SEND_WRQ_OPT(filename, mode, options) \ + SEND_STR("\0\002" filename "\0" mode "\000" options) + +/* Define a test case, for both IPv4 and IPv6 */ +#define TFTPD_TC_DEFINE(name, head, ...) \ +static void \ +name ## _body(void); \ +ATF_TC_WITH_CLEANUP(name ## _v4); \ +ATF_TC_HEAD(name ## _v4, tc) \ +{ \ + head \ +} \ +ATF_TC_BODY(name ## _v4, tc) \ +{ \ + int exitcode = 0; \ + __VA_ARGS__; \ + protocol = AF_INET; \ + s = setup(&addr, __COUNTER__); \ + name ## _body(); \ + close(s); \ + if (exitcode >= 0) \ + check_server(exitcode); \ +} \ +ATF_TC_CLEANUP(name ## _v4, tc) \ +{ \ + cleanup(); \ +} \ +ATF_TC_WITH_CLEANUP(name ## _v6); \ +ATF_TC_HEAD(name ## _v6, tc) \ +{ \ + head \ +} \ +ATF_TC_BODY(name ## _v6, tc) \ +{ \ + int exitcode = 0; \ + __VA_ARGS__; \ + protocol = AF_INET6; \ + s = setup(&addr, __COUNTER__); \ + name ## _body(); \ + close(s); \ + if (exitcode >= 0) \ + check_server(exitcode); \ +} \ +ATF_TC_CLEANUP(name ## _v6, tc) \ +{ \ + cleanup(); \ +} \ +static void \ +name ## _body(void) + +/* Add the IPv4 and IPv6 versions of a test case */ +#define TFTPD_TC_ADD(tp, name) do { \ + ATF_TP_ADD_TC(tp, name ## _v4); \ + ATF_TP_ADD_TC(tp, name ## _v6); \ +} while (0) + +static void +sigalrm(int signo __unused) +{ +} + +/* Check that server exits with specific exit code */ +static void +check_server(int exitcode) +{ + struct sigaction sa = { .sa_handler = sigalrm }; + struct itimerval it = { .it_value = { .tv_sec = 30 } }; + FILE *f; + pid_t pid; + int wstatus; + + f = fopen(pidfile, "r"); + ATF_REQUIRE(f != NULL); + ATF_REQUIRE_INTEQ(1, fscanf(f, "%d", &pid)); + ATF_CHECK_INTEQ(0, fclose(f)); + ATF_REQUIRE_INTEQ(0, sigaction(SIGALRM, &sa, NULL)); + ATF_REQUIRE_EQ(0, setitimer(ITIMER_REAL, &it, NULL)); + ATF_REQUIRE_EQ(pid, waitpid(pid, &wstatus, 0)); + ATF_CHECK(WIFEXITED(wstatus)); + ATF_CHECK_INTEQ(exitcode, WEXITSTATUS(wstatus)); + unlink(pidfile); +} + +/* Standard cleanup used by all testcases */ +static void +cleanup(void) +{ + FILE *f; + pid_t pid; + + f = fopen(pidfile, "r"); + if (f == NULL) + return; + unlink(pidfile); + if (fscanf(f, "%d", &pid) == 1) { + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + } + fclose(f); +} + +/* Assert that two binary buffers are identical */ +static void +require_bufeq(const char *expected, size_t expected_len, + const char *actual, size_t len) +{ + size_t i; + + ATF_REQUIRE_EQ_MSG(expected_len, len, + "Expected %zu bytes but got %zu", expected_len, len); + for (i = 0; i < len; i++) { + ATF_REQUIRE_EQ_MSG(expected[i], actual[i], + "Expected %#hhx at position %zu; got %hhx instead", + expected[i], i, actual[i]); + } +} + +/* + * Start tftpd and return its communicating socket + * @param to Will be filled in for use with sendto + * @param idx Unique identifier of the test case + * @return Socket ready to use + */ +static int +setup(struct sockaddr_storage *to, uint16_t idx) +{ + int client_s, server_s, pid, argv_idx; + char execname[] = "/usr/libexec/tftpd"; + char b_flag_str[] = "-b"; + char s_flag_str[] = "-s"; + char w_flag_str[] = "-w"; + char pwd[MAXPATHLEN]; + char *argv[10]; + struct sockaddr_in addr4; + struct sockaddr_in6 addr6; + struct sockaddr *server_addr; + struct pidfh *pfh; + uint16_t port = BASEPORT + idx; + socklen_t len; + int pd[2]; + + ATF_REQUIRE_EQ(0, pipe2(pd, O_CLOEXEC)); + + if (protocol == PF_INET) { + len = sizeof(addr4); + bzero(&addr4, len); + addr4.sin_len = len; + addr4.sin_family = PF_INET; + addr4.sin_port = htons(port); + server_addr = (struct sockaddr *)&addr4; + } else { + len = sizeof(addr6); + bzero(&addr6, len); + addr6.sin6_len = len; + addr6.sin6_family = PF_INET6; + addr6.sin6_port = htons(port); + server_addr = (struct sockaddr *)&addr6; + } + + ATF_REQUIRE_EQ(pwd, getcwd(pwd, sizeof(pwd))); + + /* Must bind(2) pre-fork so it happens before the client's send(2) */ + server_s = socket(protocol, SOCK_DGRAM, 0); + if (server_s < 0 && errno == EAFNOSUPPORT) { + atf_tc_skip("This test requires IPv%d support", + protocol == PF_INET ? 4 : 6); + } + ATF_REQUIRE_MSG(server_s >= 0, + "socket failed with error %s", strerror(errno)); + ATF_REQUIRE_EQ_MSG(0, bind(server_s, server_addr, len), + "bind failed with error %s", strerror(errno)); + + pid = fork(); + switch (pid) { + case -1: + atf_tc_fail("fork failed"); + break; + case 0: + /* In child */ + pfh = pidfile_open(pidfile, 0644, NULL); + ATF_REQUIRE_MSG(pfh != NULL, + "pidfile_open: %s", strerror(errno)); + ATF_REQUIRE_EQ(0, pidfile_write(pfh)); + ATF_REQUIRE_EQ(0, pidfile_close(pfh)); + + bzero(argv, sizeof(argv)); + argv[0] = execname; + argv_idx = 1; + argv[argv_idx++] = b_flag_str; + if (w_flag) + argv[argv_idx++] = w_flag_str; + if (s_flag) + argv[argv_idx++] = s_flag_str; + argv[argv_idx++] = pwd; + ATF_REQUIRE_EQ(STDOUT_FILENO, dup2(server_s, STDOUT_FILENO)); + ATF_REQUIRE_EQ(STDIN_FILENO, dup2(server_s, STDIN_FILENO)); + ATF_REQUIRE_EQ(STDERR_FILENO, dup2(server_s, STDERR_FILENO)); + execv(execname, argv); + atf_tc_fail("exec failed"); + break; + default: + /* In parent */ + ATF_REQUIRE_INTEQ(0, close(pd[1])); + /* block until other end is closed on exec() or exit() */ + ATF_REQUIRE_INTEQ(0, read(pd[0], &pd[1], sizeof(pd[1]))); + ATF_REQUIRE_INTEQ(0, close(pd[0])); + bzero(to, sizeof(*to)); + if (protocol == PF_INET) { + struct sockaddr_in *to4 = (struct sockaddr_in *)to; + to4->sin_len = sizeof(*to4); + to4->sin_family = PF_INET; + to4->sin_port = htons(port); + to4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + } else { + struct in6_addr loopback = IN6ADDR_LOOPBACK_INIT; + struct sockaddr_in6 *to6 = (struct sockaddr_in6 *)to; + to6->sin6_len = sizeof(*to6); + to6->sin6_family = PF_INET6; + to6->sin6_port = htons(port); + to6->sin6_addr = loopback; + } + + ATF_REQUIRE_INTEQ(0, close(server_s)); + ATF_REQUIRE((client_s = socket(protocol, SOCK_DGRAM, 0)) > 0); + break; + } + + /* Clear the client's umask. Test cases will specify exact modes */ + umask(0000); + + return (client_s); +} + +/* Like write(2), but never returns less than the requested length */ +static void +write_all(int fd, const void *buf, size_t nbytes) +{ + ssize_t r; + + while (nbytes > 0) { + r = write(fd, buf, nbytes); + ATF_REQUIRE(r > 0); + nbytes -= (size_t)r; + buf = (const char *)buf + (size_t)r; + } +} + + +/* + * Test Cases + */ + +/* + * Read a file, specified by absolute pathname. + */ +TFTPD_TC_DEFINE(abspath,) +{ + int fd; + char command[1024]; + size_t pathlen; + char suffix[] = {'\0', 'o', 'c', 't', 'e', 't', '\0'}; + + command[0] = 0; /* RRQ high byte */ + command[1] = 1; /* RRQ low byte */ + ATF_REQUIRE(getcwd(&command[2], sizeof(command) - 2) != NULL); + pathlen = strlcat(&command[2], "/abspath.txt", sizeof(command) - 2); + ATF_REQUIRE(pathlen + sizeof(suffix) < sizeof(command) - 2); + memmove(&command[2 + pathlen], suffix, sizeof(suffix)); + + fd = open("abspath.txt", O_CREAT | O_RDONLY, 0644); + ATF_REQUIRE(fd >= 0); + close(fd); + + send_bytes(command, 2 + pathlen + sizeof(suffix)); + recv_data(1, NULL, 0); + send_ack(1); +} + +/* + * Attempt to read a file outside of the allowed directory(ies) + */ +TFTPD_TC_DEFINE(dotdot,) +{ + ATF_REQUIRE_EQ(0, mkdir("subdir", 0777)); + SEND_RRQ("../disallowed.txt", "octet"); + RECV_ERROR(2, "Access violation"); + s = setup(&addr, __COUNTER__); + SEND_RRQ("subdir/../../disallowed.txt", "octet"); + RECV_ERROR(2, "Access violation"); + s = setup(&addr, __COUNTER__); + SEND_RRQ("/etc/passwd", "octet"); + RECV_ERROR(2, "Access violation"); +} + +/* + * With "-s", tftpd should chroot to the specified directory + */ +TFTPD_TC_DEFINE(s_flag, + atf_tc_set_md_var(tc, "require.user", "root");, + s_flag = true) +{ + int fd; + char contents[] = "small"; + + fd = open("small.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, strlen(contents) + 1); + close(fd); + + SEND_RRQ("/small.txt", "octet"); + recv_data(1, contents, strlen(contents) + 1); + send_ack(1); +} + +/* + * Read a file, and simulate a dropped ACK packet + */ +TFTPD_TC_DEFINE(rrq_dropped_ack,) +{ + int fd; + char contents[] = "small"; + + fd = open("small.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, strlen(contents) + 1); + close(fd); + + SEND_RRQ("small.txt", "octet"); + recv_data(1, contents, strlen(contents) + 1); + /* + * client "sends" the ack, but network drops it + * Eventually, tftpd should resend the data packet + */ + recv_data(1, contents, strlen(contents) + 1); + send_ack(1); +} + +/* + * Read a file, and simulate a dropped DATA packet + */ +TFTPD_TC_DEFINE(rrq_dropped_data,) +{ + int fd; + size_t i; + uint32_t contents[192]; + char buffer[1024]; + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, sizeof(contents)); + close(fd); + + SEND_RRQ("medium.txt", "octet"); + recv_data(1, (const char *)&contents[0], 512); + send_ack(1); + (void) recvfrom(s, buffer, sizeof(buffer), 0, NULL, NULL); + /* + * server "sends" the data, but network drops it + * Eventually, client should resend the last ACK + */ + send_ack(1); + recv_data(2, (const char *)&contents[128], 256); + send_ack(2); +} + +/* + * Read a medium file, and simulate a duplicated ACK packet + */ +TFTPD_TC_DEFINE(rrq_duped_ack,) +{ + int fd; + size_t i; + uint32_t contents[192]; + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, sizeof(contents)); + close(fd); + + SEND_RRQ("medium.txt", "octet"); + recv_data(1, (const char *)&contents[0], 512); + send_ack(1); + send_ack(1); /* Dupe an ACK packet */ + recv_data(2, (const char *)&contents[128], 256); + recv_data(2, (const char *)&contents[128], 256); + send_ack(2); +} + + +/* + * Attempt to read a file without read permissions + */ +TFTPD_TC_DEFINE(rrq_eaccess,) +{ + int fd; + + fd = open("empty.txt", O_CREAT | O_RDONLY, 0000); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_RRQ("empty.txt", "octet"); + RECV_ERROR(2, "Access violation"); +} + +/* + * Read an empty file + */ +TFTPD_TC_DEFINE(rrq_empty,) +{ + int fd; + + fd = open("empty.txt", O_CREAT | O_RDONLY, 0644); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_RRQ("empty.txt", "octet"); + recv_data(1, NULL, 0); + send_ack(1); +} + +/* + * Read a medium file of more than one block + */ +TFTPD_TC_DEFINE(rrq_medium,) +{ + int fd; + size_t i; + uint32_t contents[192]; + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, sizeof(contents)); + close(fd); + + SEND_RRQ("medium.txt", "octet"); + recv_data(1, (const char *)&contents[0], 512); + send_ack(1); + recv_data(2, (const char *)&contents[128], 256); + send_ack(2); +} + +/* + * Read a medium file with a window size of 2. + */ +TFTPD_TC_DEFINE(rrq_medium_window,) +{ + int fd; + size_t i; + uint32_t contents[192]; + char options[] = OPTION_STR("windowsize", "2"); + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, sizeof(contents)); + close(fd); + + SEND_RRQ_OPT("medium.txt", "octet", OPTION_STR("windowsize", "2")); + recv_oack(options, sizeof(options) - 1); + send_ack(0); + recv_data(1, (const char *)&contents[0], 512); + recv_data(2, (const char *)&contents[128], 256); + send_ack(2); +} + +/* + * Read a file in netascii format + */ +TFTPD_TC_DEFINE(rrq_netascii,) +{ + int fd; + char contents[] = "foo\nbar\rbaz\n"; + /* + * Weirdly, RFC-764 says that CR must be followed by NUL if a line feed + * is not intended + */ + char expected[] = "foo\r\nbar\r\0baz\r\n"; + + fd = open("unix.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, strlen(contents) + 1); + close(fd); + + SEND_RRQ("unix.txt", "netascii"); + recv_data(1, expected, sizeof(expected)); + send_ack(1); +} + +/* + * Read a file that doesn't exist + */ +TFTPD_TC_DEFINE(rrq_nonexistent,) +{ + SEND_RRQ("nonexistent.txt", "octet"); + RECV_ERROR(1, "File not found"); +} + +/* + * Attempt to read a file whose name exceeds PATH_MAX + */ +TFTPD_TC_DEFINE(rrq_path_max,) +{ +#define AReallyBigFileName \ + "AReallyBigFileNameXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"\ + ".txt" + ATF_REQUIRE_MSG(strlen(AReallyBigFileName) > PATH_MAX, + "Somebody increased PATH_MAX. Update the test"); + SEND_RRQ(AReallyBigFileName, "octet"); + RECV_ERROR(4, "Illegal TFTP operation"); +} + +/* + * Read a small file of less than one block + */ +TFTPD_TC_DEFINE(rrq_small,) +{ + int fd; + char contents[] = "small"; + + fd = open("small.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, strlen(contents) + 1); + close(fd); + + SEND_RRQ("small.txt", "octet"); + recv_data(1, contents, strlen(contents) + 1); + send_ack(1); +} + +/* + * Read a file following the example in RFC 7440. + */ +TFTPD_TC_DEFINE(rrq_window_rfc7440,) +{ + int fd; + size_t i; + char options[] = OPTION_STR("windowsize", "4"); + alignas(uint32_t) char contents[13 * 512 - 4]; + uint32_t *u32p; + + u32p = (uint32_t *)contents; + for (i = 0; i < sizeof(contents) / sizeof(uint32_t); i++) + u32p[i] = i; + + fd = open("rfc7440.txt", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, sizeof(contents)); + close(fd); + + SEND_RRQ_OPT("rfc7440.txt", "octet", OPTION_STR("windowsize", "4")); + recv_oack(options, sizeof(options) - 1); + send_ack(0); + recv_data(1, &contents[0 * 512], 512); + recv_data(2, &contents[1 * 512], 512); + recv_data(3, &contents[2 * 512], 512); + recv_data(4, &contents[3 * 512], 512); + send_ack(4); + recv_data(5, &contents[4 * 512], 512); + recv_data(6, &contents[5 * 512], 512); + recv_data(7, &contents[6 * 512], 512); + recv_data(8, &contents[7 * 512], 512); + + /* ACK 5 as if 6-8 were dropped. */ + send_ack(5); + recv_data(6, &contents[5 * 512], 512); + recv_data(7, &contents[6 * 512], 512); + recv_data(8, &contents[7 * 512], 512); + recv_data(9, &contents[8 * 512], 512); + send_ack(9); + recv_data(10, &contents[9 * 512], 512); + recv_data(11, &contents[10 * 512], 512); + recv_data(12, &contents[11 * 512], 512); + recv_data(13, &contents[12 * 512], 508); + + /* Drop ACK and after timeout receive 10-13. */ + recv_data(10, &contents[9 * 512], 512); + recv_data(11, &contents[10 * 512], 512); + recv_data(12, &contents[11 * 512], 512); + recv_data(13, &contents[12 * 512], 508); + send_ack(13); +} + +/* + * Try to transfer a file with an unknown mode. + */ +TFTPD_TC_DEFINE(unknown_modes,) +{ + SEND_RRQ("foo.txt", "ascii"); /* Misspelling of "ascii" */ + RECV_ERROR(4, "Illegal TFTP operation"); + s = setup(&addr, __COUNTER__); + SEND_RRQ("foo.txt", "binary"); /* Obsolete. Use "octet" instead */ + RECV_ERROR(4, "Illegal TFTP operation"); + s = setup(&addr, __COUNTER__); + SEND_RRQ("foo.txt", "en_US.UTF-8"); + RECV_ERROR(4, "Illegal TFTP operation"); + s = setup(&addr, __COUNTER__); + SEND_RRQ("foo.txt", "mail"); /* Obsolete in RFC-1350 */ + RECV_ERROR(4, "Illegal TFTP operation"); +} + +/* + * Send an unknown opcode. tftpd should respond with the appropriate error + */ +TFTPD_TC_DEFINE(unknown_opcode,) +{ + /* Looks like an RRQ or WRQ request, but with a bad opcode */ + SEND_STR("\0\007foo.txt\0octet\0"); + RECV_ERROR(4, "Illegal TFTP operation"); +} + +/* + * Invoke tftpd with "-w" and write to a nonexistent file. + */ +TFTPD_TC_DEFINE(w_flag,, w_flag = 1;) +{ + int fd; + ssize_t r; + char contents[] = "small"; + char buffer[1024]; + size_t contents_len; + + contents_len = strlen(contents) + 1; + SEND_WRQ("small.txt", "octet"); + recv_ack(0); + send_data(1, contents, contents_len); + recv_ack(1); + + fd = open("small.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq(contents, contents_len, buffer, (size_t)r); +} + +/* + * Write a medium file, and simulate a dropped ACK packet + */ +TFTPD_TC_DEFINE(wrq_dropped_ack,) +{ + int fd; + size_t i; + ssize_t r; + uint32_t contents[192]; + char buffer[1024]; + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_WRQ("medium.txt", "octet"); + recv_ack(0); + send_data(1, (const char *)&contents[0], 512); + /* + * Servers "sends" an ACK packet, but network drops it. + * Eventually, server should resend the last ACK + */ + (void) recvfrom(s, buffer, sizeof(buffer), 0, NULL, NULL); + recv_ack(1); + send_data(2, (const char *)&contents[128], 256); + recv_ack(2); + + fd = open("medium.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq((const char *)contents, 768, buffer, (size_t)r); +} + +/* + * Write a small file, and simulate a dropped DATA packet + */ +TFTPD_TC_DEFINE(wrq_dropped_data,) +{ + int fd; + ssize_t r; + char contents[] = "small"; + size_t contents_len; + char buffer[1024]; + + fd = open("small.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + contents_len = strlen(contents) + 1; + + SEND_WRQ("small.txt", "octet"); + recv_ack(0); + /* + * Client "sends" a DATA packet, but network drops it. + * Eventually, server should resend the last ACK + */ + recv_ack(0); + send_data(1, contents, contents_len); + recv_ack(1); + + fd = open("small.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq(contents, contents_len, buffer, (size_t)r); +} + +/* + * Write a medium file, and simulate a duplicated DATA packet + */ +TFTPD_TC_DEFINE(wrq_duped_data,) +{ + int fd; + size_t i; + ssize_t r; + uint32_t contents[192]; + char buffer[1024]; + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_WRQ("medium.txt", "octet"); + recv_ack(0); + send_data(1, (const char *)&contents[0], 512); + send_data(1, (const char *)&contents[0], 512); + recv_ack(1); + recv_ack(1); + send_data(2, (const char *)&contents[128], 256); + recv_ack(2); + + fd = open("medium.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq((const char *)contents, 768, buffer, (size_t)r); +} + +/* + * Attempt to write a file without write permissions + */ +TFTPD_TC_DEFINE(wrq_eaccess,) +{ + int fd; + + fd = open("empty.txt", O_CREAT | O_RDONLY, 0440); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_WRQ("empty.txt", "octet"); + RECV_ERROR(2, "Access violation"); +} + +/* + * Attempt to write a file without world write permissions, but with world + * read permissions + */ +TFTPD_TC_DEFINE(wrq_eaccess_world_readable,) +{ + int fd; + + fd = open("empty.txt", O_CREAT | O_RDONLY, 0444); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_WRQ("empty.txt", "octet"); + RECV_ERROR(2, "Access violation"); +} + + +/* + * Write a medium file of more than one block + */ +TFTPD_TC_DEFINE(wrq_medium,) +{ + int fd; + size_t i; + ssize_t r; + uint32_t contents[192]; + char buffer[1024]; + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_WRQ("medium.txt", "octet"); + recv_ack(0); + send_data(1, (const char *)&contents[0], 512); + recv_ack(1); + send_data(2, (const char *)&contents[128], 256); + recv_ack(2); + + fd = open("medium.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq((const char *)contents, 768, buffer, (size_t)r); +} + +/* + * Write a medium file with a window size of 2. + */ +TFTPD_TC_DEFINE(wrq_medium_window,) +{ + int fd; + size_t i; + ssize_t r; + uint32_t contents[192]; + char buffer[1024]; + char options[] = OPTION_STR("windowsize", "2"); + + for (i = 0; i < nitems(contents); i++) + contents[i] = i; + + fd = open("medium.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_WRQ_OPT("medium.txt", "octet", OPTION_STR("windowsize", "2")); + recv_oack(options, sizeof(options) - 1); + send_data(1, (const char *)&contents[0], 512); + send_data(2, (const char *)&contents[128], 256); + recv_ack(2); + + fd = open("medium.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq((const char *)contents, 768, buffer, (size_t)r); +} + +/* + * Write a file in netascii format + */ +TFTPD_TC_DEFINE(wrq_netascii,) +{ + int fd; + ssize_t r; + /* + * Weirdly, RFC-764 says that CR must be followed by NUL if a line feed + * is not intended + */ + char contents[] = "foo\r\nbar\r\0baz\r\n"; + char expected[] = "foo\nbar\rbaz\n"; + size_t contents_len; + char buffer[1024]; + + fd = open("unix.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + contents_len = sizeof(contents); + + SEND_WRQ("unix.txt", "netascii"); + recv_ack(0); + send_data(1, contents, contents_len); + recv_ack(1); + + fd = open("unix.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq(expected, sizeof(expected), buffer, (size_t)r); +} + +/* + * Attempt to write to a nonexistent file. With the default options, this + * isn't allowed. + */ +TFTPD_TC_DEFINE(wrq_nonexistent,) +{ + SEND_WRQ("nonexistent.txt", "octet"); + RECV_ERROR(1, "File not found"); +} + +/* + * Write a small file of less than one block + */ +TFTPD_TC_DEFINE(wrq_small,) +{ + int fd; + ssize_t r; + char contents[] = "small"; + size_t contents_len; + char buffer[1024]; + + fd = open("small.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + contents_len = strlen(contents) + 1; + + SEND_WRQ("small.txt", "octet"); + recv_ack(0); + send_data(1, contents, contents_len); + recv_ack(1); + + fd = open("small.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq(contents, contents_len, buffer, (size_t)r); +} + +/* + * Write an empty file over a non-empty one + */ +TFTPD_TC_DEFINE(wrq_truncate,) +{ + int fd; + char contents[] = "small"; + struct stat sb; + + fd = open("small.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + write_all(fd, contents, strlen(contents) + 1); + close(fd); + + SEND_WRQ("small.txt", "octet"); + recv_ack(0); + send_data(1, NULL, 0); + recv_ack(1); + + ATF_REQUIRE_EQ(0, stat("small.txt", &sb)); + ATF_REQUIRE_EQ(0, sb.st_size); +} + +/* + * Write a file following the example in RFC 7440. + */ +TFTPD_TC_DEFINE(wrq_window_rfc7440,) +{ + int fd; + size_t i; + ssize_t r; + char options[] = OPTION_STR("windowsize", "4"); + alignas(uint32_t) char contents[13 * 512 - 4]; + char buffer[sizeof(contents)]; + uint32_t *u32p; + + u32p = (uint32_t *)contents; + for (i = 0; i < sizeof(contents) / sizeof(uint32_t); i++) + u32p[i] = i; + + fd = open("rfc7440.txt", O_RDWR | O_CREAT, 0666); + ATF_REQUIRE(fd >= 0); + close(fd); + + SEND_WRQ_OPT("rfc7440.txt", "octet", OPTION_STR("windowsize", "4")); + recv_oack(options, sizeof(options) - 1); + send_data(1, &contents[0 * 512], 512); + send_data(2, &contents[1 * 512], 512); + send_data(3, &contents[2 * 512], 512); + send_data(4, &contents[3 * 512], 512); + recv_ack(4); + send_data(5, &contents[4 * 512], 512); + + /* Drop 6-8. */ + recv_ack(5); + send_data(6, &contents[5 * 512], 512); + send_data(7, &contents[6 * 512], 512); + send_data(8, &contents[7 * 512], 512); + send_data(9, &contents[8 * 512], 512); + recv_ack(9); + + /* Drop 11. */ + send_data(10, &contents[9 * 512], 512); + send_data(12, &contents[11 * 512], 512); + + /* + * We can't send 13 here as tftpd has probably already seen 12 + * and sent the ACK of 10 if running locally. While it would + * recover by sending another ACK of 10, our state machine + * would be out of sync. + */ + + /* Ignore ACK for 10 and resend 10-13. */ + recv_ack(10); + send_data(10, &contents[9 * 512], 512); + send_data(11, &contents[10 * 512], 512); + send_data(12, &contents[11 * 512], 512); + send_data(13, &contents[12 * 512], 508); + recv_ack(13); + + fd = open("rfc7440.txt", O_RDONLY); + ATF_REQUIRE(fd >= 0); + r = read(fd, buffer, sizeof(buffer)); + ATF_REQUIRE(r > 0); + close(fd); + require_bufeq(contents, sizeof(contents), buffer, (size_t)r); +} + +/* + * Send less than four bytes + */ +TFTPD_TC_DEFINE(short_packet1, /* no head */, exitcode = 1) +{ + SEND_STR("\1"); +} +TFTPD_TC_DEFINE(short_packet2, /* no head */, exitcode = 1) +{ + SEND_STR("\1\2"); +} +TFTPD_TC_DEFINE(short_packet3, /* no head */, exitcode = 1) +{ + SEND_STR("\1\2\3"); +} + + +/* + * Main + */ + +ATF_TP_ADD_TCS(tp) +{ + TFTPD_TC_ADD(tp, abspath); + TFTPD_TC_ADD(tp, dotdot); + TFTPD_TC_ADD(tp, s_flag); + TFTPD_TC_ADD(tp, rrq_dropped_ack); + TFTPD_TC_ADD(tp, rrq_dropped_data); + TFTPD_TC_ADD(tp, rrq_duped_ack); + TFTPD_TC_ADD(tp, rrq_eaccess); + TFTPD_TC_ADD(tp, rrq_empty); + TFTPD_TC_ADD(tp, rrq_medium); + TFTPD_TC_ADD(tp, rrq_medium_window); + TFTPD_TC_ADD(tp, rrq_netascii); + TFTPD_TC_ADD(tp, rrq_nonexistent); + TFTPD_TC_ADD(tp, rrq_path_max); + TFTPD_TC_ADD(tp, rrq_small); + TFTPD_TC_ADD(tp, rrq_window_rfc7440); + TFTPD_TC_ADD(tp, unknown_modes); + TFTPD_TC_ADD(tp, unknown_opcode); + TFTPD_TC_ADD(tp, w_flag); + TFTPD_TC_ADD(tp, wrq_dropped_ack); + TFTPD_TC_ADD(tp, wrq_dropped_data); + TFTPD_TC_ADD(tp, wrq_duped_data); + TFTPD_TC_ADD(tp, wrq_eaccess); + TFTPD_TC_ADD(tp, wrq_eaccess_world_readable); + TFTPD_TC_ADD(tp, wrq_medium); + TFTPD_TC_ADD(tp, wrq_medium_window); + TFTPD_TC_ADD(tp, wrq_netascii); + TFTPD_TC_ADD(tp, wrq_nonexistent); + TFTPD_TC_ADD(tp, wrq_small); + TFTPD_TC_ADD(tp, wrq_truncate); + TFTPD_TC_ADD(tp, wrq_window_rfc7440); + TFTPD_TC_ADD(tp, short_packet1); + TFTPD_TC_ADD(tp, short_packet2); + TFTPD_TC_ADD(tp, short_packet3); + + return (atf_no_error()); +} diff --git a/libexec/tftpd/tftp-file.c b/libexec/tftpd/tftp-file.c new file mode 100644 index 000000000000..405d1b9018c9 --- /dev/null +++ b/libexec/tftpd/tftp-file.c @@ -0,0 +1,299 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <netinet/in.h> +#include <arpa/tftp.h> + +#include <assert.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#include "tftp-file.h" +#include "tftp-utils.h" + +static FILE *file; +static int convert; + +static char convbuffer[66000]; +static int gotcr = 0; + +static size_t +convert_from_net(char *buffer, size_t count) +{ + size_t i, n; + + /* + * Convert all CR/LF to LF and all CR,NUL to CR + */ + + n = 0; + for (i = 0; i < count; i++) { + + if (gotcr == 0) { + convbuffer[n++] = buffer[i]; + gotcr = (buffer[i] == '\r'); + continue; + } + + /* CR, NULL -> CR */ + if (buffer[i] == '\0') { + gotcr = 0; + continue; + } + + /* CR, LF -> LF */ + if (buffer[i] == '\n') { + if (n == 0) { + if (ftell(file) != 0) { + int r = fseek(file, -1, SEEK_END); + assert(r == 0); + convbuffer[n++] = '\n'; + } else { + /* This shouldn't happen */ + tftp_log(LOG_ERR, + "Received LF as first character"); + abort(); + } + } else + convbuffer[n-1] = '\n'; + gotcr = 0; + continue; + } + + /* Everything else just accept as is */ + convbuffer[n++] = buffer[i]; + gotcr = (buffer[i] == '\r'); + continue; + } + + return fwrite(convbuffer, 1, n, file); +} + +static size_t +convert_to_net(char *buffer, size_t count, int init) +{ + size_t i; + static size_t n = 0, in = 0; + static int newline = -1; + + if (init) { + newline = -1; + n = 0; + in = 0; + return 0 ; + } + + /* + * Convert all LF to CR,LF and all CR to CR,NUL + */ + i = 0; + + if (newline != -1) { + buffer[i++] = newline; + newline = -1; + } + + while (i < count) { + if (n == in) { + /* When done we're done */ + if (feof(file)) break; + + /* Otherwise read another bunch */ + in = fread(convbuffer, 1, count, file); + if (in == 0) break; + n = 0; + } + + /* CR -> CR,NULL */ + if (convbuffer[n] == '\r') { + buffer[i++] = '\r'; + buffer[i++] = '\0'; + n++; + continue; + } + + /* LF -> CR,LF */ + if (convbuffer[n] == '\n') { + buffer[i++] = '\r'; + buffer[i++] = '\n'; + n++; + continue; + } + + buffer[i++] = convbuffer[n++]; + } + + if (i > count) { + /* + * Whoops... that isn't allowed (but it will happen + * when there is a CR or LF at the end of the buffer) + */ + newline = buffer[i-1]; + } + + if (i < count) { + /* We are done! */ + return i; + } else + return count; + +} + +int +write_init(int fd, FILE *f, const char *mode) +{ + + if (f == NULL) { + file = fdopen(fd, "w"); + if (file == NULL) { + int en = errno; + tftp_log(LOG_ERR, "fdopen() failed: %s", + strerror(errno)); + return en; + } + } else + file = f; + convert = !strcmp(mode, "netascii"); + return 0; +} + +size_t +write_file(char *buffer, int count) +{ + + if (convert == 0) + return fwrite(buffer, 1, count, file); + + return convert_from_net(buffer, count); +} + +int +write_close(void) +{ + + if (fclose(file) != 0) { + tftp_log(LOG_ERR, "fclose() failed: %s", strerror(errno)); + return 1; + } + return 0; +} + +off_t +tell_file(void) +{ + + return ftello(file); +} + +int +seek_file(off_t offset) +{ + + return fseeko(file, offset, SEEK_SET); +} + +int +read_init(int fd, FILE *f, const char *mode) +{ + + convert_to_net(NULL, 0, 1); + if (f == NULL) { + file = fdopen(fd, "r"); + if (file == NULL) { + int en = errno; + tftp_log(LOG_ERR, "fdopen() failed: %s", + strerror(errno)); + return en; + } + } else + file = f; + convert = !strcmp(mode, "netascii"); + return 0; +} + +size_t +read_file(char *buffer, int count) +{ + + if (convert == 0) + return fread(buffer, 1, count, file); + + return convert_to_net(buffer, count, 0); +} + +int +read_close(void) +{ + + if (fclose(file) != 0) { + tftp_log(LOG_ERR, "fclose() failed: %s", strerror(errno)); + return 1; + } + return 0; +} + + +/* When an error has occurred, it is possible that the two sides + * are out of synch. Ie: that what I think is the other side's + * response to packet N is really their response to packet N-1. + * + * So, to try to prevent that, we flush all the input queued up + * for us on the network connection on our host. + * + * We return the number of packets we flushed (mostly for reporting + * when trace is active). + */ + +int +synchnet(int peer) /* socket to flush */ +{ + int i, j = 0; + char rbuf[MAXPKTSIZE]; + struct sockaddr_storage from; + socklen_t fromlen; + + while (1) { + (void) ioctl(peer, FIONREAD, &i); + if (i) { + j++; + fromlen = sizeof from; + (void) recvfrom(peer, rbuf, sizeof (rbuf), 0, + (struct sockaddr *)&from, &fromlen); + } else { + return(j); + } + } +} diff --git a/libexec/tftpd/tftp-file.h b/libexec/tftpd/tftp-file.h new file mode 100644 index 000000000000..c424e5cbc75b --- /dev/null +++ b/libexec/tftpd/tftp-file.h @@ -0,0 +1,39 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +int write_init(int fd, FILE *f, const char *mode); +size_t write_file(char *buffer, int count); +int write_close(void); + +int read_init(int fd, FILE *f, const char *mode); +size_t read_file(char *buffer, int count); +int read_close(void); + +int seek_file(off_t offset); +off_t tell_file(void); + +int synchnet(int peer); diff --git a/libexec/tftpd/tftp-io.c b/libexec/tftpd/tftp-io.c new file mode 100644 index 000000000000..50102e652d2f --- /dev/null +++ b/libexec/tftpd/tftp-io.c @@ -0,0 +1,446 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <arpa/tftp.h> + +#include <assert.h> +#include <errno.h> +#include <poll.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#include "tftp-file.h" +#include "tftp-io.h" +#include "tftp-utils.h" +#include "tftp-options.h" + +struct sockaddr_storage peer_sock; +struct sockaddr_storage me_sock; + +static int send_packet(int peer, uint16_t block, char *pkt, int size); + +static struct errmsg { + int e_code; + const 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, NULL } +}; + +#define DROPPACKET(s) \ + if (packetdroppercentage != 0 && \ + arc4random()%100 < packetdroppercentage) { \ + tftp_log(LOG_DEBUG, "Artificial packet drop in %s", s); \ + return; \ + } +#define DROPPACKETn(s,n) \ + if (packetdroppercentage != 0 && \ + arc4random()%100 < packetdroppercentage) { \ + tftp_log(LOG_DEBUG, "Artificial packet drop in %s", s); \ + return (n); \ + } + +const char * +errtomsg(int error) +{ + static char ebuf[40]; + 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(ebuf, sizeof(ebuf), "error %d", error); + return (ebuf); +} + +static int +send_packet(int peer, uint16_t block, char *pkt, int size) +{ + int i; + int t = 1; + + for (i = 0; i < 12 ; i++) { + DROPPACKETn("send_packet", 0); + + if (sendto(peer, pkt, size, 0, (struct sockaddr *)&peer_sock, + peer_sock.ss_len) == size) { + if (i) + tftp_log(LOG_ERR, + "%s block %d, attempt %d successful", + packettype(ntohs(((struct tftphdr *) + (pkt))->th_opcode)), block, i); + return (0); + } + tftp_log(LOG_ERR, + "%s block %d, attempt %d failed (Error %d: %s)", + packettype(ntohs(((struct tftphdr *)(pkt))->th_opcode)), + block, i, errno, strerror(errno)); + sleep(t); + if (t < 32) + t <<= 1; + } + tftp_log(LOG_ERR, "send_packet: %s", strerror(errno)); + return (1); +} + +/* + * Send an ERROR packet (error message). + * Error code passed in is one of the + * standard TFTP codes, or a UNIX errno + * offset by 100. + */ +void +send_error(int peer, int error) +{ + struct tftphdr *tp; + int length; + struct errmsg *pe; + char buf[MAXPKTSIZE]; + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Sending ERROR %d", error); + + DROPPACKET("send_error"); + + 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 */ + } + snprintf(tp->th_msg, MAXPKTSIZE - 4, "%s%n", pe->e_msg, &length); + length += 5; /* header and terminator */ + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Sending ERROR %d: %s", error, tp->th_msg); + + if (sendto(peer, buf, length, 0, + (struct sockaddr *)&peer_sock, peer_sock.ss_len) != length) + tftp_log(LOG_ERR, "send_error: %s", strerror(errno)); +} + +/* + * Send an WRQ packet (write request). + */ +int +send_wrq(int peer, char *filename, char *mode) +{ + int n; + struct tftphdr *tp; + char *bp; + char buf[MAXPKTSIZE]; + int size; + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Sending WRQ: filename: '%s', mode '%s'", + filename, mode + ); + + DROPPACKETn("send_wrq", 0); + + tp = (struct tftphdr *)buf; + tp->th_opcode = htons((u_short)WRQ); + size = offsetof(struct tftphdr, th_stuff); + + bp = tp->th_stuff; + strlcpy(bp, filename, sizeof(buf) - size); + bp += strlen(filename); + *bp = 0; + bp++; + size += strlen(filename) + 1; + + strlcpy(bp, mode, sizeof(buf) - size); + bp += strlen(mode); + *bp = 0; + bp++; + size += strlen(mode) + 1; + + if (options_rfc_enabled) + size += make_options(peer, bp, sizeof(buf) - size); + + n = sendto(peer, buf, size, 0, + (struct sockaddr *)&peer_sock, peer_sock.ss_len); + if (n != size) { + tftp_log(LOG_ERR, "send_wrq: %s", strerror(errno)); + return (1); + } + return (0); +} + +/* + * Send an RRQ packet (write request). + */ +int +send_rrq(int peer, char *filename, char *mode) +{ + int n; + struct tftphdr *tp; + char *bp; + char buf[MAXPKTSIZE]; + int size; + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Sending RRQ: filename: '%s', mode '%s'", + filename, mode + ); + + DROPPACKETn("send_rrq", 0); + + tp = (struct tftphdr *)buf; + tp->th_opcode = htons((u_short)RRQ); + size = offsetof(struct tftphdr, th_stuff); + + bp = tp->th_stuff; + strlcpy(bp, filename, sizeof(buf) - size); + bp += strlen(filename); + *bp = 0; + bp++; + size += strlen(filename) + 1; + + strlcpy(bp, mode, sizeof(buf) - size); + bp += strlen(mode); + *bp = 0; + bp++; + size += strlen(mode) + 1; + + if (options_rfc_enabled) { + options_set_request(OPT_TSIZE, "0"); + size += make_options(peer, bp, sizeof(buf) - size); + } + + n = sendto(peer, buf, size, 0, + (struct sockaddr *)&peer_sock, peer_sock.ss_len); + if (n != size) { + tftp_log(LOG_ERR, "send_rrq: %d %s", n, strerror(errno)); + return (1); + } + return (0); +} + +/* + * Send an OACK packet (option acknowledgement). + */ +int +send_oack(int peer) +{ + struct tftphdr *tp; + int size, i, n; + char *bp; + char buf[MAXPKTSIZE]; + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Sending OACK"); + + DROPPACKETn("send_oack", 0); + + /* + * Send back an options acknowledgement (only the ones with + * a reply for) + */ + 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_reply != NULL) { + n = snprintf(bp, size, "%s%c%s", options[i].o_type, + 0, options[i].o_reply); + bp += n+1; + size -= n+1; + if (size < 0) { + tftp_log(LOG_ERR, "oack: buffer overflow"); + exit(1); + } + } + } + size = bp - buf; + + if (sendto(peer, buf, size, 0, + (struct sockaddr *)&peer_sock, peer_sock.ss_len) != size) { + tftp_log(LOG_INFO, "send_oack: %s", strerror(errno)); + return (1); + } + + return (0); +} + +/* + * Send an ACK packet (acknowledgement). + */ +int +send_ack(int fp, uint16_t block) +{ + struct tftphdr *tp; + int size; + char buf[MAXPKTSIZE]; + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Sending ACK for block %d", block); + + DROPPACKETn("send_ack", 0); + + tp = (struct tftphdr *)buf; + tp->th_opcode = htons((u_short)ACK); + tp->th_block = htons((u_short)block); + size = 4; + + if (sendto(fp, buf, size, 0, + (struct sockaddr *)&peer_sock, peer_sock.ss_len) != size) { + tftp_log(LOG_INFO, "send_ack: %s", strerror(errno)); + return (1); + } + + return (0); +} + +/* + * Send a DATA packet + */ +int +send_data(int peer, uint16_t block, char *data, int size) +{ + char buf[MAXPKTSIZE]; + struct tftphdr *pkt; + int n; + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Sending DATA packet %d of %d bytes", + block, size); + + DROPPACKETn("send_data", 0); + + pkt = (struct tftphdr *)buf; + + pkt->th_opcode = htons((u_short)DATA); + pkt->th_block = htons((u_short)block); + memcpy(pkt->th_data, data, size); + + n = send_packet(peer, block, (char *)pkt, size + 4); + return (n); +} + + +/* + * Receive a packet + * + * If timeout is negative, no error will be logged on timeout. + */ +int +receive_packet(int peer, char *data, int size, struct sockaddr_storage *from, + int timeout) +{ + struct pollfd pfd; + struct tftphdr *pkt; + struct sockaddr_storage from_local; + struct sockaddr_storage *pfrom; + socklen_t fromlen; + int n; + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, + "Waiting %d seconds for packet", timeoutpacket); + + pkt = (struct tftphdr *)data; + + pfd.fd = peer; + pfd.events = POLLIN; + if (poll(&pfd, 1, 1000 * (timeout < 0 ? -timeout : timeout)) < 1) { + if (timeout > 0) + tftp_log(LOG_ERR, "receive_packet: timeout"); + return (RP_TIMEOUT); + } + + pfrom = (from == NULL) ? &from_local : from; + fromlen = sizeof(*pfrom); + n = recvfrom(peer, data, size, 0, (struct sockaddr *)pfrom, &fromlen); + + DROPPACKETn("receive_packet", RP_TIMEOUT); + + if (n < 0) { + /* No idea what could have happened if it isn't a timeout */ + tftp_log(LOG_ERR, "receive_packet: %s", strerror(errno)); + return (RP_RECVFROM); + } + if (n < 4) { + tftp_log(LOG_ERR, + "receive_packet: packet too small (%d bytes)", n); + return (RP_TOOSMALL); + } + + pkt->th_opcode = ntohs((u_short)pkt->th_opcode); + if (pkt->th_opcode == DATA || + pkt->th_opcode == ACK) + pkt->th_block = ntohs((u_short)pkt->th_block); + + if (pkt->th_opcode == DATA && n > pktsize) { + tftp_log(LOG_ERR, "receive_packet: packet too big"); + return (RP_TOOBIG); + } + + if (((struct sockaddr_in *)(pfrom))->sin_addr.s_addr != + ((struct sockaddr_in *)(&peer_sock))->sin_addr.s_addr) { + tftp_log(LOG_ERR, + "receive_packet: received packet from wrong source"); + return (RP_WRONGSOURCE); + } + + if (pkt->th_opcode == ERROR) { + tftp_log(pkt->th_code == EUNDEF ? LOG_DEBUG : LOG_ERR, + "Got ERROR packet: %s", pkt->th_msg); + return (RP_ERROR); + } + + if (debug & DEBUG_PACKETS) + tftp_log(LOG_DEBUG, "Received %d bytes in a %s packet", + n, packettype(pkt->th_opcode)); + + return n - 4; +} diff --git a/libexec/tftpd/tftp-io.h b/libexec/tftpd/tftp-io.h new file mode 100644 index 000000000000..1d6bc2bd8b5e --- /dev/null +++ b/libexec/tftpd/tftp-io.h @@ -0,0 +1,46 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +#define RP_NONE 0 +#define RP_RECVFROM -1 +#define RP_TOOSMALL -2 +#define RP_ERROR -3 +#define RP_WRONGSOURCE -4 +#define RP_TIMEOUT -5 +#define RP_TOOBIG -6 + +const char *errtomsg(int); +void send_error(int peer, int); +int send_wrq(int peer, char *, char *); +int send_rrq(int peer, char *, char *); +int send_oack(int peer); +int send_ack(int peer, unsigned short); +int send_data(int peer, uint16_t, char *, int); +int receive_packet(int peer, char *, int, struct sockaddr_storage *, int); + +extern struct sockaddr_storage peer_sock; +extern struct sockaddr_storage me_sock; diff --git a/libexec/tftpd/tftp-options.c b/libexec/tftpd/tftp-options.c new file mode 100644 index 000000000000..7a261ac3d7c3 --- /dev/null +++ b/libexec/tftpd/tftp-options.c @@ -0,0 +1,477 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysctl.h> + +#include <netinet/in.h> +#include <arpa/tftp.h> + +#include <ctype.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> + +#include "tftp-utils.h" +#include "tftp-io.h" +#include "tftp-options.h" + +/* + * Option handlers + */ + +struct options options[] = { + { "tsize", NULL, NULL, NULL /* option_tsize */, 1 }, + { "timeout", NULL, NULL, option_timeout, 1 }, + { "blksize", NULL, NULL, option_blksize, 1 }, + { "blksize2", NULL, NULL, option_blksize2, 0 }, + { "rollover", NULL, NULL, option_rollover, 0 }, + { "windowsize", NULL, NULL, option_windowsize, 1 }, + { NULL, NULL, NULL, NULL, 0 } +}; + +/* By default allow them */ +int options_rfc_enabled = 1; +int options_extra_enabled = 1; + +int +options_set_request(enum opt_enum opt, const char *fmt, ...) +{ + va_list ap; + char *str; + int ret; + + if (fmt == NULL) { + str = NULL; + } else { + va_start(ap, fmt); + ret = vasprintf(&str, fmt, ap); + va_end(ap); + if (ret < 0) + return (ret); + } + if (options[opt].o_request != NULL && + options[opt].o_request != options[opt].o_reply) + free(options[opt].o_request); + options[opt].o_request = str; + return (0); +} + +int +options_set_reply(enum opt_enum opt, const char *fmt, ...) +{ + va_list ap; + char *str; + int ret; + + if (fmt == NULL) { + str = NULL; + } else { + va_start(ap, fmt); + ret = vasprintf(&str, fmt, ap); + va_end(ap); + if (ret < 0) + return (ret); + } + if (options[opt].o_reply != NULL && + options[opt].o_reply != options[opt].o_request) + free(options[opt].o_reply); + options[opt].o_reply = str; + return (0); +} + +static void +options_set_reply_equal_request(enum opt_enum opt) +{ + + if (options[opt].o_reply != NULL && + options[opt].o_reply != options[opt].o_request) + free(options[opt].o_reply); + options[opt].o_reply = options[opt].o_request; +} + +/* + * Rules for the option handlers: + * - If there is no o_request, there will be no processing. + * + * For servers + * - Logging is done as warnings. + * - The handler exit()s if there is a serious problem with the + * values submitted in the option. + * + * For clients + * - Logging is done as errors. After all, the server shouldn't + * return rubbish. + * - The handler returns if there is a serious problem with the + * values submitted in the option. + * - Sending the EBADOP packets is done by the handler. + */ + +int +option_tsize(int peer __unused, struct tftphdr *tp __unused, int mode, + struct stat *stbuf) +{ + + if (options[OPT_TSIZE].o_request == NULL) + return (0); + + if (mode == RRQ) + options_set_reply(OPT_TSIZE, "%ju", (uintmax_t)stbuf->st_size); + else + /* XXX Allows writes of all sizes. */ + options_set_reply_equal_request(OPT_TSIZE); + return (0); +} + +int +option_timeout(int peer) +{ + int to; + + if (options[OPT_TIMEOUT].o_request == NULL) + return (0); + + to = atoi(options[OPT_TIMEOUT].o_request); + if (to < TIMEOUT_MIN || to > TIMEOUT_MAX) { + tftp_log(acting_as_client ? LOG_ERR : LOG_WARNING, + "Received bad value for timeout. " + "Should be between %d and %d, received %d", + TIMEOUT_MIN, TIMEOUT_MAX, to); + send_error(peer, EBADOP); + if (acting_as_client) + return (1); + exit(1); + } else { + timeoutpacket = to; + options_set_reply_equal_request(OPT_TIMEOUT); + } + settimeouts(timeoutpacket, timeoutnetwork, maxtimeouts); + + if (debug & DEBUG_OPTIONS) + tftp_log(LOG_DEBUG, "Setting timeout to '%s'", + options[OPT_TIMEOUT].o_reply); + + return (0); +} + +int +option_rollover(int peer) +{ + + if (options[OPT_ROLLOVER].o_request == NULL) + return (0); + + if (strcmp(options[OPT_ROLLOVER].o_request, "0") != 0 + && strcmp(options[OPT_ROLLOVER].o_request, "1") != 0) { + tftp_log(acting_as_client ? LOG_ERR : LOG_WARNING, + "Bad value for rollover, " + "should be either 0 or 1, received '%s', " + "ignoring request", + options[OPT_ROLLOVER].o_request); + if (acting_as_client) { + send_error(peer, EBADOP); + return (1); + } + return (0); + } + options_set_reply_equal_request(OPT_ROLLOVER); + + if (debug & DEBUG_OPTIONS) + tftp_log(LOG_DEBUG, "Setting rollover to '%s'", + options[OPT_ROLLOVER].o_reply); + + return (0); +} + +int +option_blksize(int peer) +{ + u_long maxdgram; + size_t len; + + if (options[OPT_BLKSIZE].o_request == NULL) + return (0); + + /* maximum size of an UDP packet according to the system */ + len = sizeof(maxdgram); + if (sysctlbyname("net.inet.udp.maxdgram", + &maxdgram, &len, NULL, 0) < 0) { + tftp_log(LOG_ERR, "sysctl: net.inet.udp.maxdgram"); + return (acting_as_client ? 1 : 0); + } + maxdgram -= 4; /* leave room for header */ + + int size = atoi(options[OPT_BLKSIZE].o_request); + if (size < BLKSIZE_MIN || size > BLKSIZE_MAX) { + if (acting_as_client) { + tftp_log(LOG_ERR, + "Invalid blocksize (%d bytes), aborting", + size); + send_error(peer, EBADOP); + return (1); + } else { + tftp_log(LOG_WARNING, + "Invalid blocksize (%d bytes), ignoring request", + size); + return (0); + } + } + + if (size > (int)maxdgram) { + if (acting_as_client) { + tftp_log(LOG_ERR, + "Invalid blocksize (%d bytes), " + "net.inet.udp.maxdgram sysctl limits it to " + "%ld bytes.\n", size, maxdgram); + send_error(peer, EBADOP); + return (1); + } else { + tftp_log(LOG_WARNING, + "Invalid blocksize (%d bytes), " + "net.inet.udp.maxdgram sysctl limits it to " + "%ld bytes.\n", size, maxdgram); + size = maxdgram; + /* No reason to return */ + } + } + + options_set_reply(OPT_BLKSIZE, "%d", size); + segsize = size; + pktsize = size + 4; + if (debug & DEBUG_OPTIONS) + tftp_log(LOG_DEBUG, "Setting blksize to '%s'", + options[OPT_BLKSIZE].o_reply); + + return (0); +} + +int +option_blksize2(int peer __unused) +{ + u_long maxdgram; + int size, i; + size_t len; + + int sizes[] = { + 8, 16, 32, 64, 128, 256, 512, 1024, + 2048, 4096, 8192, 16384, 32768, 0 + }; + + if (options[OPT_BLKSIZE2].o_request == NULL) + return (0); + + /* maximum size of an UDP packet according to the system */ + len = sizeof(maxdgram); + if (sysctlbyname("net.inet.udp.maxdgram", + &maxdgram, &len, NULL, 0) < 0) { + tftp_log(LOG_ERR, "sysctl: net.inet.udp.maxdgram"); + return (acting_as_client ? 1 : 0); + } + + size = atoi(options[OPT_BLKSIZE2].o_request); + for (i = 0; sizes[i] != 0; i++) { + if (size == sizes[i]) break; + } + if (sizes[i] == 0) { + tftp_log(LOG_INFO, + "Invalid blocksize2 (%d bytes), ignoring request", size); + return (acting_as_client ? 1 : 0); + } + + if (size > (int)maxdgram) { + for (i = 0; sizes[i+1] != 0; i++) { + if ((int)maxdgram < sizes[i+1]) break; + } + tftp_log(LOG_INFO, + "Invalid blocksize2 (%d bytes), net.inet.udp.maxdgram " + "sysctl limits it to %ld bytes.\n", size, maxdgram); + size = sizes[i]; + /* No need to return */ + } + + options_set_reply(OPT_BLKSIZE2, "%d", size); + segsize = size; + pktsize = size + 4; + if (debug & DEBUG_OPTIONS) + tftp_log(LOG_DEBUG, "Setting blksize2 to '%s'", + options[OPT_BLKSIZE2].o_reply); + + return (0); +} + +int +option_windowsize(int peer) +{ + int size; + + if (options[OPT_WINDOWSIZE].o_request == NULL) + return (0); + + size = atoi(options[OPT_WINDOWSIZE].o_request); + if (size < WINDOWSIZE_MIN || size > WINDOWSIZE_MAX) { + if (acting_as_client) { + tftp_log(LOG_ERR, + "Invalid windowsize (%d blocks), aborting", + size); + send_error(peer, EBADOP); + return (1); + } else { + tftp_log(LOG_WARNING, + "Invalid windowsize (%d blocks), ignoring request", + size); + return (0); + } + } + + /* XXX: Should force a windowsize of 1 for non-seekable files. */ + options_set_reply(OPT_WINDOWSIZE, "%d", size); + windowsize = size; + + if (debug & DEBUG_OPTIONS) + tftp_log(LOG_DEBUG, "Setting windowsize to '%s'", + options[OPT_WINDOWSIZE].o_reply); + + return (0); +} + +/* + * Append the available options to the header + */ +uint16_t +make_options(int peer __unused, char *buffer, uint16_t size) { + int i; + char *value; + const char *option; + uint16_t length; + uint16_t returnsize = 0; + + if (!options_rfc_enabled) return (0); + + for (i = 0; options[i].o_type != NULL; i++) { + if (options[i].rfc == 0 && !options_extra_enabled) + continue; + + option = options[i].o_type; + if (acting_as_client) + value = options[i].o_request; + else + value = options[i].o_reply; + if (value == NULL) + continue; + + length = strlen(value) + strlen(option) + 2; + if (size <= length) { + tftp_log(LOG_ERR, + "Running out of option space for " + "option '%s' with value '%s': " + "needed %d bytes, got %d bytes", + option, value, size, length); + continue; + } + + sprintf(buffer, "%s%c%s%c", option, '\000', value, '\000'); + size -= length; + buffer += length; + returnsize += length; + } + + return (returnsize); +} + +/* + * Parse the received options in the header + */ +int +parse_options(int peer, char *buffer, uint16_t size) +{ + int i, options_failed; + char *c, *cp, *option, *value; + + if (!options_rfc_enabled) return (0); + + /* Parse the options */ + cp = buffer; + options_failed = 0; + while (size > 0) { + option = cp; + i = get_field(peer, cp, size); + cp += i; + + value = cp; + i = get_field(peer, cp, size); + cp += i; + + /* We are at the end */ + if (*option == '\0') break; + + if (debug & DEBUG_OPTIONS) + tftp_log(LOG_DEBUG, + "option: '%s' value: '%s'", option, value); + + for (c = option; *c; c++) + if (isupper(*c)) + *c = tolower(*c); + for (i = 0; options[i].o_type != NULL; i++) { + if (strcmp(option, options[i].o_type) == 0) { + if (!acting_as_client) + options_set_request(i, "%s", value); + if (!options_extra_enabled && !options[i].rfc) { + tftp_log(LOG_INFO, + "Option '%s' with value '%s' found " + "but it is not an RFC option", + option, value); + continue; + } + if (options[i].o_handler) + options_failed += + (options[i].o_handler)(peer); + break; + } + } + if (options[i].o_type == NULL) + tftp_log(LOG_WARNING, + "Unknown option: '%s'", option); + + size -= strlen(option) + strlen(value) + 2; + } + + return (options_failed); +} + +/* + * Set some default values in the options + */ +void +init_options(void) +{ + + options_set_request(OPT_ROLLOVER, "0"); +} diff --git a/libexec/tftpd/tftp-options.h b/libexec/tftpd/tftp-options.h new file mode 100644 index 000000000000..f1b0a5cfaf32 --- /dev/null +++ b/libexec/tftpd/tftp-options.h @@ -0,0 +1,68 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +/* + * Options + */ + +void init_options(void); +uint16_t make_options(int peer, char *buffer, uint16_t size); +int parse_options(int peer, char *buffer, uint16_t size); + +/* Call back functions */ +int option_tsize(int peer, struct tftphdr *, int, struct stat *); +int option_timeout(int peer); +int option_blksize(int peer); +int option_blksize2(int peer); +int option_rollover(int peer); +int option_windowsize(int peer); + +extern int options_extra_enabled; +extern int options_rfc_enabled; + +struct options { + const char *o_type; + char *o_request; + char *o_reply; + int (*o_handler)(int peer); + int rfc; +}; + +extern struct options options[]; +enum opt_enum { + OPT_TSIZE = 0, + OPT_TIMEOUT, + OPT_BLKSIZE, + OPT_BLKSIZE2, + OPT_ROLLOVER, + OPT_WINDOWSIZE, +}; + +int options_set_request(enum opt_enum, const char *, ...) + __printf0like(2, 3); +int options_set_reply(enum opt_enum, const char *, ...) + __printf0like(2, 3); diff --git a/libexec/tftpd/tftp-transfer.c b/libexec/tftpd/tftp-transfer.c new file mode 100644 index 000000000000..ea386f8a3c1a --- /dev/null +++ b/libexec/tftpd/tftp-transfer.c @@ -0,0 +1,446 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +#include <sys/param.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> + +#include <netinet/in.h> +#include <arpa/tftp.h> + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> + +#include "tftp-file.h" +#include "tftp-io.h" +#include "tftp-utils.h" +#include "tftp-options.h" +#include "tftp-transfer.h" + +struct block_data { + off_t offset; + uint16_t block; + int size; +}; + +/* + * Send a file via the TFTP data session. + */ +int +tftp_send(int peer, uint16_t *block, struct tftp_stats *ts) +{ + struct tftphdr *rp; + int size, n_data, n_ack, sendtry, acktry; + u_int i, j; + uint16_t oldblock, windowblock; + char sendbuffer[MAXPKTSIZE]; + char recvbuffer[MAXPKTSIZE]; + struct block_data window[WINDOWSIZE_MAX]; + + rp = (struct tftphdr *)recvbuffer; + *block = 1; + ts->amount = 0; + windowblock = 0; + acktry = 0; + do { +read_block: + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, "Sending block %d (window block %d)", + *block, windowblock); + + window[windowblock].offset = tell_file(); + window[windowblock].block = *block; + size = read_file(sendbuffer, segsize); + if (size < 0) { + tftp_log(LOG_ERR, "read_file returned %d", size); + send_error(peer, errno + 100); + return -1; + } + window[windowblock].size = size; + windowblock++; + + for (sendtry = 0; ; sendtry++) { + n_data = send_data(peer, *block, sendbuffer, size); + if (n_data == 0) + break; + + if (sendtry == maxtimeouts) { + tftp_log(LOG_ERR, + "Cannot send DATA packet #%d, " + "giving up", *block); + return -1; + } + tftp_log(LOG_ERR, + "Cannot send DATA packet #%d, trying again", + *block); + } + + /* Only check for ACK for last block in window. */ + if (windowblock == windowsize || size != segsize) { + n_ack = receive_packet(peer, recvbuffer, + MAXPKTSIZE, NULL, timeoutpacket); + if (n_ack < 0) { + if (n_ack == RP_TIMEOUT) { + if (acktry == maxtimeouts) { + tftp_log(LOG_ERR, + "Timeout #%d send ACK %d " + "giving up", acktry, *block); + return -1; + } + tftp_log(LOG_WARNING, + "Timeout #%d on ACK %d", + acktry, *block); + + acktry++; + ts->retries++; + if (seek_file(window[0].offset) != 0) { + tftp_log(LOG_ERR, + "seek_file failed: %s", + strerror(errno)); + send_error(peer, errno + 100); + return -1; + } + *block = window[0].block; + windowblock = 0; + goto read_block; + } + + /* Either read failure or ERROR packet */ + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_ERR, "Aborting: %s", + rp_strerror(n_ack)); + return -1; + } + if (rp->th_opcode == ACK) { + /* + * Look for the ACKed block in our open + * window. + */ + for (i = 0; i < windowblock; i++) { + if (rp->th_block == window[i].block) + break; + } + + if (i == windowblock) { + /* Did not recognize ACK. */ + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, + "ACK %d out of window", + rp->th_block); + + /* Re-synchronize with the other side */ + (void) synchnet(peer); + + /* Resend the current window. */ + ts->retries++; + if (seek_file(window[0].offset) != 0) { + tftp_log(LOG_ERR, + "seek_file failed: %s", + strerror(errno)); + send_error(peer, errno + 100); + return -1; + } + *block = window[0].block; + windowblock = 0; + goto read_block; + } + + /* ACKed at least some data. */ + acktry = 0; + for (j = 0; j <= i; j++) { + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, + "ACKed block %d", + window[j].block); + ts->blocks++; + ts->amount += window[j].size; + } + + /* + * Partial ACK. Rewind state to first + * un-ACKed block. + */ + if (i + 1 != windowblock) { + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, + "Partial ACK"); + if (seek_file(window[i + 1].offset) != + 0) { + tftp_log(LOG_ERR, + "seek_file failed: %s", + strerror(errno)); + send_error(peer, errno + 100); + return -1; + } + *block = window[i + 1].block; + windowblock = 0; + ts->retries++; + goto read_block; + } + + windowblock = 0; + } + + } + oldblock = *block; + (*block)++; + if (oldblock > *block) { + if (options[OPT_ROLLOVER].o_request == NULL) { + /* + * "rollover" option not specified in + * tftp client. Default to rolling block + * counter to 0. + */ + *block = 0; + } else { + *block = atoi(options[OPT_ROLLOVER].o_request); + } + + ts->rollovers++; + } + gettimeofday(&(ts->tstop), NULL); + } while (size == segsize); + return 0; +} + +/* + * Receive a file via the TFTP data session. + * + * - It could be that the first block has already arrived while + * trying to figure out if we were receiving options or not. In + * that case it is passed to this function. + */ +int +tftp_receive(int peer, uint16_t *block, struct tftp_stats *ts, + struct tftphdr *firstblock, size_t fb_size) +{ + struct tftphdr *rp; + uint16_t oldblock, windowstart; + int n_data, n_ack, writesize, i, retry, windowblock; + char recvbuffer[MAXPKTSIZE]; + + ts->amount = 0; + windowblock = 0; + + if (firstblock != NULL) { + writesize = write_file(firstblock->th_data, fb_size); + ts->amount += writesize; + ts->blocks++; + windowblock++; + if (windowsize == 1 || fb_size != segsize) { + for (i = 0; ; i++) { + n_ack = send_ack(peer, *block); + if (n_ack > 0) { + if (i == maxtimeouts) { + tftp_log(LOG_ERR, + "Cannot send ACK packet #%d, " + "giving up", *block); + return -1; + } + tftp_log(LOG_ERR, + "Cannot send ACK packet #%d, trying again", + *block); + continue; + } + + break; + } + } + + if (fb_size != segsize) { + write_close(); + gettimeofday(&(ts->tstop), NULL); + return 0; + } + } + + rp = (struct tftphdr *)recvbuffer; + do { + oldblock = *block; + (*block)++; + if (oldblock > *block) { + if (options[OPT_ROLLOVER].o_request == NULL) { + /* + * "rollover" option not specified in + * tftp client. Default to rolling block + * counter to 0. + */ + *block = 0; + } else { + *block = atoi(options[OPT_ROLLOVER].o_request); + } + + ts->rollovers++; + } + + for (retry = 0; ; retry++) { + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, + "Receiving DATA block %d (window block %d)", + *block, windowblock); + + n_data = receive_packet(peer, recvbuffer, + MAXPKTSIZE, NULL, timeoutpacket); + if (n_data < 0) { + if (retry == maxtimeouts) { + tftp_log(LOG_ERR, + "Timeout #%d on DATA block %d, " + "giving up", retry, *block); + return -1; + } + if (n_data == RP_TIMEOUT) { + tftp_log(LOG_WARNING, + "Timeout #%d on DATA block %d", + retry, *block); + send_ack(peer, oldblock); + windowblock = 0; + continue; + } + + /* Either read failure or ERROR packet */ + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, "Aborting: %s", + rp_strerror(n_data)); + return -1; + } + if (rp->th_opcode == DATA) { + ts->blocks++; + + if (rp->th_block == *block) + break; + + /* + * Ignore duplicate blocks within the + * window. + * + * This does not handle duplicate + * blocks during a rollover as + * gracefully, but that should still + * recover eventually. + */ + if (*block > windowsize) + windowstart = *block - windowsize; + else + windowstart = 0; + if (rp->th_block > windowstart && + rp->th_block < *block) { + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, + "Ignoring duplicate DATA block %d", + rp->th_block); + windowblock++; + retry = 0; + continue; + } + + tftp_log(LOG_WARNING, + "Expected DATA block %d, got block %d", + *block, rp->th_block); + + /* Re-synchronize with the other side */ + (void) synchnet(peer); + + tftp_log(LOG_INFO, "Trying to sync"); + *block = oldblock; + ts->retries++; + goto send_ack; /* rexmit */ + + } else { + tftp_log(LOG_WARNING, + "Expected DATA block, got %s block", + packettype(rp->th_opcode)); + } + } + + if (n_data > 0) { + writesize = write_file(rp->th_data, n_data); + ts->amount += writesize; + if (writesize <= 0) { + tftp_log(LOG_ERR, + "write_file returned %d", writesize); + if (writesize < 0) + send_error(peer, errno + 100); + else + send_error(peer, ENOSPACE); + return -1; + } + } + if (n_data != segsize) + write_close(); + windowblock++; + + /* Only send ACKs for the last block in the window. */ + if (windowblock < windowsize && n_data == segsize) + continue; +send_ack: + for (i = 0; ; i++) { + n_ack = send_ack(peer, *block); + if (n_ack > 0) { + + if (i == maxtimeouts) { + tftp_log(LOG_ERR, + "Cannot send ACK packet #%d, " + "giving up", *block); + return -1; + } + + tftp_log(LOG_ERR, + "Cannot send ACK packet #%d, trying again", + *block); + continue; + } + + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, "Sent ACK for %d", *block); + windowblock = 0; + break; + } + gettimeofday(&(ts->tstop), NULL); + } while (n_data == segsize); + + /* Don't do late packet management for the client implementation */ + if (acting_as_client) + return 0; + + for (i = 0; ; i++) { + n_data = receive_packet(peer, (char *)rp, pktsize, + NULL, -timeoutpacket); + if (n_data <= 0) + break; + if (n_data > 0 && + rp->th_opcode == DATA && /* and got a data block */ + *block == rp->th_block) /* then my last ack was lost */ + send_ack(peer, *block); /* resend final ack */ + } + + return 0; +} diff --git a/libexec/tftpd/tftp-transfer.h b/libexec/tftpd/tftp-transfer.h new file mode 100644 index 000000000000..449f29c246e0 --- /dev/null +++ b/libexec/tftpd/tftp-transfer.h @@ -0,0 +1,30 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +int tftp_send(int peer, uint16_t *block, struct tftp_stats *tp); +int tftp_receive(int peer, uint16_t *block, struct tftp_stats *tp, + struct tftphdr *firstblock, size_t fb_size); diff --git a/libexec/tftpd/tftp-utils.c b/libexec/tftpd/tftp-utils.c new file mode 100644 index 000000000000..8ce7c09c9992 --- /dev/null +++ b/libexec/tftpd/tftp-utils.c @@ -0,0 +1,328 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +#include <sys/cdefs.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> + +#include <netinet/in.h> +#include <arpa/tftp.h> + +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> + +#include "tftp-utils.h" +#include "tftp-io.h" + +/* + * Default values, can be changed later via the TFTP Options + */ +int timeoutpacket = TIMEOUT; +int timeoutnetwork = MAX_TIMEOUTS * TIMEOUT; +int maxtimeouts = MAX_TIMEOUTS; +uint16_t segsize = SEGSIZE; +uint16_t pktsize = SEGSIZE + 4; +uint16_t windowsize = WINDOWSIZE; + +int acting_as_client; + + +/* + * Set timeout values for packet reception. The idea is that you + * get 'maxtimeouts' of 5 seconds between 'timeoutpacket' (i.e. the + * first timeout) to 'timeoutnetwork' (i.e. the last timeout) + */ +int +settimeouts(int _timeoutpacket, int _timeoutnetwork, int _maxtimeouts __unused) +{ + int i; + + /* We cannot do impossible things */ + if (_timeoutpacket >= _timeoutnetwork) + return (0); + + maxtimeouts = 0; + i = _timeoutpacket; + while (i < _timeoutnetwork || maxtimeouts < MIN_TIMEOUTS) { + maxtimeouts++; + i += 5; + } + + timeoutpacket = _timeoutpacket; + timeoutnetwork = i; + return (1); +} + +/* translate IPv4 mapped IPv6 address to IPv4 address */ +void +unmappedaddr(struct sockaddr_in6 *sin6) +{ + struct sockaddr_in *sin4; + u_int32_t addr; + int port; + + if (sin6->sin6_family != AF_INET6 || + !IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) + return; + sin4 = (struct sockaddr_in *)sin6; + memcpy(&addr, &sin6->sin6_addr.s6_addr[12], sizeof(addr)); + port = sin6->sin6_port; + memset(sin4, 0, sizeof(struct sockaddr_in)); + sin4->sin_addr.s_addr = addr; + sin4->sin_port = port; + sin4->sin_family = AF_INET; + sin4->sin_len = sizeof(struct sockaddr_in); +} + +/* Get a field from a \0 separated string */ +size_t +get_field(int peer, char *buffer, size_t size) +{ + char *cp = buffer; + + while (cp < buffer + size) { + if (*cp == '\0') break; + cp++; + } + if (*cp != '\0') { + tftp_log(LOG_ERR, "Bad option - no trailing \\0 found"); + send_error(peer, EBADOP); + exit(1); + } + return (cp - buffer + 1); +} + +/* + * Logging functions + */ +static int _tftp_logtostdout = 1; + +void +tftp_openlog(const char *ident, int logopt, int facility) +{ + + _tftp_logtostdout = (ident == NULL); + if (_tftp_logtostdout == 0) + openlog(ident, logopt, facility); +} + +void +tftp_closelog(void) +{ + + if (_tftp_logtostdout == 0) + closelog(); +} + +void +tftp_log(int priority, const char *message, ...) +{ + va_list ap; + int serrno; + char *s; + + serrno = errno; + va_start(ap, message); + if (_tftp_logtostdout == 0) { + vasprintf(&s, message, ap); + syslog(priority, "%s", s); + } else { + vprintf(message, ap); + printf("\n"); + } + va_end(ap); + errno = serrno; +} + +/* + * Packet types + */ +struct packettypes packettypes[] = { + { RRQ, "RRQ" }, + { WRQ, "WRQ" }, + { DATA, "DATA" }, + { ACK, "ACK" }, + { ERROR, "ERROR" }, + { OACK, "OACK" }, + { 0, NULL }, +}; + +const char * +packettype(int type) +{ + static char failed[100]; + int i = 0; + + while (packettypes[i].name != NULL) { + if (packettypes[i].value == type) + break; + i++; + } + if (packettypes[i].name != NULL) + return packettypes[i].name; + sprintf(failed, "unknown (type: %d)", type); + return (failed); +} + +/* + * Debugs + */ +int debug = DEBUG_NONE; +struct debugs debugs[] = { + { DEBUG_PACKETS, "packet", "Packet debugging" }, + { DEBUG_SIMPLE, "simple", "Simple debugging" }, + { DEBUG_OPTIONS, "options", "Options debugging" }, + { DEBUG_ACCESS, "access", "TCPd access debugging" }, + { DEBUG_NONE, NULL, "No debugging" }, +}; +unsigned int packetdroppercentage = 0; + +int +debug_find(char *s) +{ + int i = 0; + + while (debugs[i].name != NULL) { + if (strcasecmp(debugs[i].name, s) == 0) + break; + i++; + } + return (debugs[i].value); +} + +int +debug_finds(char *s) +{ + int i = 0; + char *ps = s; + + while (s != NULL) { + ps = strchr(s, ' '); + if (ps != NULL) + *ps = '\0'; + i += debug_find(s); + if (ps != NULL) + *ps = ' '; + s = ps; + } + return (i); +} + +const char * +debug_show(int d) +{ + static char s[100]; + size_t space = sizeof(s); + int i = 0; + + s[0] = '\0'; + while (debugs[i].name != NULL) { + if (d&debugs[i].value) { + if (s[0] != '\0') + strlcat(s, " ", space); + strlcat(s, debugs[i].name, space); + } + i++; + } + if (s[0] != '\0') + return (s); + return ("none"); +} + +/* + * RP_ + */ +struct rp_errors rp_errors[] = { + { RP_TIMEOUT, "Network timeout" }, + { RP_TOOSMALL, "Not enough data bytes" }, + { RP_WRONGSOURCE, "Invalid IP address of UDP port" }, + { RP_ERROR, "Error packet" }, + { RP_RECVFROM, "recvfrom() complained" }, + { RP_TOOBIG, "Too many data bytes" }, + { RP_NONE, NULL } +}; + +char * +rp_strerror(int error) +{ + static char s[100]; + size_t space = sizeof(s); + int i = 0; + + while (rp_errors[i].desc != NULL) { + if (rp_errors[i].error == error) { + strlcpy(s, rp_errors[i].desc, space); + space -= strlen(rp_errors[i].desc); + } + i++; + } + if (s[0] == '\0') + sprintf(s, "unknown (error=%d)", error); + return (s); +} + +/* + * Performance figures + */ + +void +stats_init(struct tftp_stats *ts) +{ + + ts->amount = 0; + ts->rollovers = 0; + ts->retries = 0; + ts->blocks = 0; + ts->amount = 0; + gettimeofday(&(ts->tstart), NULL); +} + +void +printstats(const char *direction, int verbose, struct tftp_stats *ts) +{ + double delta; /* compute delta in 1/10's second units */ + + delta = ((ts->tstop.tv_sec*10.)+(ts->tstop.tv_usec/100000)) - + ((ts->tstart.tv_sec*10.)+(ts->tstart.tv_usec/100000)); + delta = delta/10.; /* back to seconds */ + + printf("%s %zu bytes during %.1f seconds in %u blocks", + direction, ts->amount, delta, ts->blocks); + + if (ts->rollovers != 0) + printf(" with %d rollover%s", + ts->rollovers, ts->rollovers != 1 ? "s" : ""); + + if (verbose) + printf(" [%.0f bits/sec]", (ts->amount*8.)/delta); + putchar('\n'); +} diff --git a/libexec/tftpd/tftp-utils.h b/libexec/tftpd/tftp-utils.h new file mode 100644 index 000000000000..276dedcf74cd --- /dev/null +++ b/libexec/tftpd/tftp-utils.h @@ -0,0 +1,129 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 2008 Edwin Groothuis. 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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. + */ + +/* + */ +#define TIMEOUT 5 +#define MAX_TIMEOUTS 5 + +/* Generic values */ +#define MAXSEGSIZE 65464 /* Maximum size of the data segment */ +#define MAXPKTSIZE (MAXSEGSIZE + 4) /* Maximum size of the packet */ + +/* For the blksize option */ +#define BLKSIZE_MIN 8 /* Minimum size of the data segment */ +#define BLKSIZE_MAX MAXSEGSIZE /* Maximum size of the data segment */ + +/* For the timeout option */ +#define TIMEOUT_MIN 0 /* Minimum timeout value */ +#define TIMEOUT_MAX 255 /* Maximum timeout value */ +#define MIN_TIMEOUTS 3 + +/* For the windowsize option */ +#define WINDOWSIZE 1 +#define WINDOWSIZE_MIN 1 +#define WINDOWSIZE_MAX 65535 + +extern int timeoutpacket; +extern int timeoutnetwork; +extern int maxtimeouts; +int settimeouts(int timeoutpacket, int timeoutnetwork, int maxtimeouts); + +extern uint16_t segsize; +extern uint16_t pktsize; +extern uint16_t windowsize; + +extern int acting_as_client; + +/* + */ +void unmappedaddr(struct sockaddr_in6 *sin6); +size_t get_field(int peer, char *buffer, size_t size); + +/* + * Packet types + */ +struct packettypes { + int value; + const char *const name; +}; +extern struct packettypes packettypes[]; +const char *packettype(int); + +/* + * RP_ + */ +struct rp_errors { + int error; + const char *const desc; +}; +extern struct rp_errors rp_errors[]; +char *rp_strerror(int error); + +/* + * Debug features + */ +#define DEBUG_NONE 0x0000 +#define DEBUG_PACKETS 0x0001 +#define DEBUG_SIMPLE 0x0002 +#define DEBUG_OPTIONS 0x0004 +#define DEBUG_ACCESS 0x0008 +struct debugs { + int value; + const char *const name; + const char *const desc; +}; +extern int debug; +extern struct debugs debugs[]; +extern unsigned int packetdroppercentage; +int debug_find(char *s); +int debug_finds(char *s); +const char *debug_show(int d); + +/* + * Log routines + */ +#define DEBUG(s) tftp_log(LOG_DEBUG, "%s", s) +extern int tftp_logtostdout; +void tftp_openlog(const char *ident, int logopt, int facility); +void tftp_closelog(void); +void tftp_log(int priority, const char *message, ...) __printflike(2, 3); + +/* + * Performance figures + */ +struct tftp_stats { + size_t amount; + int rollovers; + uint32_t blocks; + int retries; + struct timeval tstart; + struct timeval tstop; +}; + +void stats_init(struct tftp_stats *ts); +void printstats(const char *direction, int verbose, struct tftp_stats *ts); diff --git a/libexec/tftpd/tftpd.8 b/libexec/tftpd/tftpd.8 new file mode 100644 index 000000000000..0118198da53d --- /dev/null +++ b/libexec/tftpd/tftpd.8 @@ -0,0 +1,333 @@ +.\" Copyright (c) 1983, 1991, 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. 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. +.\" +.Dd November 3, 2024 +.Dt TFTPD 8 +.Os +.Sh NAME +.Nm tftpd +.Nd Internet Trivial File Transfer Protocol server +.Sh SYNOPSIS +.Nm tftpd +.Op Fl bCcdlnoSw +.Op Fl F Ar strftime-format +.Op Fl s Ar directory +.Op Fl U Ar umask +.Op Fl u Ar user +.Op Ar directory ... +.Sh DESCRIPTION +The +.Nm +utility is a server which supports the +Internet Trivial File Transfer +Protocol +.Pq Tn RFC 1350 . +The +.Tn TFTP +server operates +at the port indicated in the +.Ql tftp +service description; +see +.Xr services 5 . +The server is normally started by +.Xr inetd 8 . +.Pp +The use of +.Xr tftp 1 +does not require an account or password on the remote system. +Due to the lack of authentication information, +.Nm +will allow only publicly readable files to be +accessed. +Files containing the string +.Dq Li "/../" +or starting with +.Dq Li "../" +are not allowed. +Files may be written only if they already exist (unless the +.Fl w +option is used) and are publicly writable (unless chrooted and the +.Fl S +option is used). +Note that this extends the concept of +.Dq public +to include +all users on all hosts that can be reached through the network; +this may not be appropriate on all systems, and its implications +should be considered before enabling tftp service. +The server should have the user ID with the lowest possible privilege. +.Pp +Access to files may be restricted by invoking +.Nm +with a list of directories by including up to 20 pathnames +as server program arguments in +.Xr inetd.conf 5 . +In this case access is restricted to files whose +names are prefixed by the one of the given directories. +The given directories are also treated as a search path for +relative filename requests. +.Pp +The +.Fl s +option provides additional security by changing +the root directory of +.Nm , +thereby prohibiting accesses to outside of the specified +.Ar directory . +Because +.Xr chroot 2 +requires super-user privileges, +.Nm +must be run as +.Li root . +However, after performing the +.Xr chroot 2 +call, +.Nm +will set its user ID to that of the specified +.Ar user , +or +.Dq Li nobody +if no +.Fl u +option is specified. +.Pp +The options are: +.Bl -tag -width Ds +.It Fl b +By default, +.Nm +expects an initial message to be available on its input socket. +If no data is available, the server exits immediately. +If +.Fl b +is specified, +.Nm +will block waiting for the initial message. +.It Fl c +Changes the default root directory of a connecting host via +.Xr chroot 2 +based on the connecting IP address. +This prevents multiple clients from writing to the same file at the same time. +If the directory does not exist, the client connection is refused. +The +.Fl s +option is required for +.Fl c +and the specified +.Ar directory +is used as a base. +.It Fl C +Operates the same as +.Fl c +except it falls back to +.Ar directory +specified via +.Fl s +if a directory does not exist for the client's IP. +.It Fl F +Use this +.Xr strftime 3 +compatible format string for the creation of the suffix if +.Fl W +is specified. +By default the string "%Y%m%d" is used. +.It Fl d, d Ar [value] +Enables debug output. +If +.Ar value +is not specified, then the debug level is increased by one +for each instance of +.Fl d +which is specified. +.Pp +If +.Ar value +is specified, then the debug level is set to +.Ar value . +The debug level is a bitmask implemented in +.Pa src/libexec/tftpd/tftp-utils.h . +Valid values are 0 (DEBUG_NONE), 1 (DEBUG_PACKETS), 2, (DEBUG_SIMPLE), +4 (DEBUG_OPTIONS), and 8 (DEBUG_ACCESS). Multiple debug values can be combined +in the bitmask by logically OR'ing the values. For example, specifying +.Fl d +.Ar 15 +will enable all the debug values. +.It Fl l +Log all requests using +.Xr syslog 3 +with the facility of +.Dv LOG_FTP . +.Sy Note : +Logging of +.Dv LOG_FTP +messages +must also be enabled in the syslog configuration file, +.Xr syslog.conf 5 . +.It Fl n +Suppress negative acknowledgement of requests for nonexistent +relative filenames. +.It Fl o +Disable support for RFC2347 style TFTP Options. +.It Fl s Ar directory +Cause +.Nm +to change its root directory to +.Ar directory . +After doing that but before accepting commands, +.Nm +will switch credentials to an unprivileged user. +.It Fl S +If +.Nm +runs chrooted, the option allows write requests according to generic +file permissions, skipping requirement for files to be publicly writable. +The option is ignored for non-chrooted run. +.It Fl u Ar user +Switch credentials to +.Ar user +(default +.Dq Li nobody ) +when the +.Fl s +option is used. +The user must be specified by name, not a numeric UID. +.It Fl U Ar umask +Set the +.Ar umask +for newly created files. +The default is 022 +.Pq Dv S_IWGRP | S_IWOTH . +.It Fl w +Allow write requests to create new files. +By default +.Nm +requires that the file specified in a write request exist. +Note that this only works in directories writable by the user +specified with +.Fl u +option +.It Fl W +As +.Fl w +but append a YYYYMMDD.nn sequence number to the end of the filename. +Note that the string YYYYMMDD can be changed with the +.Fl F +option. +.El +.Sh SEE ALSO +.Xr tftp 1 , +.Xr chroot 2 , +.Xr syslog 3 , +.Xr inetd.conf 5 , +.Xr services 5 , +.Xr syslog.conf 5 , +.Xr inetd 8 +.Pp +The following RFC's are supported: +.Rs +.%T RFC 1350: The TFTP Protocol (Revision 2) +.Re +.Rs +.%T RFC 2347: TFTP Option Extension +.Re +.Rs +.%T RFC 2348: TFTP Blocksize Option +.Re +.Rs +.%T RFC 2349: TFTP Timeout Interval and Transfer Size Options +.Re +.Rs +.%T RFC 7440: TFTP Windowsize Option +.Re +.Pp +The non-standard +.Cm rollover +and +.Cm blksize2 +TFTP options are mentioned here: +.Rs +.%T Extending TFTP +.%U http://www.compuphase.com/tftp.htm +.Re +.Sh HISTORY +The +.Nm +utility appeared in +.Bx 4.2 ; +the +.Fl s +option was introduced in +.Fx 2.2 , +the +.Fl u +option was introduced in +.Fx 4.2 , +the +.Fl c +option was introduced in +.Fx 4.3 , +the +.Fl F +and +.Fl W +options were introduced in +.Fx 7.4 , +and the +.Fl S +option was introduced in +.Fx 13.3 . +.Pp +Support for Timeout Interval and Transfer Size Options (RFC2349) +was introduced in +.Fx 5.0 , +support for the TFTP Blocksize Option (RFC2348) and the blksize2 option +was introduced in +.Fx 7.4 . +.Pp +Edwin Groothuis <edwin@FreeBSD.org> performed a major rewrite of the +.Nm +and +.Xr tftp 1 +code to support RFC2348. +.Pp +Support for the windowsize option (RFC7440) was introduced in +.Fx 13.0 . +.Sh NOTES +Files larger than 33,553,919 octets (65535 blocks, last one <512 +octets) cannot be correctly transferred without client and server +supporting blocksize negotiation (RFCs 2347 and 2348), +or the non-standard TFTP rollover option. +As a kludge, +.Nm +accepts a sequence of block number which wrap to zero after 65535, +even if the rollover option is not specified. +.Pp +Many tftp clients will not transfer files over 16,776,703 octets +(32767 blocks), as they incorrectly count the block number using +a signed rather than unsigned 16-bit integer. diff --git a/libexec/tftpd/tftpd.c b/libexec/tftpd/tftpd.c new file mode 100644 index 000000000000..a3faee86e7d0 --- /dev/null +++ b/libexec/tftpd/tftpd.c @@ -0,0 +1,849 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * 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. 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. + */ + +/* + * 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/socket.h> +#include <sys/stat.h> +#include <sys/time.h> + +#include <netinet/in.h> +#include <arpa/tftp.h> + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <netdb.h> +#include <pwd.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <time.h> +#include <unistd.h> + +#include "tftp-file.h" +#include "tftp-io.h" +#include "tftp-utils.h" +#include "tftp-transfer.h" +#include "tftp-options.h" + +#ifdef LIBWRAP +#include <tcpd.h> +#endif + +static void tftp_wrq(int peer, char *, size_t); +static void tftp_rrq(int peer, char *, size_t); + +/* + * 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 { + const char *name; + size_t len; +} dirs[MAXDIRS+1]; +static int suppress_naks; +static int logging; +static int ipchroot; +static int check_woth = 1; +static int create_new = 0; +static const char *newfile_format = "%Y%m%d"; +static int increase_name = 0; +static mode_t mask = S_IWGRP | S_IWOTH; + +struct formats; +static void tftp_recvfile(int peer, const char *mode); +static void tftp_xmitfile(int peer, const char *mode); +static int validate_access(int peer, char **, int); +static char peername[NI_MAXHOST]; + +static FILE *file; + +static struct formats { + const char *f_mode; + int f_convert; +} formats[] = { + { "netascii", 1 }, + { "octet", 0 }, + { NULL, 0 } +}; + +int +main(int argc, char *argv[]) +{ + struct tftphdr *tp; + int peer; + socklen_t peerlen, len; + ssize_t n; + int ch; + char *chroot_dir = NULL; + struct passwd *nobody; + const char *chuser = "nobody"; + char recvbuffer[MAXPKTSIZE]; + int allow_ro = 1, allow_wo = 1, block = 0, on = 1; + pid_t pid; + + tzset(); /* syslog in localtime */ + acting_as_client = 0; + + tftp_openlog("tftpd", LOG_PID | LOG_NDELAY, LOG_FTP); + while ((ch = getopt(argc, argv, "bcCd::F:lnoOp:s:Su:U:wW")) != -1) { + switch (ch) { + case 'b': + block = 1; + break; + case 'c': + ipchroot = 1; + break; + case 'C': + ipchroot = 2; + break; + case 'd': + if (optarg == NULL) + debug++; + else if (atoi(optarg) != 0) + debug += atoi(optarg); + else + debug |= debug_finds(optarg); + break; + case 'F': + newfile_format = optarg; + break; + case 'l': + logging = 1; + break; + case 'n': + suppress_naks = 1; + break; + case 'o': + options_rfc_enabled = 0; + break; + case 'O': + options_extra_enabled = 0; + break; + case 'p': + packetdroppercentage = (unsigned int)atoi(optarg); + tftp_log(LOG_INFO, + "Randomly dropping %d out of 100 packets", + packetdroppercentage); + break; + case 's': + chroot_dir = optarg; + break; + case 'S': + check_woth = -1; + break; + case 'u': + chuser = optarg; + break; + case 'U': + mask = strtol(optarg, NULL, 0); + break; + case 'w': + create_new = 1; + break; + case 'W': + create_new = 1; + increase_name = 1; + break; + default: + tftp_log(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 > 0 && chroot_dir == NULL) { + tftp_log(LOG_ERR, "-c requires -s"); + exit(1); + } + + umask(mask); + + /* Find out who we are talking to and what we are going to do */ + peerlen = sizeof(peer_sock); + n = recvfrom(0, recvbuffer, MAXPKTSIZE, block ? 0 : MSG_DONTWAIT, + (struct sockaddr *)&peer_sock, &peerlen); + if (n < 0) { + tftp_log(LOG_ERR, "recvfrom: %s", strerror(errno)); + exit(1); + } + getnameinfo((struct sockaddr *)&peer_sock, peer_sock.ss_len, + peername, sizeof(peername), NULL, 0, NI_NUMERICHOST); + if ((size_t)n < 4 /* tftphdr */) { + tftp_log(LOG_ERR, "Rejecting %zd-byte request from %s", + n, peername); + exit(1); + } + + if (ioctl(0, FIONBIO, &on) < 0) { + tftp_log(LOG_ERR, "ioctl(FIONBIO): %s", strerror(errno)); + 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. + */ + pid = fork(); + if (pid < 0) { + tftp_log(LOG_ERR, "fork: %s", strerror(errno)); + exit(1); + } else if (pid != 0) { + exit(0); + } + /* child */ + +#ifdef LIBWRAP + /* + * See if the client is allowed to talk to me. + * (This needs to be done before the chroot()) + */ + { + struct request_info req; + + request_init(&req, RQ_CLIENT_ADDR, peername, 0); + request_set(&req, RQ_DAEMON, "tftpd", 0); + + if (hosts_access(&req) == 0) { + if (debug & DEBUG_ACCESS) + tftp_log(LOG_WARNING, + "Access denied by 'tftpd' entry " + "in /etc/hosts.allow"); + + /* + * Full access might be disabled, but maybe the + * client is allowed to do read-only access. + */ + request_set(&req, RQ_DAEMON, "tftpd-ro", 0); + allow_ro = hosts_access(&req); + + request_set(&req, RQ_DAEMON, "tftpd-wo", 0); + allow_wo = hosts_access(&req); + + if (allow_ro == 0 && allow_wo == 0) { + tftp_log(LOG_WARNING, + "Unauthorized access from %s", peername); + exit(1); + } + + if (debug & DEBUG_ACCESS) { + if (allow_ro) + tftp_log(LOG_WARNING, + "But allowed readonly access " + "via 'tftpd-ro' entry"); + if (allow_wo) + tftp_log(LOG_WARNING, + "But allowed writeonly access " + "via 'tftpd-wo' entry"); + } + } else + if (debug & DEBUG_ACCESS) + tftp_log(LOG_WARNING, + "Full access allowed" + "in /etc/hosts.allow"); + } +#endif + + /* + * 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 > 0) { + char *tempchroot; + struct stat sb; + int statret; + struct sockaddr_storage ss; + char hbuf[NI_MAXHOST]; + + statret = -1; + memcpy(&ss, &peer_sock, peer_sock.ss_len); + unmappedaddr((struct sockaddr_in6 *)&ss); + getnameinfo((struct sockaddr *)&ss, ss.ss_len, + hbuf, sizeof(hbuf), NULL, 0, + NI_NUMERICHOST); + asprintf(&tempchroot, "%s/%s", chroot_dir, hbuf); + if (ipchroot == 2) + statret = stat(tempchroot, &sb); + if (ipchroot == 1 || + (statret == 0 && (sb.st_mode & S_IFDIR))) + chroot_dir = tempchroot; + } + /* Must get this before chroot because /etc might go away */ + if ((nobody = getpwnam(chuser)) == NULL) { + tftp_log(LOG_ERR, "%s: no such user", chuser); + exit(1); + } + if (chroot(chroot_dir)) { + tftp_log(LOG_ERR, "chroot: %s: %s", + chroot_dir, strerror(errno)); + exit(1); + } + if (chdir("/") != 0) { + tftp_log(LOG_ERR, "chdir: %s", strerror(errno)); + exit(1); + } + if (setgroups(0, NULL) != 0) { + tftp_log(LOG_ERR, "setgroups failed"); + exit(1); + } + if (setgid(nobody->pw_gid) != 0) { + tftp_log(LOG_ERR, "setgid failed"); + exit(1); + } + if (setuid(nobody->pw_uid) != 0) { + tftp_log(LOG_ERR, "setuid failed"); + exit(1); + } + if (check_woth == -1) + check_woth = 0; + } + if (check_woth == -1) + check_woth = 1; + + len = sizeof(me_sock); + if (getsockname(0, (struct sockaddr *)&me_sock, &len) == 0) { + switch (me_sock.ss_family) { + case AF_INET: + ((struct sockaddr_in *)&me_sock)->sin_port = 0; + break; + case AF_INET6: + ((struct sockaddr_in6 *)&me_sock)->sin6_port = 0; + break; + default: + /* unsupported */ + break; + } + } else { + memset(&me_sock, 0, sizeof(me_sock)); + me_sock.ss_family = peer_sock.ss_family; + me_sock.ss_len = peer_sock.ss_len; + } + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + peer = socket(peer_sock.ss_family, SOCK_DGRAM, 0); + if (peer < 0) { + tftp_log(LOG_ERR, "socket: %s", strerror(errno)); + exit(1); + } + if (bind(peer, (struct sockaddr *)&me_sock, me_sock.ss_len) < 0) { + tftp_log(LOG_ERR, "bind: %s", strerror(errno)); + exit(1); + } + + tp = (struct tftphdr *)recvbuffer; + tp->th_opcode = ntohs(tp->th_opcode); + if (tp->th_opcode == RRQ) { + if (allow_ro) + tftp_rrq(peer, tp->th_stuff, (size_t)n - 1); + else { + tftp_log(LOG_WARNING, + "%s read access denied", peername); + exit(1); + } + } else if (tp->th_opcode == WRQ) { + if (allow_wo) + tftp_wrq(peer, tp->th_stuff, (size_t)n - 1); + else { + tftp_log(LOG_WARNING, + "%s write access denied", peername); + exit(1); + } + } else + send_error(peer, EBADOP); + exit(1); +} + +static void +reduce_path(char *fn) +{ + char *slash, *ptr; + + /* Reduce all "/+./" to "/" (just in case we've got "/./../" later */ + while ((slash = strstr(fn, "/./")) != NULL) { + for (ptr = slash; ptr > fn && ptr[-1] == '/'; ptr--) + ; + slash += 2; + while (*slash) + *++ptr = *++slash; + } + + /* Now reduce all "/something/+../" to "/" */ + while ((slash = strstr(fn, "/../")) != NULL) { + if (slash == fn) + break; + for (ptr = slash; ptr > fn && ptr[-1] == '/'; ptr--) + ; + for (ptr--; ptr >= fn; ptr--) + if (*ptr == '/') + break; + if (ptr < fn) + break; + slash += 3; + while (*slash) + *++ptr = *++slash; + } +} + +static char * +parse_header(int peer, char *recvbuffer, size_t size, + char **filename, char **mode) +{ + struct formats *pf; + char *cp; + size_t i; + + *mode = NULL; + cp = recvbuffer; + + i = get_field(peer, recvbuffer, size); + if (i >= PATH_MAX) { + tftp_log(LOG_ERR, "Bad option - filename too long"); + send_error(peer, EBADOP); + exit(1); + } + *filename = recvbuffer; + tftp_log(LOG_INFO, "Filename: '%s'", *filename); + cp += i; + + i = get_field(peer, cp, size); + *mode = cp; + + /* Find the file transfer mode */ + for (; *cp; cp++) + if (isupper((unsigned char)*cp)) + *cp = tolower((unsigned char)*cp); + for (pf = formats; pf->f_mode; pf++) + if (strcmp(pf->f_mode, *mode) == 0) + break; + if (pf->f_mode == NULL) { + tftp_log(LOG_ERR, + "Bad option - Unknown transfer mode (%s)", *mode); + send_error(peer, EBADOP); + exit(1); + } + tftp_log(LOG_INFO, "Mode: '%s'", *mode); + + return (cp + 1); +} + +/* + * WRQ - receive a file from the client + */ +void +tftp_wrq(int peer, char *recvbuffer, size_t size) +{ + char *cp; + int has_options = 0, ecode; + char *filename, *mode; + char fnbuf[PATH_MAX]; + + cp = parse_header(peer, recvbuffer, size, &filename, &mode); + size -= (cp - recvbuffer) + 1; + + strlcpy(fnbuf, filename, sizeof(fnbuf)); + reduce_path(fnbuf); + filename = fnbuf; + + if (size > 0) { + if (options_rfc_enabled) + has_options = !parse_options(peer, cp, size); + else + tftp_log(LOG_INFO, "Options found but not enabled"); + } + + ecode = validate_access(peer, &filename, WRQ); + if (ecode == 0) { + if (has_options) + send_oack(peer); + else + send_ack(peer, 0); + } + if (logging) { + tftp_log(LOG_INFO, "%s: write request for %s: %s", peername, + filename, errtomsg(ecode)); + } + + if (ecode) { + send_error(peer, ecode); + exit(1); + } + tftp_recvfile(peer, mode); + exit(0); +} + +/* + * RRQ - send a file to the client + */ +void +tftp_rrq(int peer, char *recvbuffer, size_t size) +{ + char *cp; + int has_options = 0, ecode; + char *filename, *mode; + char fnbuf[PATH_MAX]; + + cp = parse_header(peer, recvbuffer, size, &filename, &mode); + size -= (cp - recvbuffer) + 1; + + strlcpy(fnbuf, filename, sizeof(fnbuf)); + reduce_path(fnbuf); + filename = fnbuf; + + if (size > 0) { + if (options_rfc_enabled) + has_options = !parse_options(peer, cp, size); + else + tftp_log(LOG_INFO, "Options found but not enabled"); + } + + ecode = validate_access(peer, &filename, RRQ); + if (ecode == 0) { + if (has_options) { + int n; + char lrecvbuffer[MAXPKTSIZE]; + struct tftphdr *rp = (struct tftphdr *)lrecvbuffer; + + send_oack(peer); + n = receive_packet(peer, lrecvbuffer, MAXPKTSIZE, + NULL, timeoutpacket); + if (n < 0) { + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, "Aborting: %s", + rp_strerror(n)); + return; + } + if (rp->th_opcode != ACK) { + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, + "Expected ACK, got %s on OACK", + packettype(rp->th_opcode)); + return; + } + } + } + + if (logging) + tftp_log(LOG_INFO, "%s: read request for %s: %s", peername, + 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); + send_error(peer, ecode); + exit(1); + } + tftp_xmitfile(peer, mode); +} + +/* + * Find the next value for YYYYMMDD.nn when the file to be written should + * be unique. Due to the limitations of nn, we will fail if nn reaches 100. + * Besides, that is four updates per hour on a file, which is kind of + * execessive anyway. + */ +static int +find_next_name(char *filename, int *fd) +{ + /* + * GCC "knows" that we might write all of yyyymmdd plus the static + * elemenents in the format into into newname and thus complains + * unless we reduce the size. This array is still too big, but since + * the format is user supplied, it's not clear what a better limit + * value would be and this is sufficent to silence the warnings. + */ + static const int suffix_len = strlen("..00"); + char yyyymmdd[MAXPATHLEN - suffix_len]; + char newname[MAXPATHLEN]; + int i, ret; + time_t tval; + size_t len, namelen; + struct tm lt; + + /* Create the YYYYMMDD part of the filename */ + time(&tval); + lt = *localtime(&tval); + len = strftime(yyyymmdd, sizeof(yyyymmdd), newfile_format, <); + if (len == 0) { + syslog(LOG_WARNING, + "Filename suffix too long (%zu characters maximum)", + sizeof(yyyymmdd) - 1); + return (EACCESS); + } + + /* Make sure the new filename is not too long */ + namelen = strlen(filename); + if (namelen >= sizeof(newname) - len - suffix_len) { + syslog(LOG_WARNING, + "Filename too long (%zu characters, %zu maximum)", + namelen, + sizeof(newname) - len - suffix_len - 1); + return (EACCESS); + } + + /* Find the first file which doesn't exist */ + for (i = 0; i < 100; i++) { + ret = snprintf(newname, sizeof(newname), "%s.%s.%02d", + filename, yyyymmdd, i); + /* + * Size checked above so this can't happen, we'd use a + * (void) cast, but gcc intentionally ignores that if + * snprintf has __attribute__((warn_unused_result)). + */ + if (ret < 0 || (size_t)ret >= sizeof(newname)) + __unreachable(); + *fd = open(newname, O_WRONLY | O_CREAT | O_EXCL, 0666); + if (*fd > 0) + return 0; + } + + return (EEXIST); +} + +/* + * 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(int peer, char **filep, int mode) +{ + static char pathname[MAXPATHLEN]; + struct stat sb; + struct dirlist *dirp; + char *filename = *filep; + int err, fd; + + /* + * Prevent tricksters from getting around the directory restrictions + */ + if (strncmp(filename, "../", 3) == 0 || + strstr(filename, "/../") != NULL) + return (EACCESS); + + if (*filename == '/') { + /* + * Absolute file name: allow the request if it's in one of the + * approved locations. + */ + for (dirp = dirs; dirp->name != NULL; dirp++) { + if (dirp->len == 1) + /* Only "/" can have len 1 */ + break; + if (strncmp(filename, dirp->name, dirp->len) == 0 && + 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, &sb) != 0) + return (errno == ENOENT ? ENOTFOUND : EACCESS); + if (!S_ISREG(sb.st_mode)) + return (ENOTFOUND); + if (mode == RRQ) { + if ((sb.st_mode & S_IROTH) == 0) + return (EACCESS); + } else { + if (check_woth && (sb.st_mode & S_IWOTH) == 0) + return (EACCESS); + } + } else { + /* + * Relative file name: search the approved locations for it. + * 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, &sb) != 0) + continue; + if (!S_ISREG(sb.st_mode)) + continue; + err = EACCESS; + if (mode == RRQ) { + if ((sb.st_mode & S_IROTH) == 0) + continue; + } else { + if (check_woth && (sb.st_mode & S_IWOTH) == 0) + continue; + } + break; + } + if (dirp->name != NULL) + *filep = filename = pathname; + else if (mode == RRQ) + return (err); + else if (err != ENOTFOUND || !create_new) + return (err); + } + + /* + * This option is handled here because it (might) require(s) the + * size of the file. + */ + option_tsize(peer, NULL, mode, &sb); + + if (mode == RRQ) { + fd = open(filename, O_RDONLY); + } else if (create_new) { + if (increase_name) { + err = find_next_name(filename, &fd); + if (err > 0) + return (err + 100); + } else { + fd = open(filename, + O_WRONLY | O_TRUNC | O_CREAT, + S_IRUSR | S_IWUSR | S_IRGRP | + S_IWGRP | S_IROTH | S_IWOTH ); + } + } else { + fd = open(filename, O_WRONLY | O_TRUNC); + } + if (fd < 0) + return (errno + 100); + file = fdopen(fd, mode == RRQ ? "r" : "w"); + if (file == NULL) { + close(fd); + return (errno + 100); + } + return (0); +} + +static void +tftp_xmitfile(int peer, const char *mode) +{ + uint16_t block; + time_t now; + struct tftp_stats ts; + + memset(&ts, 0, sizeof(ts)); + now = time(NULL); + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, "Transmitting file"); + + read_init(0, file, mode); + block = 1; + tftp_send(peer, &block, &ts); + read_close(); + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_INFO, "Sent %jd bytes in %jd seconds", + (intmax_t)ts.amount, (intmax_t)time(NULL) - now); +} + +static void +tftp_recvfile(int peer, const char *mode) +{ + uint16_t block; + struct timeval now1, now2; + struct tftp_stats ts; + + gettimeofday(&now1, NULL); + if (debug & DEBUG_SIMPLE) + tftp_log(LOG_DEBUG, "Receiving file"); + + write_init(0, file, mode); + + block = 0; + tftp_receive(peer, &block, &ts, NULL, 0); + + gettimeofday(&now2, NULL); + + if (debug & DEBUG_SIMPLE) { + double f; + if (now1.tv_usec > now2.tv_usec) { + now2.tv_usec += 1000000; + now2.tv_sec--; + } + + f = now2.tv_sec - now1.tv_sec + + (now2.tv_usec - now1.tv_usec) / 100000.0; + tftp_log(LOG_INFO, + "Download of %jd bytes in %d blocks completed after %0.1f seconds\n", + (intmax_t)ts.amount, block, f); + } + + return; +} |