diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 882a898..7547ae0 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -19,8 +19,15 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential autoconf automake libtool pkg-config + sudo apt-get install -y build-essential autoconf automake libtool pkg-config \ + tftpd-hpa tftp-hpa sudo modprobe tun + # The tftpd-hpa package installs a systemd unit that binds the + # standard TFTP port. The interop test starts its own in.tftpd + # bound to the test tap link, so stop the system instance to + # keep ports / file ownership predictable. + sudo systemctl stop tftpd-hpa || true + sudo systemctl disable tftpd-hpa || true - name: Clone and build wolfSSL from nightly-snapshot run: | @@ -129,6 +136,28 @@ jobs: set -euo pipefail timeout --preserve-status 5m build/test/unit + - name: Build TFTP interop test + run: | + make build/test-tftp-interop + + - name: Run TFTP interop test (wolfIP client vs tftpd-hpa, wolfIP server vs tftp-hpa) + timeout-minutes: 3 + run: | + set -euo pipefail + timeout --preserve-status 3m sudo ./build/test-tftp-interop all + sudo killall tcpdump || true + + - name: Upload TFTP interop diagnostics on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: tftp-interop-diagnostics + path: | + /tmp/wolfip-tftp.pcap + /tmp/wolfip-tftpd-hpa.log + /tmp/wolfip-tftp-client.log + if-no-files-found: ignore + - name: Build ESP unit tests run: | make unit-esp diff --git a/.gitignore b/.gitignore index e4878b3..4583854 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.bin *.swp *.elf +*.gcov CMakeCache.txt CMakeFiles CMakeScripts diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c5c8e8..e74851c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,11 +52,25 @@ string(TOLOWER "${CMAKE_SYSTEM_NAME}" CMAKE_SYSTEM_NAME_LC) set(WOLFIP_TAP_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/port/posix/tap_${CMAKE_SYSTEM_NAME_LC}.c") +# Optional TFTP client/server module. Default off to match config.h +# (WOLFIP_ENABLE_TFTP == 0); turn on with -DWOLFIP_ENABLE_TFTP=ON. +option(WOLFIP_ENABLE_TFTP "Build and link the wolfIP TFTP client/server" OFF) +if (WOLFIP_ENABLE_TFTP) + file(GLOB WOLFIP_TFTP_SRCS CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/tftp/*.c") + add_compile_definitions(WOLFIP_ENABLE_TFTP=1) +else() + set(WOLFIP_TFTP_SRCS ) +endif() + if (NOT EXISTS "${WOLFIP_TAP_SRC}") message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") endif() -set(WOLFIP_SRCS src/wolfip.c ${WOLFIP_TAP_SRC}) +set(WOLFIP_SRCS + src/wolfip.c + ${WOLFIP_TFTP_SRCS} + ${WOLFIP_TAP_SRC}) set(CERT_SRCS ${CMAKE_BINARY_DIR}/certs/server_cert.c @@ -187,7 +201,7 @@ add_executable(test-ttl-expired ${EXCLUDE_TEST_BINARY} target_compile_definitions(test-ttl-expired PRIVATE -DWOLFIP_MAX_INTERFACES=2 -DWOLFIP_ENABLE_FORWARDING=1) add_test(NAME ttl-expired COMMAND test-ttl-expired) -if (NOT Check_FOUND) +if (Check_FOUND) add_executable(unit ${EXCLUDE_TEST_BINARY} src/test/unit/unit.c ) diff --git a/Makefile b/Makefile index 6125f05..ede8457 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,21 @@ endif TAP_OBJ:=$(NETDEV_OBJ) TAP_PIE_OBJ:=$(NETDEV_PIE_OBJ) +# Optional TFTP module. Default to off to match config.h +# (WOLFIP_ENABLE_TFTP == 0); set WOLFIP_ENABLE_TFTP=1 on the command +# line to compile and link the TFTP client/server objects. +WOLFIP_ENABLE_TFTP ?= 0 +ifeq ($(WOLFIP_ENABLE_TFTP),1) +WOLFIP_TFTP_SRC:=$(wildcard src/tftp/*.c) +WOLFIP_TFTP_OBJ:=$(patsubst src/%.c,build/%.o,$(WOLFIP_TFTP_SRC)) +WOLFIP_TFTP_PIE_OBJ:=$(patsubst src/%.c,build/pie/%.o,$(WOLFIP_TFTP_SRC)) +CFLAGS+=-DWOLFIP_ENABLE_TFTP=1 +else +WOLFIP_TFTP_SRC:= +WOLFIP_TFTP_OBJ:= +WOLFIP_TFTP_PIE_OBJ:= +endif + ifeq ($(UNAME_S),Darwin) BEGIN_GROUP:= END_GROUP:= @@ -135,12 +150,15 @@ CPPCHECK_FLAGS=--enable=warning,performance,portability,missingInclude \ --error-exitcode=1 --xml --xml-version=2 OBJ=build/wolfip.o \ + $(WOLFIP_TFTP_OBJ) \ $(TAP_OBJ) IPFILTER_OBJ=build/ipfilter/wolfip.o \ + $(WOLFIP_TFTP_OBJ) \ $(TAP_OBJ) ESP_OBJ=build/esp/wolfip.o \ + $(WOLFIP_TFTP_OBJ) \ $(TAP_OBJ) HAVE_WOLFSSL:=$(shell printf "#include \nint main(void){return 0;}\n" | $(CC) $(CFLAGS) -x c - -c -o /dev/null 2>/dev/null && echo 1) @@ -185,6 +203,7 @@ libtcpip.a: $(OBJ) libwolfip.so:CFLAGS+=-fPIC libwolfip.so: build/pie/port/posix/bsd_socket.o build/pie/wolfip.o \ + $(WOLFIP_TFTP_PIE_OBJ) \ $(TAP_PIE_OBJ) @mkdir -p `dirname $@` || true @echo "[LD] $@" @@ -257,6 +276,39 @@ build/test-dns: $(OBJ) build/test/test_dhcp_dns.o @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP) +# Bidirectional TFTP interop test against tftpd-hpa / tftp-hpa. +# Forces WOLFIP_ENABLE_TFTP=1 and uses a single-session server so the +# default UDP socket pool can hold both the listen and the transfer +# socket without raising MAX_UDPSOCKETS. +build/tftp-interop/wolfip.o: src/wolfip.c + @mkdir -p `dirname $@` || true + @echo "[CC] $< (tftp-interop)" + @$(CC) $(CFLAGS) -DWOLFIP_ENABLE_TFTP=1 -c $< -o $@ + +build/tftp-interop/wolftftp.o: src/tftp/wolftftp.c + @mkdir -p `dirname $@` || true + @echo "[CC] $< (tftp-interop)" + @$(CC) $(CFLAGS) -DWOLFIP_ENABLE_TFTP=1 -DWOLFTFTP_SERVER_MAX_SESSIONS=1 \ + -c $< -o $@ + +build/test/test_tftp_interop.o: src/test/test_tftp_interop.c + @mkdir -p `dirname $@` || true + @echo "[CC] $<" + @$(CC) $(CFLAGS) -DWOLFIP_ENABLE_TFTP=1 -DWOLFTFTP_SERVER_MAX_SESSIONS=1 \ + -c $< -o $@ + +build/test-tftp-interop: build/tftp-interop/wolfip.o \ + build/tftp-interop/wolftftp.o $(TAP_OBJ) \ + build/test/test_tftp_interop.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP) + +.PHONY: tftp-interop-test +tftp-interop-test: build/test-tftp-interop + @echo "[RUN] $< (requires root, tftpd-hpa and tftp-hpa)" + @sudo -n true >/dev/null 2>&1 || { echo "tftp-interop-test needs to run as root (sudo)"; exit 1; } + @sudo ./build/test-tftp-interop all + build/tcpecho: $(OBJ) build/port/posix/bsd_socket.o build/test/tcp_echo.o @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP) @@ -321,7 +373,7 @@ build/esp-server: $(ESP_OBJ) build/port/posix/bsd_socket.o build/test/esp_server @echo "[LD] $@" @$(CC) $(CFLAGS) $(ESP_CFLAGS) $(LDFLAGS) -o $@ $(BEGIN_GROUP) $(^) -lwolfssl $(END_GROUP) -build/test-wolfssl-forwarding: build/test/test_wolfssl_forwarding.o build/test/wolfip_forwarding.o $(TAP_OBJ) build/port/wolfssl_io.o build/certs/server_key.o build/certs/ca_cert.o build/certs/server_cert.o +build/test-wolfssl-forwarding: build/test/test_wolfssl_forwarding.o build/test/wolfip_forwarding.o $(WOLFIP_TFTP_OBJ) $(TAP_OBJ) build/port/wolfssl_io.o build/certs/server_key.o build/certs/ca_cert.o build/certs/server_cert.o @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) -lwolfssl $(END_GROUP) @@ -333,7 +385,7 @@ build/test/wolfip_forwarding.o: src/wolfip.c @$(CC) $(CFLAGS) -DWOLFIP_MAX_INTERFACES=2 -DWOLFIP_ENABLE_FORWARDING=1 -c $< -o $@ build/test/test_ttl_expired.o: CFLAGS+=-DWOLFIP_MAX_INTERFACES=2 -DWOLFIP_ENABLE_FORWARDING=1 -build/test-ttl-expired: build/test/test_ttl_expired.o build/test/wolfip_forwarding.o +build/test-ttl-expired: build/test/test_ttl_expired.o build/test/wolfip_forwarding.o $(WOLFIP_TFTP_OBJ) @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP) @@ -386,7 +438,8 @@ UNIT_TEST_SRCS:=src/test/unit/unit.c \ src/test/unit/unit_tests_tcp_ack.c \ src/test/unit/unit_tests_tcp_flow.c \ src/test/unit/unit_tests_proto.c \ - src/test/unit/unit_tests_multicast.c + src/test/unit/unit_tests_multicast.c \ + src/test/unit/unit_tests_tftp.c unit: build/test/unit @@ -486,19 +539,29 @@ $(COV_MCAST_UNIT): $(COV_MCAST_UNIT_O) cov: unit $(COV_UNIT) @echo "[RUN] unit (coverage)" @rm -f $(COV_DIR)/*.gcda + @rm -f $(COV_DIR)/unit-multicast $(COV_DIR)/unit-multicast.o \ + $(COV_DIR)/unit-multicast.gcno $(COV_DIR)/unit-multicast.gcda @$(COV_UNIT) @echo "[COV] gcovr html" @mkdir -p build/coverage - @gcovr -r . --exclude "src/test/unit/.*" --html-details -o build/coverage/index.html + @gcovr -r . --exclude "src/test/unit/.*" \ + --gcov-ignore-errors=no_working_dir_found \ + --merge-mode-functions=merge-use-line-min \ + --html-details -o build/coverage/index.html @$(OPEN_CMD) build/coverage/index.html autocov: unit $(COV_UNIT) @echo "[RUN] unit (coverage)" @rm -f $(COV_DIR)/*.gcda + @rm -f $(COV_DIR)/unit-multicast $(COV_DIR)/unit-multicast.o \ + $(COV_DIR)/unit-multicast.gcno $(COV_DIR)/unit-multicast.gcda @$(COV_UNIT) @echo "[COV] gcovr html" @mkdir -p build/coverage - @gcovr -r . --exclude "src/test/unit/.*" --html-details -o build/coverage/index.html + @gcovr -r . --exclude "src/test/unit/.*" \ + --gcov-ignore-errors=no_working_dir_found \ + --merge-mode-functions=merge-use-line-min \ + --html-details -o build/coverage/index.html autocov-multicast: unit-multicast $(COV_MCAST_UNIT) @echo "[RUN] unit multicast (coverage)" @@ -506,7 +569,10 @@ autocov-multicast: unit-multicast $(COV_MCAST_UNIT) @$(COV_MCAST_UNIT) @echo "[COV] gcovr multicast html" @mkdir -p build/coverage - @gcovr -r . --exclude "src/test/unit/.*" --html-details -o build/coverage/multicast.html + @gcovr -r . --exclude "src/test/unit/.*" \ + --gcov-ignore-errors=no_working_dir_found \ + --merge-mode-functions=merge-use-line-min \ + --html-details -o build/coverage/multicast.html # Install dynamic library to re-link linux applications # diff --git a/README.md b/README.md index cebbf6e..5c77832 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ configured to forward traffic between multiple network interfaces. - Multi-interface support - Optional IPv4-forwarding - Optional IPv4 UDP multicast with IGMPv3 ASM membership reports +- Reusable allocation-free TFTP module under `src/tftp/` ## Supported socket types @@ -53,6 +54,7 @@ wolfIP exposes a BSD-like `socket(2)` API for IPv4 sockets: | **Application** | DHCP | Client only (DORA) | [RFC 2131](https://datatracker.ietf.org/doc/html/rfc2131) | | **Application** | DNS | A and PTR record queries (client) | [RFC 1035](https://datatracker.ietf.org/doc/html/rfc1035) | | **Application** | HTTP/HTTPS | Server with wolfSSL TLS support | [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110) | +| **Application** | TFTP | Client/server octet-mode transfers with callback-driven storage and verification | [RFC 1350](https://datatracker.ietf.org/doc/html/rfc1350), [RFC 2347](https://datatracker.ietf.org/doc/html/rfc2347), [RFC 2348](https://datatracker.ietf.org/doc/html/rfc2348), [RFC 2349](https://datatracker.ietf.org/doc/html/rfc2349), [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) | | **VPN** | wolfGuard | FIPS-compliant WireGuard (P-256, AES-256-GCM, SHA-256) | [Wolfguard](https://www.github.com/wolfssl/wireguard) | ## wolfGuard (FIPS WireGuard) @@ -180,6 +182,14 @@ This port follows the same model as the POSIX wrapper: - Socket wrappers serialize stack access with a mutex - Blocking operations wait on callback-driven wakeups (instead of busy polling) +## Source Layout + +- `src/wolfip.c`: core TCP/IP stack +- `src/http/`: optional HTTP/HTTPS server pieces +- `src/tftp/`: reusable TFTP module sources, auto-registered by the top-level `Makefile` and `CMakeLists.txt` when present +- `src/port/`: platform and OS adaptation layers +- `src/test/`: integration and unit tests + ## Copyright and License wolfIP is licensed under the GPLv3 license. See the LICENSE file for details. diff --git a/config.h b/config.h index e2a328e..6e80309 100644 --- a/config.h +++ b/config.h @@ -70,6 +70,10 @@ #define WOLFIP_ENABLE_HTTP #endif +#ifndef WOLFIP_ENABLE_TFTP +#define WOLFIP_ENABLE_TFTP 0 +#endif + #if WOLFIP_ENABLE_LOOPBACK && WOLFIP_MAX_INTERFACES < 2 #error "WOLFIP_ENABLE_LOOPBACK requires WOLFIP_MAX_INTERFACES > 1" #endif diff --git a/docs/API.md b/docs/API.md index ad3bf14..69636ae 100644 --- a/docs/API.md +++ b/docs/API.md @@ -16,9 +16,25 @@ wolfIP is a minimal TCP/IP stack designed for resource-constrained embedded syst - ICMP (RFC 792) - ping replies only - DHCP (RFC 2131) - client only - DNS (RFC 1035) - client only + - TFTP (RFC 1350, RFC 2347, RFC 2348, RFC 2349, RFC 7440) via the reusable `src/tftp/` module - UDP (RFC 768) - unicast, optional IPv4 multicast with `IP_MULTICAST` - TCP (RFC 793) with options (Timestamps, MSS) +## Build Integration + +The top-level build systems register reusable module sources from `src/tftp/` +automatically: + +- `Makefile` adds any `src/tftp/*.c` files to the shared library, static library, + and top-level executable link sets. +- `CMakeLists.txt` globs `src/tftp/*.c` with `CONFIGURE_DEPENDS` so the same + sources are compiled into the main `wolfip` and `tcpip` targets. + +The TFTP module is callback-driven and allocation-free. Callers provide the UDP +send hook plus open/read/write/close callbacks for storage, and may additionally +provide streaming hash-update and final verification callbacks for firmware +download flows. + ## Core Data Structures ### Device Driver Interface diff --git a/src/test/test_tftp_interop.c b/src/test/test_tftp_interop.c new file mode 100644 index 0000000..a3929db --- /dev/null +++ b/src/test/test_tftp_interop.c @@ -0,0 +1,963 @@ +/* test_tftp_interop.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Bidirectional TFTP interop test against the Debian tftp-hpa/tftpd-hpa + * pair, exercising the wolfIP TFTP client and server end-to-end over a + * TAP link. + * + * client mode: wolfIP TFTP client GETs a fixture file from a local + * in.tftpd daemon launched against + * tools/scripts/tftpd-hpa-wolfip.conf + * server mode: a Linux /usr/bin/tftp client (tftp-hpa) GETs a fixture + * file from the wolfIP TFTP server + * + * Both directions transfer a small known-content file and only succeed + * if the bytes on the receiving end match the fixture exactly. + * + * The test requires root (to set up the TAP link), in.tftpd, and the + * tftp-hpa command line client. Without those, it returns 77 to signal + * "skipped" so the make target can be safely run on bare CI images. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "wolfip.h" +#include "src/tftp/wolftftp.h" + +#ifndef WOLFIP_ENABLE_TFTP +#error "test_tftp_interop requires WOLFIP_ENABLE_TFTP=1" +#endif + +#define TFTP_INTEROP_PORT 6969 +#define TFTP_INTEROP_TRANSFER_PORT 6970 +#define TFTP_INTEROP_CLIENT_PORT 6989 + +#define TFTP_INTEROP_REMOTE_NAME "wolfip_tftp_fixture.bin" + +/* All filesystem paths used by this test live under a fresh + * mkdtemp() directory (mode 0700, owned by the running user) so that + * a hostile local user on a multi-tenant box cannot precreate a + * symlink at one of the test's well-known paths and redirect the + * (root-running) test's writes elsewhere. The previous fixed-path + * layout (`/tmp/wolfip-tftp-*`) was a TOCTOU / symlink-attack + * vector — fixed only paths now are the diagnostics pcap and the + * Makefile-side artifact uploader's expectations, both kept under + * /tmp and re-emitted by name from the temp root before tearing it + * down. */ +static char tftp_workdir[64]; +static char tftp_local_dir[96]; +static char tftp_fixture_path[160]; +static char tftp_download_path[160]; +static char tftp_host_get_path[160]; +static char tftp_tftpd_log[160]; +static char tftp_tftp_log[160]; + +#define TFTP_INTEROP_DIAG_PCAP "/tmp/wolfip-tftp.pcap" +#define TFTP_INTEROP_DIAG_TFTPD "/tmp/wolfip-tftpd-hpa.log" +#define TFTP_INTEROP_DIAG_TFTP "/tmp/wolfip-tftp-client.log" + +/* Picked so it is NOT an exact multiple of WOLFTFTP_DEFAULT_BLKSIZE + * (512): the last DATA block must be smaller than blksize so the + * receiving side can detect EOF without an extra 0-byte trailer. */ +#define TFTP_INTEROP_FIXTURE_SIZE 1500U + +#define TFTP_INTEROP_TIMEOUT_MS 8000U + +#define TFTP_EXIT_SUCCESS 0 +#define TFTP_EXIT_FAIL 1 +#define TFTP_EXIT_SKIP 77 + +extern int tap_init(struct wolfIP_ll_dev *dev, const char *name, uint32_t host_ip); + +static uint64_t now_ms(void) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + return (uint64_t)tv.tv_sec * 1000U + (uint64_t)tv.tv_usec / 1000U; +} + +static int file_equal(const char *a, const char *b) +{ + FILE *fa; + FILE *fb; + int rc = 0; + int ca; + int cb; + + fa = fopen(a, "rb"); + fb = fopen(b, "rb"); + if (fa == NULL || fb == NULL) { + if (fa != NULL) fclose(fa); + if (fb != NULL) fclose(fb); + return 0; + } + do { + ca = fgetc(fa); + cb = fgetc(fb); + if (ca != cb) { + rc = 0; + goto done; + } + } while (ca != EOF); + rc = 1; +done: + fclose(fa); + fclose(fb); + return rc; +} + +/* Open a file beneath the test workdir for writing. O_NOFOLLOW + * refuses to traverse a symlink at the leaf (so a precreated link + * can't redirect our writes to / etc / shadow when the test runs as + * root); O_EXCL refuses to overwrite an existing entry. The workdir + * itself is mode 0700, so a hostile user shouldn't be able to plant + * anything in it to begin with — these flags are belt-and-braces. */ +static int open_workdir_file_for_write(const char *path) +{ + return open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600); +} + +static int write_fixture(const char *path) +{ + FILE *fp; + int fd; + unsigned int i; + + (void)unlink(path); + fd = open_workdir_file_for_write(path); + if (fd < 0) + return -1; + fp = fdopen(fd, "wb"); + if (fp == NULL) { + close(fd); + return -1; + } + for (i = 0; i < TFTP_INTEROP_FIXTURE_SIZE; i++) { + unsigned char b = (unsigned char)((i * 31U + 7U) & 0xFFU); + if (fputc(b, fp) == EOF) { + fclose(fp); + return -1; + } + } + fclose(fp); + /* tftpd-hpa's validate_access() refuses to serve a file unless + * S_IROTH is set on it — the rationale being that UDP has no + * authentication, so files have to be explicitly marked + * "okay to serve". The privacy of the fixture is already covered + * by the 0700 mkdtemp workdir wrapping it, so no other user can + * traverse down to this file. */ + (void)chmod(path, 0644); + return 0; +} + +/* Build the per-run workdir layout under a freshly-created mkdtemp + * directory. Returns 0 on success, -1 on failure with errno set. The + * directory is mode 0700 by mkdtemp's contract. */ +static int tftp_workdir_setup(void) +{ + snprintf(tftp_workdir, sizeof(tftp_workdir), + "/tmp/wolfip-tftp-XXXXXX"); + if (mkdtemp(tftp_workdir) == NULL) + return -1; + /* in.tftpd chroots into the TFTP root and serves files relative + * to it. Create a "root/" subdir inside the workdir so the chroot + * target is owned by us and predictable. */ + snprintf(tftp_local_dir, sizeof(tftp_local_dir), + "%s/root", tftp_workdir); + if (mkdir(tftp_local_dir, 0700) != 0) + return -1; + snprintf(tftp_fixture_path, sizeof(tftp_fixture_path), + "%s/%s", tftp_local_dir, TFTP_INTEROP_REMOTE_NAME); + snprintf(tftp_download_path, sizeof(tftp_download_path), + "%s/download.bin", tftp_workdir); + snprintf(tftp_host_get_path, sizeof(tftp_host_get_path), + "%s/host-get.bin", tftp_workdir); + snprintf(tftp_tftpd_log, sizeof(tftp_tftpd_log), + "%s/tftpd.log", tftp_workdir); + snprintf(tftp_tftp_log, sizeof(tftp_tftp_log), + "%s/tftp-client.log", tftp_workdir); + return 0; +} + +static void tftp_workdir_publish_diagnostics(void) +{ + char cmd[256]; + /* Copy the diagnostic artifacts to fixed /tmp paths the CI + * artifact uploader expects. These targets are short-lived and + * only created on the failure path, so a symlink-attack window + * here is minimal — but use --no-target-directory to refuse to + * overwrite anything we did not create ourselves. */ + if (tftp_workdir[0] == '\0') + return; + snprintf(cmd, sizeof(cmd), + "cp -f -- %s/tftpd.log " TFTP_INTEROP_DIAG_TFTPD + " 2>/dev/null || true", tftp_workdir); + (void)system(cmd); + snprintf(cmd, sizeof(cmd), + "cp -f -- %s/tftp-client.log " TFTP_INTEROP_DIAG_TFTP + " 2>/dev/null || true", tftp_workdir); + (void)system(cmd); +} + +static void tftp_workdir_teardown(void) +{ + char cmd[160]; + if (tftp_workdir[0] == '\0') + return; + snprintf(cmd, sizeof(cmd), "rm -rf -- %s", tftp_workdir); + (void)system(cmd); + tftp_workdir[0] = '\0'; +} + +static int file_present(const char *path) +{ + struct stat st; + return stat(path, &st) == 0 && (st.st_mode & S_IXUSR) != 0; +} + +/* ---------- file-backed io_ops shared by client and server tests ---- */ + +struct tftp_file_ctx { + FILE *fp; + const char *path; + int is_write; + uint32_t size; +}; + +static int io_open(void *arg, const char *name, int is_write, + uint32_t *size_hint, void **handle) +{ + struct tftp_file_ctx *ctx = (struct tftp_file_ctx *)arg; + struct stat st; + + (void)name; + ctx->is_write = is_write; + ctx->fp = fopen(ctx->path, is_write ? "wb+" : "rb"); + if (ctx->fp == NULL) + return -1; + if (!is_write && stat(ctx->path, &st) == 0) { + ctx->size = (uint32_t)st.st_size; + if (size_hint != NULL) + *size_hint = ctx->size; + } + *handle = ctx->fp; + return 0; +} + +static int io_read(void *arg, void *handle, uint32_t offset, + uint8_t *buf, uint16_t max_len, uint16_t *out_len, int *is_last) +{ + FILE *fp = (FILE *)handle; + size_t n; + + (void)arg; + if (fseek(fp, (long)offset, SEEK_SET) != 0) + return -1; + n = fread(buf, 1, max_len, fp); + *out_len = (uint16_t)n; + /* Only flag is_last when this read produced *less* than the + * negotiated blksize. Files that end on a block boundary must be + * followed by a 0-byte DATA block per RFC 1350, so we let the + * server pull one more (empty) block instead of declaring EOF on + * a full-sized read. */ + *is_last = (n < max_len) ? 1 : 0; + return 0; +} + +static int io_write(void *arg, void *handle, uint32_t offset, + const uint8_t *buf, uint16_t len) +{ + FILE *fp = (FILE *)handle; + + (void)arg; + if (fseek(fp, (long)offset, SEEK_SET) != 0) + return -1; + if (fwrite(buf, 1, len, fp) != len) + return -1; + fflush(fp); + return 0; +} + +static void io_close(void *arg, void *handle, int status) +{ + (void)arg; + (void)status; + if (handle != NULL) + fclose((FILE *)handle); +} + +/* ---------- transport glue for the client ------------------------- */ + +struct client_glue { + struct wolfIP *s; + int sock; + int trace; +}; + +static void trace_packet(const char *who, const char *dir, + uint32_t ip, uint16_t port, const uint8_t *buf, int len) +{ + uint16_t opcode = (len >= 2) ? + (uint16_t)(((uint16_t)buf[0] << 8) | buf[1]) : 0; + uint16_t block = (len >= 4) ? + (uint16_t)(((uint16_t)buf[2] << 8) | buf[3]) : 0; + fprintf(stderr, "[%s] %s %d.%d.%d.%d:%u op=%u blk/data0=%u len=%d\n", + who, dir, + (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF, + port, opcode, block, len); +} + +static int client_send(void *arg, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len) +{ + struct client_glue *g = (struct client_glue *)arg; + struct wolfIP_sockaddr_in dst; + int ret; + + (void)local_port; + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = ee16(remote->port); + dst.sin_addr.s_addr = ee32(remote->ip); + ret = wolfIP_sock_sendto(g->s, g->sock, buf, len, 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)); + if (g->trace) + trace_packet("client", "TX", remote->ip, remote->port, buf, + ret > 0 ? ret : (int)len); + if (ret == (int)len) + return 0; + fprintf(stderr, "[client] sendto returned %d (expected %u)\n", ret, len); + return ret < 0 ? ret : -1; +} + +/* ---------- transport glue for the server ------------------------- */ + +struct server_glue { + struct wolfIP *s; + int listen_sock; + int transfer_sock; + int trace; +}; + +static int server_send(void *arg, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len) +{ + struct server_glue *g = (struct server_glue *)arg; + struct wolfIP_sockaddr_in dst; + int sock; + int ret; + + if (local_port == TFTP_INTEROP_PORT) + sock = g->listen_sock; + else + sock = g->transfer_sock; + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = ee16(remote->port); + dst.sin_addr.s_addr = ee32(remote->ip); + ret = wolfIP_sock_sendto(g->s, sock, buf, len, 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)); + if (g->trace) + trace_packet("server", "TX", remote->ip, remote->port, buf, + ret > 0 ? ret : (int)len); + if (ret == (int)len) + return 0; + fprintf(stderr, "[server] sendto on local_port %u returned %d " + "(expected %u)\n", local_port, ret, len); + return ret < 0 ? ret : -1; +} + +/* ---------- tftpd-hpa lifecycle ----------------------------------- */ + +static pid_t tftpd_pid = -1; + +static void tftpd_stop(void) +{ + if (tftpd_pid > 0) { + kill(tftpd_pid, SIGTERM); + waitpid(tftpd_pid, NULL, 0); + tftpd_pid = -1; + } +} + +static int tftpd_start(void) +{ + pid_t pid; + char addrport[64]; + const char *exe = "/usr/sbin/in.tftpd"; + int log_fd; + + snprintf(addrport, sizeof(addrport), "%s:%d", HOST_STACK_IP, + TFTP_INTEROP_PORT); + pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + /* Child: become in.tftpd. tftpd-hpa otherwise logs via syslog, + * which the test runner can't see — redirect stderr (where -v + * prints) so any rejection is visible after the run. */ + /* Log file lives in the mkdtemp workdir (mode 0700). Use + * O_NOFOLLOW to refuse to follow any symlink at the leaf, + * and O_TRUNC over O_EXCL because we may rerun within the + * same workdir if the test is invoked twice. */ + log_fd = open(tftp_tftpd_log, + O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600); + if (log_fd >= 0) { + dup2(log_fd, STDOUT_FILENO); + dup2(log_fd, STDERR_FILENO); + close(log_fd); + } + execl(exe, "in.tftpd", + "-l", /* --listen: standalone, bind UDP ourselves */ + "-L", /* --foreground: do not detach (keeps our PID + * valid and stderr attached) */ + "-vvv", + "-u", "root", + "-a", addrport, + "-s", tftp_local_dir, + (char *)NULL); + perror("execl in.tftpd"); + _exit(127); + } + /* Parent: give the daemon a moment to bind, then check it is + * still alive. */ + tftpd_pid = pid; + usleep(500 * 1000); + if (waitpid(pid, NULL, WNOHANG) != 0) { + tftpd_pid = -1; + fprintf(stderr, "[client] in.tftpd exited early — see %s\n", + tftp_tftpd_log); + return -1; + } + /* tftpd-hpa logs everything to syslog (so the captured stderr will + * be empty even on success). Cross-check that the daemon is + * actually bound to the expected UDP port — this catches silent + * --listen / --address mistakes that otherwise look just like the + * "no reply" failure mode. */ + { + char check[160]; + snprintf(check, sizeof(check), + "ss -lnup 'sport = :%d' 2>/dev/null | tail -n +2 | grep -q .", + TFTP_INTEROP_PORT); + if (system(check) != 0) { + fprintf(stderr, + "[client] in.tftpd is running (pid %d) but nothing is " + "listening on UDP %s:%d — check syslog " + "(journalctl -t in.tftpd) and tftpd-hpa flags.\n", + (int)tftpd_pid, HOST_STACK_IP, TFTP_INTEROP_PORT); + } + } + return 0; +} + +static void dump_log_file(const char *label, const char *path) +{ + FILE *fp; + char line[512]; + + fp = fopen(path, "r"); + if (fp == NULL) + return; + fprintf(stderr, "----- %s (%s) -----\n", label, path); + while (fgets(line, sizeof(line), fp) != NULL) + fputs(line, stderr); + fprintf(stderr, "----- end %s -----\n", label); + fclose(fp); +} + +/* ---------- pump the wolfIP stack and the TFTP module ------------- */ + +static void pump_client(struct wolfIP *s, struct wolftftp_client *client, + int sock, int trace) +{ + uint8_t pkt[1500]; + struct wolfIP_sockaddr_in remote; + socklen_t rlen = sizeof(remote); + int n; + + (void)wolfIP_poll(s, now_ms()); + for (;;) { + rlen = sizeof(remote); + n = wolfIP_sock_recvfrom(s, sock, pkt, sizeof(pkt), 0, + (struct wolfIP_sockaddr *)&remote, &rlen); + if (n <= 0) + break; + { + struct wolftftp_endpoint rep; + rep.ip = ee32(remote.sin_addr.s_addr); + rep.port = ee16(remote.sin_port); + if (trace) + trace_packet("client", "RX", rep.ip, rep.port, pkt, n); + (void)wolftftp_client_receive(client, + TFTP_INTEROP_CLIENT_PORT, &rep, pkt, (uint16_t)n); + } + } + (void)wolftftp_client_poll(client, (uint32_t)now_ms()); +} + +static void pump_server(struct wolfIP *s, struct wolftftp_server *server, + int listen_sock, int transfer_sock, int trace) +{ + uint8_t pkt[1500]; + struct wolfIP_sockaddr_in remote; + socklen_t rlen; + int n; + int i; + int socks[2]; + uint16_t ports[2]; + + socks[0] = listen_sock; + socks[1] = transfer_sock; + ports[0] = TFTP_INTEROP_PORT; + ports[1] = TFTP_INTEROP_TRANSFER_PORT; + + (void)wolfIP_poll(s, now_ms()); + for (i = 0; i < 2; i++) { + for (;;) { + rlen = sizeof(remote); + n = wolfIP_sock_recvfrom(s, socks[i], pkt, sizeof(pkt), 0, + (struct wolfIP_sockaddr *)&remote, &rlen); + if (n <= 0) + break; + { + struct wolftftp_endpoint rep; + rep.ip = ee32(remote.sin_addr.s_addr); + rep.port = ee16(remote.sin_port); + if (trace) + trace_packet("server", "RX", rep.ip, rep.port, pkt, n); + (void)wolftftp_server_receive(server, ports[i], &rep, + pkt, (uint16_t)n); + } + } + } + (void)wolftftp_server_poll(server, (uint32_t)now_ms()); +} + +/* ---------- client direction: wolfIP client vs in.tftpd ----------- */ + +static int run_client_test(struct wolfIP *s) +{ + struct client_glue glue; + struct tftp_file_ctx file_ctx; + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_transfer_cfg cfg; + struct wolftftp_client client; + struct wolftftp_endpoint srv; + struct wolfIP_sockaddr_in bind_addr; + int sock; + int ret; + uint64_t deadline; + + if (!file_present("/usr/sbin/in.tftpd")) { + fprintf(stderr, "[client] skipping: /usr/sbin/in.tftpd not found\n"); + return TFTP_EXIT_SKIP; + } + + /* tftp_local_dir is created once by tftp_workdir_setup() with + * mode 0700; we only need to (re)create the fixture file. */ + if (write_fixture(tftp_fixture_path) != 0) { + fprintf(stderr, "[client] cannot create fixture %s\n", + tftp_fixture_path); + return TFTP_EXIT_FAIL; + } + (void)unlink(tftp_download_path); + + if (tftpd_start() != 0) { + fprintf(stderr, "[client] failed to launch in.tftpd\n"); + return TFTP_EXIT_FAIL; + } + + sock = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, 0); + if (sock < 0) { + tftpd_stop(); + return TFTP_EXIT_FAIL; + } + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = ee16(TFTP_INTEROP_CLIENT_PORT); + bind_addr.sin_addr.s_addr = 0; + if (wolfIP_sock_bind(s, sock, (struct wolfIP_sockaddr *)&bind_addr, + sizeof(bind_addr)) < 0) { + fprintf(stderr, "[client] wolfIP UDP bind failed\n"); + wolfIP_sock_close(s, sock); + tftpd_stop(); + return TFTP_EXIT_FAIL; + } + + memset(&file_ctx, 0, sizeof(file_ctx)); + file_ctx.path = tftp_download_path; + memset(&glue, 0, sizeof(glue)); + glue.s = s; + glue.sock = sock; + glue.trace = 1; + memset(&transport, 0, sizeof(transport)); + transport.send = client_send; + transport.arg = &glue; + memset(&io, 0, sizeof(io)); + io.open = io_open; + io.write = io_write; + io.close = io_close; + io.arg = &file_ctx; + /* All-defaults cfg keeps the RRQ option-free; some tftpd-hpa + * builds only enable a subset of options and reject the whole + * request with EBADOPT (code 8) if any unexpected option appears. */ + memset(&cfg, 0, sizeof(cfg)); + cfg.local_port = TFTP_INTEROP_CLIENT_PORT; + cfg.blksize = WOLFTFTP_DEFAULT_BLKSIZE; + cfg.timeout_s = WOLFTFTP_DEFAULT_TIMEOUT_S; + cfg.windowsize = 1; + cfg.max_retries = 5; + + wolftftp_client_init(&client, &transport, &io, &cfg); + memset(&srv, 0, sizeof(srv)); + srv.ip = atoip4(HOST_STACK_IP); + srv.port = TFTP_INTEROP_PORT; + ret = wolftftp_client_start_rrq(&client, &srv, TFTP_INTEROP_REMOTE_NAME); + if (ret != 0) { + fprintf(stderr, "[client] start_rrq failed: %d\n", ret); + wolfIP_sock_close(s, sock); + tftpd_stop(); + return TFTP_EXIT_FAIL; + } + + deadline = now_ms() + TFTP_INTEROP_TIMEOUT_MS; + while (client.state != WOLFTFTP_CLIENT_COMPLETE && + client.state != WOLFTFTP_CLIENT_ERROR && + now_ms() < deadline) { + pump_client(s, &client, sock, glue.trace); + usleep(2000); + } + + wolfIP_sock_close(s, sock); + tftpd_stop(); + + if (client.state != WOLFTFTP_CLIENT_COMPLETE) { + fprintf(stderr, "[client] transfer did not complete (state=%d, " + "status=%d)\n", client.state, client.last_status); + dump_log_file("in.tftpd", tftp_tftpd_log); + return TFTP_EXIT_FAIL; + } + if (!file_equal(tftp_fixture_path, tftp_download_path)) { + fprintf(stderr, "[client] downloaded contents diverge from fixture\n"); + return TFTP_EXIT_FAIL; + } + printf("[client] wolfIP client successfully fetched %u bytes from " + "tftpd-hpa\n", TFTP_INTEROP_FIXTURE_SIZE); + return TFTP_EXIT_SUCCESS; +} + +/* ---------- server direction: tftp-hpa client vs wolfIP server ---- */ + +static volatile int server_close_calls = 0; +static volatile int server_close_status = 0; + +static void server_io_close(void *arg, void *handle, int status) +{ + (void)arg; + if (handle != NULL) + fclose((FILE *)handle); + server_close_status = status; + server_close_calls++; +} + +static int run_server_test(struct wolfIP *s) +{ + struct server_glue glue; + struct tftp_file_ctx file_ctx; + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_transfer_cfg cfg; + struct wolftftp_server server; + struct wolfIP_sockaddr_in bind_addr; + int listen_sock; + int transfer_sock; + pid_t tftp_pid; + int wstatus; + int rc; + uint64_t deadline; + + if (!file_present("/usr/bin/tftp")) { + fprintf(stderr, "[server] skipping: /usr/bin/tftp not found\n"); + return TFTP_EXIT_SKIP; + } + + if (write_fixture(tftp_fixture_path) != 0) + return TFTP_EXIT_FAIL; + (void)unlink(tftp_host_get_path); + + listen_sock = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, 0); + transfer_sock = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, 0); + if (listen_sock < 0 || transfer_sock < 0) { + fprintf(stderr, "[server] socket() failed\n"); + return TFTP_EXIT_FAIL; + } + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_addr.s_addr = 0; + bind_addr.sin_port = ee16(TFTP_INTEROP_PORT); + if (wolfIP_sock_bind(s, listen_sock, (struct wolfIP_sockaddr *)&bind_addr, + sizeof(bind_addr)) < 0) { + fprintf(stderr, "[server] bind listen failed\n"); + return TFTP_EXIT_FAIL; + } + bind_addr.sin_port = ee16(TFTP_INTEROP_TRANSFER_PORT); + if (wolfIP_sock_bind(s, transfer_sock, (struct wolfIP_sockaddr *)&bind_addr, + sizeof(bind_addr)) < 0) { + fprintf(stderr, "[server] bind transfer failed\n"); + return TFTP_EXIT_FAIL; + } + + memset(&file_ctx, 0, sizeof(file_ctx)); + file_ctx.path = tftp_fixture_path; + memset(&glue, 0, sizeof(glue)); + glue.s = s; + glue.listen_sock = listen_sock; + glue.transfer_sock = transfer_sock; + glue.trace = 1; + memset(&transport, 0, sizeof(transport)); + transport.send = server_send; + transport.arg = &glue; + memset(&io, 0, sizeof(io)); + io.open = io_open; + io.read = io_read; + io.write = io_write; + io.close = server_io_close; + io.arg = &file_ctx; + memset(&cfg, 0, sizeof(cfg)); + cfg.blksize = WOLFTFTP_DEFAULT_BLKSIZE; + cfg.timeout_s = 2; + cfg.windowsize = 1; + cfg.max_retries = 5; + + wolftftp_server_init(&server, &transport, &io, &cfg); + server.listen_port = TFTP_INTEROP_PORT; + server.transfer_port_base = TFTP_INTEROP_TRANSFER_PORT; + + server_close_calls = 0; + server_close_status = 0; + + /* Linux tftp-hpa client is driven via -c get: it issues an RRQ + * for the fixture and saves it to tftp_host_get_path. */ + tftp_pid = fork(); + if (tftp_pid < 0) { + return TFTP_EXIT_FAIL; + } + if (tftp_pid == 0) { + /* The default mode of tftp-hpa is "netascii", which the wolfIP + * server rejects with EBADOPT — force binary so the request + * uses "octet". Stderr is captured so the post-mortem can show + * exactly what the host client saw. */ + char port[8]; + int log_fd; + + snprintf(port, sizeof(port), "%d", TFTP_INTEROP_PORT); + log_fd = open(tftp_tftp_log, + O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600); + if (log_fd >= 0) { + dup2(log_fd, STDOUT_FILENO); + dup2(log_fd, STDERR_FILENO); + close(log_fd); + } + execl("/usr/bin/tftp", "tftp", WOLFIP_IP, port, + "-m", "binary", + "-v", + "-c", "get", TFTP_INTEROP_REMOTE_NAME, + tftp_host_get_path, + (char *)NULL); + _exit(127); + } + + deadline = now_ms() + TFTP_INTEROP_TIMEOUT_MS; + while (server_close_calls == 0 && now_ms() < deadline) { + pump_server(s, &server, listen_sock, transfer_sock, glue.trace); + usleep(2000); + } + /* Keep pumping briefly so any final ACK is flushed before we tear + * down the sockets (which would otherwise make the host client + * report a transient error even on a successful transfer). */ + { + uint64_t flush_end = now_ms() + 250U; + while (now_ms() < flush_end) { + pump_server(s, &server, listen_sock, transfer_sock, glue.trace); + usleep(2000); + } + } + + if (waitpid(tftp_pid, &wstatus, WNOHANG) == 0) { + /* Client may still be exiting normally; give it a moment. */ + usleep(500 * 1000); + if (waitpid(tftp_pid, &wstatus, WNOHANG) == 0) { + kill(tftp_pid, SIGTERM); + waitpid(tftp_pid, &wstatus, 0); + } + } + + wolfIP_sock_close(s, transfer_sock); + wolfIP_sock_close(s, listen_sock); + + if (server_close_calls == 0) { + fprintf(stderr, "[server] wolfIP server session never closed\n"); + dump_log_file("tftp client", tftp_tftp_log); + return TFTP_EXIT_FAIL; + } + if (server_close_status != 0) { + fprintf(stderr, "[server] session close status %d\n", + server_close_status); + dump_log_file("tftp client", tftp_tftp_log); + return TFTP_EXIT_FAIL; + } + rc = file_equal(tftp_fixture_path, tftp_host_get_path); + if (!rc) { + fprintf(stderr, "[server] host-side download diverges from fixture\n"); + dump_log_file("tftp client", tftp_tftp_log); + return TFTP_EXIT_FAIL; + } + printf("[server] tftp-hpa client successfully fetched %u bytes from " + "wolfIP server\n", TFTP_INTEROP_FIXTURE_SIZE); + return TFTP_EXIT_SUCCESS; +} + +/* ---------- driver ------------------------------------------------ */ + +static int setup_stack(struct wolfIP **out_s, struct wolfIP_ll_dev **out_dev) +{ + struct wolfIP *s; + struct wolfIP_ll_dev *tapdev; + struct in_addr host_stack_ip; + char cmd[160]; + + wolfIP_init_static(&s); + tapdev = wolfIP_getdev(s); + if (tapdev == NULL) + return -1; + inet_aton(HOST_STACK_IP, &host_stack_ip); + if (tap_init(tapdev, "wtftp0", host_stack_ip.s_addr) < 0) { + perror("tap_init"); + return -1; + } + wolfIP_ipconfig_set(s, atoip4(WOLFIP_IP), atoip4("255.255.255.0"), + atoip4(HOST_STACK_IP)); + + /* Drop a pcap on disk for post-mortem inspection. */ + snprintf(cmd, sizeof(cmd), + "tcpdump -i %s -w /tmp/wolfip-tftp.pcap " + "-U >/dev/null 2>&1 &", tapdev->ifname); + (void)system(cmd); + usleep(200 * 1000); + + *out_s = s; + *out_dev = tapdev; + return 0; +} + +int main(int argc, char **argv) +{ + struct wolfIP *s = NULL; + struct wolfIP_ll_dev *dev = NULL; + const char *mode = "all"; + int rc_client = TFTP_EXIT_SUCCESS; + int rc_server = TFTP_EXIT_SUCCESS; + + if (argc > 1) + mode = argv[1]; + + if (geteuid() != 0) { + fprintf(stderr, "test_tftp_interop: requires root to set up the " + "TAP link; skipping\n"); + return TFTP_EXIT_SKIP; + } + + if (tftp_workdir_setup() != 0) { + perror("test_tftp_interop: mkdtemp"); + return TFTP_EXIT_FAIL; + } + + if (setup_stack(&s, &dev) != 0) { + tftp_workdir_teardown(); + return TFTP_EXIT_FAIL; + } + (void)dev; + + /* Give ARP a chance to resolve the host before the first transfer. */ + { + uint64_t end = now_ms() + 250U; + while (now_ms() < end) { + (void)wolfIP_poll(s, now_ms()); + usleep(2000); + } + } + + if (strcmp(mode, "client") == 0 || strcmp(mode, "all") == 0) + rc_client = run_client_test(s); + if (strcmp(mode, "server") == 0 || strcmp(mode, "all") == 0) + rc_server = run_server_test(s); + + tftpd_stop(); + (void)system("pkill -INT -f 'tcpdump -i wtftp0' >/dev/null 2>&1"); + usleep(200 * 1000); /* let tcpdump flush */ + + if (rc_client == TFTP_EXIT_FAIL || rc_server == TFTP_EXIT_FAIL) { + /* Republish the workdir-private logs at the well-known paths + * the CI artifact uploader expects, then tear the workdir + * down. The pcap was always written at the well-known path + * because tcpdump runs out-of-process. */ + tftp_workdir_publish_diagnostics(); + fprintf(stderr, + "Diagnostics: " TFTP_INTEROP_DIAG_PCAP ", " + TFTP_INTEROP_DIAG_TFTPD ", " TFTP_INTEROP_DIAG_TFTP "\n"); + fprintf(stderr, "----- wire summary (tcpdump -nn -r) -----\n"); + fflush(stderr); + (void)system("tcpdump -nn -tt -r " TFTP_INTEROP_DIAG_PCAP " 2>&1 " + "| sed 's/^/ /' >&2"); + fprintf(stderr, "----- end wire summary -----\n"); + if (file_present("/usr/bin/journalctl")) { + fprintf(stderr, "----- last 20 in.tftpd syslog lines -----\n"); + fflush(stderr); + (void)system("journalctl -t in.tftpd -n 20 --no-pager 2>&1 " + "| sed 's/^/ /' >&2"); + fprintf(stderr, "----- end syslog -----\n"); + } + tftp_workdir_teardown(); + return TFTP_EXIT_FAIL; + } + tftp_workdir_teardown(); + if (rc_client == TFTP_EXIT_SKIP && rc_server == TFTP_EXIT_SKIP) + return TFTP_EXIT_SKIP; + return TFTP_EXIT_SUCCESS; +} diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index cc95d33..da0b410 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -6,6 +6,7 @@ #include "unit_tests_tcp_flow.c" #include "unit_tests_proto.c" #include "unit_tests_multicast.c" +#include "unit_tests_tftp.c" Suite *wolf_suite(void) { @@ -390,6 +391,12 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_udp_try_recv_len_below_header_rejected); tcase_add_test(tc_utils, test_udp_try_recv_conf_null); tcase_add_test(tc_utils, test_udp_try_recv_remote_ip_matches_local_ip); + tcase_add_test(tc_utils, + test_udp_try_recv_unconnected_accepts_any_peer_port); + tcase_add_test(tc_utils, test_udp_try_recv_connected_filters_peer_port); + tcase_add_test(tc_utils, test_udp_sock_connect_sets_connected_flag); + tcase_add_test(tc_utils, + test_udp_sock_connect_failed_validation_leaves_socket_unconnected); tcase_add_test(tc_utils, test_udp_try_recv_unmatched_port_sends_icmp_unreachable); tcase_add_test(tc_utils, test_udp_try_recv_unmatched_nonlocal_dst_does_not_send_icmp); tcase_add_test(tc_utils, test_udp_try_recv_full_fifo_drop_does_not_set_readable_or_send_icmp); @@ -870,6 +877,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_regression_dns_id_never_zero); tcase_add_test(tc_proto, test_tcp_input_listen_synack_sends_rst_and_stays_listen); tcase_add_test(tc_proto, test_tcp_input_listen_accept_final_ack_does_not_send_rst); + add_tftp_tests(tc_proto); tcase_add_test(tc_utils, test_transport_checksum); tcase_add_test(tc_utils, test_iphdr_set_checksum); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index cb7f0b9..53ada60 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -36,6 +36,8 @@ #ifndef WOLFIP_ENABLE_FORWARDING #define WOLFIP_ENABLE_FORWARDING 1 #endif +#undef WOLFIP_ENABLE_TFTP +#define WOLFIP_ENABLE_TFTP 1 #if WOLFIP_ENABLE_LOOPBACK #define TEST_LOOPBACK_IF 0U #define TEST_PRIMARY_IF 1U @@ -47,6 +49,7 @@ #endif #include #include "../../wolfip.c" +#include "../../tftp/wolftftp.c" #include /* for random() */ #include "mocks/wolfssl/wolfcrypt/settings.h" #include "mocks/wolfssl/wolfcrypt/memory.h" diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 8ba650b..81d9aef 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -5073,6 +5073,13 @@ START_TEST(test_udp_try_recv_remote_ip_matches_local_ip) ts->src_port = 1234; ts->local_ip = local_ip; ts->remote_ip = local_ip; + /* The intent here is "verify the peer filter on a connected UDP + * socket". Since the POSIX-correct delivery path only enforces + * remote_ip/dst_port match when the socket has been connected, + * flip the flag manually (mirroring what wolfIP_sock_connect does) + * so the assertion below keeps testing the filter and not the + * unconnected (any-peer) path. */ + ts->sock.udp.connected = 1; memset(&udp, 0, sizeof(udp)); udp.ip.dst = ee32(local_ip); @@ -5084,6 +5091,192 @@ START_TEST(test_udp_try_recv_remote_ip_matches_local_ip) } END_TEST +/* Regression: an unconnected UDP socket must accept datagrams from + * any source, even after sendto() set the socket's dst_port to + * something specific. Prior wolfIP behaviour conflated "destination + * of last send" with "RX filter" and rejected the reply with ICMP + * port-unreachable when the peer answered from a different port — + * which is exactly what RFC 1350 TFTP does on the first DATA/OACK. */ +START_TEST(test_udp_try_recv_unconnected_accepts_any_peer_port) +{ + struct wolfIP s; + struct tsocket *ts; + /* Back the datagram with a real buffer that fits the full + * ETH+IP+UDP+payload frame; the delivery path memcpys frame_len + * bytes into the socket's rxbuf and would otherwise read past a + * bare struct wolfIP_udp_datagram. */ + uint8_t udp_buf[sizeof(struct wolfIP_udp_datagram) + 4]; + struct wolfIP_udp_datagram *udp = (struct wolfIP_udp_datagram *)udp_buf; + uint32_t local_ip = 0x0A000001U; + uint32_t peer_ip = 0x0A000002U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = udp_new_socket(&s); + ck_assert_ptr_nonnull(ts); + ts->src_port = 6989; + ts->local_ip = local_ip; + /* Mirror what sendto(peer_ip:6969) leaves behind: dst_port and + * remote_ip set, but the socket is NOT connected. */ + ts->dst_port = 6969; + ts->remote_ip = peer_ip; + ck_assert_uint_eq(ts->sock.udp.connected, 0U); + + /* Peer replies from a *different* source port (TFTP TID change). */ + memset(udp_buf, 0, sizeof(udp_buf)); + udp->ip.src = ee32(peer_ip); + udp->ip.dst = ee32(local_ip); + udp->ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + 4); + udp->src_port = ee16(57722); + udp->dst_port = ee16(6989); + udp->len = ee16(UDP_HEADER_LEN + 4); + udp_try_recv(&s, TEST_PRIMARY_IF, udp, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); + ck_assert_ptr_nonnull(fifo_peek(&ts->sock.udp.rxbuf)); +} +END_TEST + +/* Companion regression: a CONNECTED UDP socket must continue to filter + * out datagrams from any peer other than the one it was connected to. + * If this contract regresses, applications that use connect() for UDP + * (e.g. DNS clients tied to a single resolver) would start delivering + * forged responses from arbitrary sources. */ +START_TEST(test_udp_try_recv_connected_filters_peer_port) +{ + struct wolfIP s; + struct tsocket *ts; + /* Sized to hold the full frame so the accepted-peer assertion at + * the end does not memcpy past the stack buffer. */ + uint8_t udp_buf[sizeof(struct wolfIP_udp_datagram) + 4]; + struct wolfIP_udp_datagram *udp = (struct wolfIP_udp_datagram *)udp_buf; + uint32_t local_ip = 0x0A000001U; + uint32_t peer_ip = 0x0A000002U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = udp_new_socket(&s); + ck_assert_ptr_nonnull(ts); + ts->src_port = 6989; + ts->local_ip = local_ip; + ts->dst_port = 6969; + ts->remote_ip = peer_ip; + ts->sock.udp.connected = 1; + + /* Same peer IP but a foreign source port (53000) must be rejected. */ + memset(udp_buf, 0, sizeof(udp_buf)); + udp->ip.src = ee32(peer_ip); + udp->ip.dst = ee32(local_ip); + udp->ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + 4); + udp->src_port = ee16(53000); + udp->dst_port = ee16(6989); + udp->len = ee16(UDP_HEADER_LEN + 4); + udp_try_recv(&s, TEST_PRIMARY_IF, udp, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); + ck_assert_ptr_eq(fifo_peek(&ts->sock.udp.rxbuf), NULL); + + /* The connected peer (peer_ip:6969) must still be accepted. */ + udp->src_port = ee16(6969); + udp_try_recv(&s, TEST_PRIMARY_IF, udp, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); + ck_assert_ptr_nonnull(fifo_peek(&ts->sock.udp.rxbuf)); +} +END_TEST + +/* Regression: wolfIP_sock_connect() must flip the UDP socket into the + * "connected" state so the RX filter starts honouring dst_port / + * remote_ip. Pin the public-API path so a future refactor that forgot + * to flip the flag would surface here. */ +START_TEST(test_udp_sock_connect_sets_connected_flag) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in remote; + struct tsocket *ts; + int fd; + int rc; + uint32_t local_ip = 0x0A000001U; + uint32_t peer_ip = 0x0A000002U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + fd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, 0); + ck_assert_int_ge(fd, 0); + ts = &s.udpsockets[SOCKET_UNMARK(fd)]; + ck_assert_uint_eq(ts->sock.udp.connected, 0U); + + memset(&remote, 0, sizeof(remote)); + remote.sin_family = AF_INET; + remote.sin_port = ee16(6969); + remote.sin_addr.s_addr = ee32(peer_ip); + rc = wolfIP_sock_connect(&s, fd, (struct wolfIP_sockaddr *)&remote, + sizeof(remote)); + ck_assert_int_eq(rc, 0); + ck_assert_uint_eq(ts->sock.udp.connected, 1U); + ck_assert_uint_eq(ts->dst_port, 6969U); + ck_assert_uint_eq(ts->remote_ip, peer_ip); + + wolfIP_sock_close(&s, fd); +} +END_TEST + +/* Regression: wolfIP_sock_connect() must NOT leave the UDP socket + * half-connected when validation (e.g. bound_local_ip not bound to + * any current interface) fails. Otherwise the peer RX filter would + * activate against the failed-connect peer and drop legitimate + * datagrams arriving on an unconnected socket. */ +START_TEST(test_udp_sock_connect_failed_validation_leaves_socket_unconnected) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in bind_addr; + struct wolfIP_sockaddr_in remote; + struct tsocket *ts; + int fd; + int rc; + uint32_t local_ip = 0x0A000001U; + uint32_t bogus_ip = 0xC0A80001U; /* 192.168.0.1, not configured */ + uint32_t peer_ip = 0x0A000002U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + fd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, 0); + ck_assert_int_ge(fd, 0); + ts = &s.udpsockets[SOCKET_UNMARK(fd)]; + + /* Pin a bound_local_ip that is *not* configured on any interface + * so the bound_match check inside connect() must fail. */ + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = ee16(1234); + bind_addr.sin_addr.s_addr = ee32(bogus_ip); + /* Force bound_local_ip without going through bind() (which would + * reject the unknown address); we want to specifically exercise + * the post-bind connect() validation path. */ + ts->bound_local_ip = bogus_ip; + ts->src_port = 1234; + + memset(&remote, 0, sizeof(remote)); + remote.sin_family = AF_INET; + remote.sin_port = ee16(6969); + remote.sin_addr.s_addr = ee32(peer_ip); + rc = wolfIP_sock_connect(&s, fd, (struct wolfIP_sockaddr *)&remote, + sizeof(remote)); + ck_assert_int_eq(rc, -WOLFIP_EINVAL); + /* None of the persistent state should reflect the attempted peer. */ + ck_assert_uint_eq(ts->sock.udp.connected, 0U); + ck_assert_uint_eq(ts->dst_port, 0U); + ck_assert_uint_eq(ts->remote_ip, 0U); + + wolfIP_sock_close(&s, fd); +} +END_TEST + START_TEST(test_udp_try_recv_dhcp_running_local_zero) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_tftp.c b/src/test/unit/unit_tests_tftp.c new file mode 100644 index 0000000..9531dc8 --- /dev/null +++ b/src/test/unit/unit_tests_tftp.c @@ -0,0 +1,2084 @@ +/* unit_tests_tftp.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +struct tftp_test_ctx { + uint8_t sent[32][WOLFTFTP_PKT_MAX]; + uint16_t sent_len[32]; + uint16_t sent_local_port[32]; + struct wolftftp_endpoint sent_remote[32]; + int send_calls; + int send_fail; + + int open_calls; + int write_calls; + int read_calls; + int hash_calls; + int verify_calls; + int close_calls; + int close_status; + + int open_fail; + int write_fail; + int read_fail; + int hash_fail; + int verify_fail; + + void *handle_out; + uint32_t open_size_hint; + int open_is_write; + char opened_name[WOLFTFTP_MAX_FILENAME]; + + uint8_t read_data[WOLFTFTP_MAX_BLKSIZE * 4]; + uint16_t read_len[8]; + int read_last[8]; + int read_count; + + uint8_t write_buf[WOLFTFTP_MAX_BLKSIZE * 4]; + uint32_t write_offset; + uint16_t write_len; +}; + +static void tftp_test_ctx_reset(struct tftp_test_ctx *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->handle_out = ctx; +} + +static int tftp_test_send(void *arg, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len) +{ + struct tftp_test_ctx *ctx = (struct tftp_test_ctx *)arg; + int idx = ctx->send_calls; + + if (ctx->send_fail != 0) + return ctx->send_fail; + ck_assert_int_lt(idx, 32); + memcpy(ctx->sent[idx], buf, len); + ctx->sent_len[idx] = len; + ctx->sent_local_port[idx] = local_port; + ctx->sent_remote[idx] = *remote; + ctx->send_calls++; + return 0; +} + +static int tftp_test_open(void *arg, const char *name, int is_write, + uint32_t *size_hint, void **handle) +{ + struct tftp_test_ctx *ctx = (struct tftp_test_ctx *)arg; + + ctx->open_calls++; + ctx->open_is_write = is_write; + ctx->open_size_hint = size_hint != NULL ? *size_hint : 0; + (void)wolftftp_copy_string(ctx->opened_name, sizeof(ctx->opened_name), name); + if (ctx->open_fail != 0) + return ctx->open_fail; + if (handle != NULL) + *handle = ctx->handle_out; + if (!is_write && size_hint != NULL && *size_hint == 0) + *size_hint = 7; + return 0; +} + +static int tftp_test_read(void *arg, void *handle, uint32_t offset, + uint8_t *buf, uint16_t max_len, uint16_t *out_len, int *is_last) +{ + struct tftp_test_ctx *ctx = (struct tftp_test_ctx *)arg; + uint16_t len; + int idx = ctx->read_calls++; + + (void)handle; + if (ctx->read_fail != 0) + return ctx->read_fail; + ck_assert_int_lt(idx, 8); + len = ctx->read_len[idx]; + ck_assert_uint_le(len, max_len); + memcpy(buf, ctx->read_data + offset, len); + *out_len = len; + *is_last = ctx->read_last[idx]; + return 0; +} + +static int tftp_test_write(void *arg, void *handle, uint32_t offset, + const uint8_t *buf, uint16_t len) +{ + struct tftp_test_ctx *ctx = (struct tftp_test_ctx *)arg; + + (void)handle; + if (ctx->write_fail != 0) + return ctx->write_fail; + ctx->write_calls++; + ctx->write_offset = offset; + ctx->write_len = len; + memcpy(ctx->write_buf + offset, buf, len); + return 0; +} + +static int tftp_test_hash(void *arg, void *handle, const uint8_t *buf, + uint16_t len) +{ + struct tftp_test_ctx *ctx = (struct tftp_test_ctx *)arg; + + (void)handle; + (void)buf; + (void)len; + ctx->hash_calls++; + return ctx->hash_fail; +} + +static int tftp_test_verify(void *arg, void *handle, uint32_t total_size) +{ + struct tftp_test_ctx *ctx = (struct tftp_test_ctx *)arg; + + (void)handle; + ctx->verify_calls++; + ctx->open_size_hint = total_size; + return ctx->verify_fail; +} + +static void tftp_test_close(void *arg, void *handle, int status) +{ + struct tftp_test_ctx *ctx = (struct tftp_test_ctx *)arg; + + (void)handle; + ctx->close_calls++; + ctx->close_status = status; +} + +static struct wolftftp_transport_ops tftp_transport_ops(struct tftp_test_ctx *ctx) +{ + struct wolftftp_transport_ops ops; + + memset(&ops, 0, sizeof(ops)); + ops.send = tftp_test_send; + ops.arg = ctx; + return ops; +} + +static struct wolftftp_io_ops tftp_io_ops(struct tftp_test_ctx *ctx) +{ + struct wolftftp_io_ops ops; + + memset(&ops, 0, sizeof(ops)); + ops.open = tftp_test_open; + ops.read = tftp_test_read; + ops.write = tftp_test_write; + ops.hash_update = tftp_test_hash; + ops.verify = tftp_test_verify; + ops.close = tftp_test_close; + ops.arg = ctx; + return ops; +} + +static struct wolftftp_transfer_cfg tftp_cfg_defaults(void) +{ + struct wolftftp_transfer_cfg cfg; + + memset(&cfg, 0, sizeof(cfg)); + cfg.local_port = 12000; + cfg.blksize = 16; + cfg.timeout_s = 2; + cfg.windowsize = 2; + cfg.max_retries = 3; + cfg.max_image_size = 128; + return cfg; +} + +static struct wolftftp_endpoint tftp_remote(uint32_t ip, uint16_t port) +{ + struct wolftftp_endpoint ep; + + ep.ip = ip; + ep.port = port; + return ep; +} + +START_TEST(test_tftp_helpers_and_builders) +{ + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t out_len = 0; + uint8_t requested = 0; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_parsed_req req; + struct wolftftp_negotiated neg; + struct wolftftp_parsed_data data; + + uint32_t parsed = 0xDEADBEEFU; + + ck_assert_int_eq(wolftftp_stricmp_local("octet", "OCTET"), 0); + ck_assert_int_eq(wolftftp_parse_u32("42", 100, &parsed), 0); + ck_assert_uint_eq(parsed, 42U); + parsed = 0xDEADBEEFU; + ck_assert_int_eq(wolftftp_parse_u32("999", 10, &parsed), -1); + ck_assert_uint_eq(parsed, 0xDEADBEEFU); + /* Valid zero is distinguishable from invalid input. */ + parsed = 0xDEADBEEFU; + ck_assert_int_eq(wolftftp_parse_u32("0", 100, &parsed), 0); + ck_assert_uint_eq(parsed, 0U); + parsed = 0xDEADBEEFU; + ck_assert_int_eq(wolftftp_parse_u32("abc", 100, &parsed), -1); + ck_assert_uint_eq(parsed, 0xDEADBEEFU); + ck_assert_int_eq(wolftftp_parse_u32(NULL, 100, &parsed), -1); + ck_assert_int_eq(wolftftp_parse_u32("", 100, &parsed), -1); + ck_assert_int_eq(wolftftp_parse_u32("12", 100, NULL), -1); + ck_assert_int_eq(wolftftp_copy_string(NULL, 0, "x"), -WOLFIP_EINVAL); + + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &requested, &out_len), 0); + ck_assert_uint_eq(wolftftp_read_u16(pkt), WOLFTFTP_OP_RRQ); + ck_assert(requested != 0U); + ck_assert_int_eq(wolftftp_parse_request(pkt, out_len, &req), 0); + ck_assert_str_eq(req.filename, "fw.bin"); + ck_assert_uint_eq(req.blksize, cfg.blksize); + ck_assert_uint_eq(req.timeout_s, cfg.timeout_s); + ck_assert_uint_eq(req.windowsize, cfg.windowsize); + + wolftftp_neg_defaults(&neg, &cfg); + neg.tsize = 33; + neg.have_tsize = 1; + ck_assert_int_gt(wolftftp_build_oack(pkt, sizeof(pkt), &neg, + WOLFTFTP_OPT_BLKSIZE | WOLFTFTP_OPT_TIMEOUT | + WOLFTFTP_OPT_TSIZE | WOLFTFTP_OPT_WINDOWSIZE), 0); + ck_assert_int_eq(wolftftp_parse_oack(pkt, (uint16_t)strlen((char *)(pkt + 2)) + 14, + &neg), WOLFTFTP_ERR_PACKET); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, (uint16_t)(2 + strlen("blksize") + 1 + + strlen("16") + 1 + strlen("timeout") + 1 + strlen("2") + 1 + + strlen("tsize") + 1 + strlen("33") + 1 + strlen("windowsize") + 1 + + strlen("2") + 1), &neg), 0); + ck_assert_uint_eq(neg.tsize, 33U); + ck_assert_uint_eq(neg.have_tsize, 1U); + + memcpy(pkt + 4, "abc", 3); + ck_assert_int_eq(wolftftp_build_data(pkt, sizeof(pkt), 7, pkt + 4, 3), 7); + ck_assert_int_eq(wolftftp_parse_data(pkt, 7, &data), 0); + ck_assert_uint_eq(data.block, 7U); + ck_assert_uint_eq(data.data_len, 3U); + ck_assert_mem_eq(data.data, "abc", 3); + + ck_assert_int_eq(wolftftp_build_error(pkt, 7, WOLFTFTP_EBADOP, "x"), 6); + ck_assert_int_eq(wolftftp_packet_opcode(pkt, 6), WOLFTFTP_OP_ERROR); + ck_assert_int_eq(wolftftp_packet_opcode(NULL, 0), -1); + ck_assert_uint_eq(wolftftp_deadline(&neg, 10), 2010U); +} +END_TEST + +START_TEST(test_tftp_parse_request_error_paths) +{ + uint8_t pkt[64]; + struct wolftftp_parsed_req req; + struct wolftftp_negotiated neg; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0netascii\0", 12); + ck_assert_int_eq(wolftftp_parse_request(pkt, 14, &req), + WOLFTFTP_ERR_UNSUPPORTED); + + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0octet\0blksize\01\0", 19); + ck_assert_int_eq(wolftftp_parse_request(pkt, 19, &req), + WOLFTFTP_ERR_PACKET); + + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0octet\0mystery\01\0", 19); + ck_assert_int_eq(wolftftp_parse_request(pkt, 19, &req), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_parse_request(pkt, 3, &req), + WOLFTFTP_ERR_PACKET); + + /* Option value not NUL-terminated within the datagram. The bytes + * after the would-be-NUL are intentionally non-zero so that any + * regression to a value-side `>` (instead of `>=`) bounds check + * would let parse_u32 walk into them. The whole frame must be + * rejected as malformed without reading past buf+len. */ + memset(pkt, 0xFF, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0octet\0blksize\0", 17); + memcpy(pkt + 19, "512", 3); /* deliberately no trailing NUL */ + ck_assert_int_eq(wolftftp_parse_request(pkt, 22, &req), + WOLFTFTP_ERR_PACKET); + + /* Same shape on the OACK side: the value runs right up to len + * with no NUL. Must be rejected. */ + memset(pkt, 0xFF, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, "blksize\0", 8); + memcpy(pkt + 10, "512", 3); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 13, &neg), + WOLFTFTP_ERR_PACKET); +} +END_TEST + +START_TEST(test_tftp_client_rrq_oack_and_data_success) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000001U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 1069); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ck_assert_int_eq(ctx.send_calls, 1); + ck_assert_uint_eq(client.state, WOLFTFTP_CLIENT_WAIT_FIRST); + + len = wolftftp_build_oack(pkt, sizeof(pkt), &client.neg, + WOLFTFTP_OPT_BLKSIZE | WOLFTFTP_OPT_TIMEOUT | WOLFTFTP_OPT_WINDOWSIZE); + ck_assert_int_gt(len, 0); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_int_eq(ctx.send_calls, 2); + ck_assert_uint_eq(client.server.port, 1069U); + + memcpy(pkt + 4, "abcdefghijklmnop", 16); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 16); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_int_eq(ctx.write_calls, 1); + ck_assert_int_eq(ctx.hash_calls, 1); + ck_assert_int_eq(ctx.send_calls, 2); + + memcpy(pkt + 4, "end", 3); + len = wolftftp_build_data(pkt, sizeof(pkt), 2, pkt + 4, 3); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_int_eq(ctx.send_calls, 3); + ck_assert_int_eq(ctx.verify_calls, 1); + ck_assert_int_eq(ctx.close_calls, 1); + ck_assert_int_eq(ctx.close_status, 0); + ck_assert_uint_eq(client.state, WOLFTFTP_CLIENT_COMPLETE); + ck_assert_mem_eq(ctx.write_buf, "abcdefghijklmnopend", 19); +} +END_TEST + +START_TEST(test_tftp_client_fallback_duplicate_and_tid_errors) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000002U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 2000); + struct wolftftp_endpoint bad_tid = tftp_remote(srv.ip, 2001); + struct wolftftp_endpoint other_ip = tftp_remote(0x0A000099U, 2000); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + + memcpy(pkt + 4, "1234567890123456", 16); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 16); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_uint_eq(client.server.port, 2000U); + + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_int_eq(ctx.send_calls, 2); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &bad_tid, pkt, (uint16_t)len), WOLFTFTP_ERR_TID); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &other_ip, pkt, (uint16_t)len), 0); +} +END_TEST + +START_TEST(test_tftp_client_error_and_failure_paths) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000003U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 3000); + struct wolftftp_transfer_cfg cfg2; + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + cfg.max_image_size = 4; + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + + wolftftp_neg_defaults(&client.neg, &cfg); + client.neg.tsize = 9; + client.neg.have_tsize = 1; + len = wolftftp_build_oack(pkt, sizeof(pkt), &client.neg, WOLFTFTP_OPT_TSIZE); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), WOLFTFTP_ERR_SIZE); + ck_assert_uint_eq(client.state, WOLFTFTP_CLIENT_ERROR); + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + cfg2 = tftp_cfg_defaults(); + wolftftp_client_init(&client, &transport, &io, &cfg2); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ctx.open_fail = -1; + memcpy(pkt + 4, "end", 3); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 3); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), WOLFTFTP_ERR_IO); + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + cfg2 = tftp_cfg_defaults(); + wolftftp_client_init(&client, &transport, &io, &cfg2); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ctx.hash_fail = -1; + memcpy(pkt + 4, "end", 3); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 3); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), WOLFTFTP_ERR_VERIFY); + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + cfg2 = tftp_cfg_defaults(); + wolftftp_client_init(&client, &transport, &io, &cfg2); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ctx.verify_fail = -1; + memcpy(pkt + 4, "end", 3); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 3); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), WOLFTFTP_ERR_VERIFY); + + wolftftp_write_u16(pkt, WOLFTFTP_OP_ERROR); + wolftftp_write_u16(pkt + 2, WOLFTFTP_EUNDEF); + memcpy(pkt + 4, "x\0", 2); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, 6), WOLFTFTP_ERR_STATE); +} +END_TEST + +START_TEST(test_tftp_client_poll_and_status_paths) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000004U, 0); + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_status(NULL), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_client_poll(NULL, 0), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_client_poll(&client, 0), 0); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), + WOLFTFTP_ERR_STATE); + ck_assert_int_eq(wolftftp_client_poll(&client, 10), 0); + ck_assert_int_eq(wolftftp_client_poll(&client, 1000), 0); + ck_assert_int_eq(ctx.send_calls, 1); + ck_assert_int_eq(wolftftp_client_poll(&client, 3000), 0); + ck_assert_int_eq(ctx.send_calls, 2); + ck_assert_int_eq(wolftftp_client_poll(&client, 6000), 0); + ck_assert_int_eq(ctx.send_calls, 3); + ck_assert_int_eq(wolftftp_client_poll(&client, 12000), 0); + ck_assert_int_eq(ctx.send_calls, 4); + ck_assert_int_eq(wolftftp_client_poll(&client, 15000), WOLFTFTP_ERR_TIMEOUT); + ck_assert_int_eq(wolftftp_client_status(&client), WOLFTFTP_ERR_TIMEOUT); +} +END_TEST + +START_TEST(test_tftp_server_rrq_success_and_poll) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000011U, 4000); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + uint16_t ack0; + + tftp_test_ctx_reset(&ctx); + memcpy(ctx.read_data, "abcdefg", 7); + ctx.read_len[0] = 7; + ctx.read_last[0] = 1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(ctx.open_calls, 1); + ck_assert_int_eq(ctx.send_calls, 1); + ack0 = (uint16_t)wolftftp_build_ack(pkt, 0); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, ack0), 0); + ck_assert_int_eq(ctx.send_calls, 2); + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + wolftftp_write_u16(pkt + 2, 1); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, 4), 0); + ck_assert_int_eq(ctx.close_calls, 1); + + ck_assert_int_eq(wolftftp_server_poll(&server, 10), 0); + ck_assert_int_eq(wolftftp_server_poll(NULL, 10), -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_tftp_server_wrq_success_and_failures) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000012U, 5000); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + int len; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_WRQ, + "fw.bin", &cfg, 10, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(ctx.send_calls, 1); + memcpy(pkt + 4, "done", 4); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 4); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, (uint16_t)len), 0); + ck_assert_int_eq(ctx.write_calls, 1); + ck_assert_int_eq(ctx.hash_calls, 1); + ck_assert_int_eq(ctx.verify_calls, 1); + ck_assert_int_eq(ctx.close_calls, 1); + + tftp_test_ctx_reset(&ctx); + ctx.verify_fail = -1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_WRQ, + "fw.bin", &cfg, 10, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + memcpy(pkt + 4, "bad", 3); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 3); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, (uint16_t)len), WOLFTFTP_ERR_VERIFY); + + tftp_test_ctx_reset(&ctx); + cfg.max_image_size = 2; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_WRQ, + "fw.bin", &cfg, 10, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + memcpy(pkt + 4, "toolong", 7); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 7); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, (uint16_t)len), WOLFTFTP_ERR_SIZE); +} +END_TEST + +START_TEST(test_tftp_server_request_errors_and_timeouts) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000013U, 6000); + struct wolftftp_endpoint wrong_tid = tftp_remote(remote.ip, 6001); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + + memset(pkt, 0, 6); + wolftftp_write_u16(pkt, WOLFTFTP_OP_ERROR); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, 6), 0); + + memcpy(pkt, "\x00\x01fw\0octet\0bogus\01\0", 18); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, 18), 0); + + tftp_test_ctx_reset(&ctx); + ctx.open_fail = -1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + + tftp_test_ctx_reset(&ctx); + memcpy(ctx.read_data, "abcdefg", 7); + ctx.read_len[0] = 7; + ctx.read_last[0] = 1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &wrong_tid, pkt, 4), 0); + ck_assert_int_eq(wolftftp_server_poll(&server, 1), 0); + ck_assert_int_eq(wolftftp_server_poll(&server, 2000), 0); + ck_assert_int_eq(wolftftp_server_poll(&server, 4000), 0); + ck_assert_int_eq(wolftftp_server_poll(&server, 6000), 0); +} +END_TEST + +START_TEST(test_tftp_server_session_reaped_after_completion) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote; + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + unsigned int n; + unsigned int i; + + /* Run more transfers than the static session pool can hold; if + * wolftftp_server_finish failed to free the slot, the second batch + * of allocs would fall through to the "no slots" error path and + * leave open_calls at WOLFTFTP_SERVER_MAX_SESSIONS. */ + n = WOLFTFTP_SERVER_MAX_SESSIONS * 2U + 1U; + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + + for (i = 0; i < n; i++) { + remote = tftp_remote(0x0A000100U + i, (uint16_t)(7000U + i)); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_WRQ, + "fw.bin", &cfg, 4, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + memcpy(pkt + 4, "end", 3); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, + (uint16_t)wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 3)), 0); + ck_assert_uint_eq(server.sessions[0].state, WOLFTFTP_SESSION_FREE); + } + ck_assert_int_eq(ctx.open_calls, (int)n); + ck_assert_int_eq(ctx.close_calls, (int)n); +} +END_TEST + +START_TEST(test_tftp_client_honors_caller_server_port) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + /* User supplies a non-default server port (e.g. 1069). The RRQ + * must go to that port and TID locking must still happen on the + * first response, even though the configured port is not 69. */ + struct wolftftp_endpoint srv = tftp_remote(0x0A000020U, 1069); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 5005); + struct wolftftp_endpoint bad_tid = tftp_remote(srv.ip, 5006); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ck_assert_uint_eq(client.server.port, 1069U); + ck_assert_uint_eq(client.tid_locked, 0U); + ck_assert_uint_eq(ctx.sent_remote[0].port, 1069U); + + memcpy(pkt + 4, "0123456789abcdef", 16); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 16); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_uint_eq(client.server.port, 5005U); + ck_assert_uint_eq(client.tid_locked, 1U); + + /* Once locked, a packet from the original non-default port is no + * longer accepted (it is now an unknown TID). */ + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &srv, pkt, (uint16_t)len), WOLFTFTP_ERR_TID); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &bad_tid, pkt, (uint16_t)len), WOLFTFTP_ERR_TID); +} +END_TEST + +START_TEST(test_tftp_client_default_port_when_zero) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000021U, 0); + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ck_assert_uint_eq(client.server.port, WOLFTFTP_PORT); + ck_assert_uint_eq(ctx.sent_remote[0].port, WOLFTFTP_PORT); +} +END_TEST + +START_TEST(test_tftp_parse_tsize_rejects_non_numeric) +{ + uint8_t pkt[64]; + struct wolftftp_parsed_req req; + struct wolftftp_negotiated neg; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + + /* tsize="abc": must be rejected as unsupported, not silently + * treated as tsize=0. Same check for OACK. Note: pass len so the + * trailing NUL of "abc" lies INSIDE buf+len, otherwise the test + * would be probing the (now-fixed) OOB read instead of the + * non-numeric rejection path. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0octet\0tsize\0abc\0", 19); + ck_assert_int_eq(wolftftp_parse_request(pkt, 21, &req), + WOLFTFTP_ERR_UNSUPPORTED); + + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, "tsize\0abc\0", 10); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 12, &neg), + WOLFTFTP_ERR_UNSUPPORTED); + + /* tsize=0 is still valid and parses to 0. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, "tsize\0" "0\0", 8); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 10, &neg), 0); + ck_assert_uint_eq(neg.have_tsize, 1U); + ck_assert_uint_eq(neg.tsize, 0U); +} +END_TEST + +START_TEST(test_tftp_build_request_fits_max_options) +{ + /* All four options enabled plus a near-max filename must still + * fit in the buffer used by the client for retransmits. */ + uint8_t buf[WOLFTFTP_REQ_BUF_MAX]; + struct wolftftp_transfer_cfg cfg; + char name[WOLFTFTP_MAX_FILENAME]; + uint8_t requested = 0; + uint16_t out_len = 0; + size_t i; + + memset(&cfg, 0, sizeof(cfg)); + cfg.blksize = WOLFTFTP_MAX_BLKSIZE; + cfg.timeout_s = 255; + cfg.windowsize = WOLFTFTP_MAX_WINDOWSIZE; + cfg.max_image_size = 0xFFFFFFFFU; + for (i = 0; i + 1 < sizeof(name); i++) + name[i] = 'a'; + name[sizeof(name) - 1] = '\0'; + + ck_assert_int_eq(wolftftp_build_request(buf, sizeof(buf), WOLFTFTP_OP_RRQ, + name, &cfg, 0xFFFFFFFFU, &requested, &out_len), 0); + ck_assert_uint_eq(requested, + (uint8_t)(WOLFTFTP_OPT_BLKSIZE | WOLFTFTP_OPT_TIMEOUT | + WOLFTFTP_OPT_WINDOWSIZE | WOLFTFTP_OPT_TSIZE)); + ck_assert_uint_le(out_len, sizeof(buf)); +} +END_TEST + +START_TEST(test_tftp_server_rrq_retransmit_replays_window) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + /* Server uses defaults (no options), with a windowsize of 2 so we + * actually get a multi-block window we can compare across the + * retransmit boundary. */ + struct wolftftp_transfer_cfg cfg; + struct wolftftp_transfer_cfg req_cfg; + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000030U, 4001); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + uint16_t blksize = 8U; + + memset(&cfg, 0, sizeof(cfg)); + cfg.blksize = blksize; + cfg.timeout_s = 1; + cfg.windowsize = 2; + cfg.max_retries = 5; + + /* Request side wants all defaults so no options are negotiated; + * the server immediately sends the first window of data. */ + memset(&req_cfg, 0, sizeof(req_cfg)); + req_cfg.blksize = WOLFTFTP_DEFAULT_BLKSIZE; + req_cfg.timeout_s = WOLFTFTP_DEFAULT_TIMEOUT_S; + req_cfg.windowsize = 1; + + tftp_test_ctx_reset(&ctx); + /* Fill the source with several full blocks of distinguishable data + * so we can spot any accidental advance past the unacked window. */ + memset(ctx.read_data, 'A', blksize); + memset(ctx.read_data + blksize, 'B', blksize); + memset(ctx.read_data + 2 * blksize, 'C', blksize); + memset(ctx.read_data + 3 * blksize, 'D', blksize); + ctx.read_len[0] = blksize; + ctx.read_len[1] = blksize; + /* Replay (block 1, block 2) again on retransmit. */ + ctx.read_len[2] = blksize; + ctx.read_len[3] = blksize; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &req_cfg, 0, &opts, &req_len), 0); + ck_assert_uint_eq(opts, 0U); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + /* No-option RRQ skips the OACK; first window of 2 blocks is sent. */ + ck_assert_int_eq(ctx.send_calls, 2); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0] + 2), 1U); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[1] + 2), 2U); + + /* Force a timeout; this triggers the RRQ retransmit branch in + * wolftftp_server_poll. Before the fix it would send blocks 3 and + * 4 (advancing past the unacked window); after the fix it must + * replay blocks 1 and 2. */ + ck_assert_int_eq(wolftftp_server_poll(&server, 1U), 0); + ck_assert_int_eq(wolftftp_server_poll(&server, 5000U), 0); + ck_assert_int_eq(ctx.send_calls, 4); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[2] + 2), 1U); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[3] + 2), 2U); + ck_assert_mem_eq(ctx.sent[2] + 4, ctx.sent[0] + 4, blksize); + ck_assert_mem_eq(ctx.sent[3] + 4, ctx.sent[1] + 4, blksize); + /* Session state must remain anchored at the (still-unacked) start + * of the replayed window. */ + ck_assert_uint_eq(server.sessions[0].window_start_block, 1U); + ck_assert_uint_eq(server.sessions[0].window_start_offset, 0U); +} +END_TEST + +START_TEST(test_tftp_client_poll_deadline_is_wrap_safe) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000040U, 0); + uint32_t base = 0xFFFFF000U; /* near uint32_t wrap */ + + cfg.timeout_s = 2; /* deadline = base + 2000, which wraps past 0 */ + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ck_assert_int_eq(ctx.send_calls, 1); + + /* Arm the deadline at base; the next tick computes + * deadline = base + 2000 which wraps to a small value. */ + ck_assert_int_eq(wolftftp_client_poll(&client, base), 0); + ck_assert_uint_ne(client.deadline_ms, 0U); + /* "After 1000ms" — wrap is still in the future — must not retry. */ + ck_assert_int_eq(wolftftp_client_poll(&client, base + 1000U), 0); + ck_assert_int_eq(ctx.send_calls, 1); + /* After deadline, retry fires even though now < deadline numerically + * would have been true with the old unsigned compare. */ + ck_assert_int_eq(wolftftp_client_poll(&client, base + 2500U), 0); + ck_assert_int_eq(ctx.send_calls, 2); +} +END_TEST + +START_TEST(test_tftp_server_rrq_sends_zero_byte_terminator_on_exact_multiple) +{ + /* RFC 1350: when the file length is an exact multiple of blksize + * the server must still send a trailing 0-byte DATA block so the + * peer can recognise EOF. Pin this behaviour with a 2 * blksize + * read source. */ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg; + struct wolftftp_transfer_cfg req_cfg; + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000050U, 4200); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + uint16_t blksize = 8U; + + memset(&cfg, 0, sizeof(cfg)); + cfg.blksize = blksize; + cfg.timeout_s = 1; + cfg.windowsize = 1; + cfg.max_retries = 3; + + memset(&req_cfg, 0, sizeof(req_cfg)); + req_cfg.blksize = WOLFTFTP_DEFAULT_BLKSIZE; + req_cfg.timeout_s = WOLFTFTP_DEFAULT_TIMEOUT_S; + req_cfg.windowsize = 1; + + tftp_test_ctx_reset(&ctx); + memset(ctx.read_data, 'A', blksize); + memset(ctx.read_data + blksize, 'B', blksize); + /* Two full blocks of data; reader hints is_last after each because + * fread-like callbacks don't know the file is an exact multiple. */ + ctx.read_len[0] = blksize; ctx.read_last[0] = 0; + ctx.read_len[1] = blksize; ctx.read_last[1] = 1; + /* A real callback would return 0 bytes past EOF. */ + ctx.read_len[2] = 0; ctx.read_last[2] = 1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &req_cfg, 0, &opts, &req_len), 0); + ck_assert_uint_eq(opts, 0U); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + /* First DATA: block 1, full blksize. */ + ck_assert_int_eq(ctx.send_calls, 1); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0]), WOLFTFTP_OP_DATA); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0] + 2), 1U); + ck_assert_uint_eq(ctx.sent_len[0], 4U + blksize); + + /* ACK 1 → expect block 2, also full blksize. */ + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + wolftftp_write_u16(pkt + 2, 1); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, 4), 0); + ck_assert_int_eq(ctx.send_calls, 2); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[1] + 2), 2U); + ck_assert_uint_eq(ctx.sent_len[1], 4U + blksize); + + /* ACK 2 → expect the explicit 0-byte block 3 (the EOF marker), + * not session completion. */ + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + wolftftp_write_u16(pkt + 2, 2); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, 4), 0); + ck_assert_int_eq(ctx.send_calls, 3); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[2] + 2), 3U); + ck_assert_uint_eq(ctx.sent_len[2], 4U); /* opcode + block, no data */ + ck_assert_int_eq(ctx.close_calls, 0); + + /* Final ACK closes the session. */ + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + wolftftp_write_u16(pkt + 2, 3); + ck_assert_int_eq(wolftftp_server_receive(&server, server.sessions[0].local_port, + &remote, pkt, 4), 0); + ck_assert_int_eq(ctx.close_calls, 1); + ck_assert_int_eq(ctx.close_status, 0); +} +END_TEST + +START_TEST(test_tftp_server_poll_deadline_is_wrap_safe) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000041U, 4100); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + uint32_t base = 0xFFFFF000U; + + cfg.timeout_s = 2; + + tftp_test_ctx_reset(&ctx); + memcpy(ctx.read_data, "abcdefg", 7); + ctx.read_len[0] = 7; + ctx.read_last[0] = 1; + /* Replay buffer for the retransmit path: */ + ctx.read_len[1] = 7; + ctx.read_last[1] = 1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(ctx.send_calls, 1); + + ck_assert_int_eq(wolftftp_server_poll(&server, base), 0); + ck_assert_uint_ne(server.sessions[0].deadline_ms, 0U); + /* Before wrap-adjusted deadline: must not retransmit. */ + ck_assert_int_eq(wolftftp_server_poll(&server, base + 1000U), 0); + ck_assert_int_eq(ctx.send_calls, 1); + /* After deadline (with arithmetic wrap): must retransmit. */ + ck_assert_int_eq(wolftftp_server_poll(&server, base + 2500U), 0); + ck_assert_int_eq(ctx.send_calls, 2); +} +END_TEST + +/* RFC 2347: when option negotiation was used the server MUST replay + * the OACK on timeout, not a bare ACK(0) or ACK(last_acked_block). + * Exercise both RRQ-with-options and WRQ-with-options paths and + * compare the retransmitted bytes to the original OACK. */ +START_TEST(test_tftp_server_timeout_replays_oack_after_option_negotiation) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote; + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + uint16_t original_len; + + /* ---- RRQ + options ---- */ + tftp_test_ctx_reset(&ctx); + memcpy(ctx.read_data, "wxyz", 4); + ctx.read_len[0] = 4; + /* Reader claims EOF; data_len < blksize so no extra 0-byte block. */ + remote = tftp_remote(0x0A000060U, 5050); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert(opts != 0U); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + /* First send is the OACK, not a DATA — sanity. */ + ck_assert_int_eq(ctx.send_calls, 1); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0]), WOLFTFTP_OP_OACK); + original_len = ctx.sent_len[0]; + ck_assert_uint_eq(server.sessions[0].options_sent, 1U); + ck_assert(server.sessions[0].oack_opts != 0U); + + /* Arm + trip the timeout. The retransmit must be a byte-for-byte + * copy of the OACK, never ACK(0). */ + ck_assert_int_eq(wolftftp_server_poll(&server, 0U), 0); + ck_assert_int_eq(wolftftp_server_poll(&server, + (uint32_t)(cfg.timeout_s * 1000U + 1U)), 0); + ck_assert_int_eq(ctx.send_calls, 2); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[1]), WOLFTFTP_OP_OACK); + ck_assert_uint_eq(ctx.sent_len[1], original_len); + ck_assert_mem_eq(ctx.sent[1], ctx.sent[0], original_len); + + /* ACK(0) clears options_sent; the next timeout must NOT replay + * the OACK any more — it should retransmit the data window. */ + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + wolftftp_write_u16(pkt + 2, 0); + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, 4), 0); + ck_assert_uint_eq(server.sessions[0].options_sent, 0U); + /* The ACK(0) triggered the first DATA send. */ + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[ctx.send_calls - 1]), + WOLFTFTP_OP_DATA); + + /* ---- WRQ + options ---- */ + tftp_test_ctx_reset(&ctx); + remote = tftp_remote(0x0A000061U, 5060); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_WRQ, + "fw.bin", &cfg, 4, &opts, &req_len), 0); + ck_assert(opts != 0U); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(ctx.send_calls, 1); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0]), WOLFTFTP_OP_OACK); + original_len = ctx.sent_len[0]; + + /* Timeout: must replay the OACK rather than ACK(0). */ + ck_assert_int_eq(wolftftp_server_poll(&server, 0U), 0); + ck_assert_int_eq(wolftftp_server_poll(&server, + (uint32_t)(cfg.timeout_s * 1000U + 1U)), 0); + ck_assert_int_eq(ctx.send_calls, 2); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[1]), WOLFTFTP_OP_OACK); + ck_assert_uint_eq(ctx.sent_len[1], original_len); + ck_assert_mem_eq(ctx.sent[1], ctx.sent[0], original_len); + + /* First DATA from client implicitly ACKs the OACK. options_sent + * must clear so further timeouts retransmit ACK(last) instead. */ + memcpy(pkt + 4, "abcd", 4); + { + int data_len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 4); + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, (uint16_t)data_len), 0); + } + ck_assert_uint_eq(server.sessions[0].options_sent, 0U); +} +END_TEST + +/* --------------------------------------------------------------------------- + * Coverage gap closures (RFC 1350 / 2347 / 2348 / 2349 / 7440 corners). + * + * Each test below targets a specific cluster of previously-uncovered + * branches in src/tftp/wolftftp.c. They are grouped by area: + * + * A. defensive NULL / bounds checks across the public + private API + * B. send-callback failure propagation + * C. option-range / option-parser edge cases + * D. client edge cases (TID lock, unexpected opcode, verify failure, + * tsize > max_image_size, duplicate-block ACK retransmit replay) + * E. server edge cases (WRQ full flow with options, io.* missing, + * bad-block ACK, retries exhausted, ENOSPACE on WRQ, hash failure) + * + * The tests intentionally exercise small, surgical scenarios so each + * failure points at one root cause rather than a regression in the + * happy path. + * ------------------------------------------------------------------------- */ + +START_TEST(test_tftp_helpers_null_and_bounds) +{ + /* strnlen_local: NULL input returns 0. */ + ck_assert_uint_eq(wolftftp_strnlen_local(NULL, 16), 0U); + /* No NUL within max_len returns max_len. */ + { + char no_nul[8]; + memset(no_nul, 'a', sizeof(no_nul)); + ck_assert_uint_eq(wolftftp_strnlen_local(no_nul, sizeof(no_nul)), + sizeof(no_nul)); + } + /* stricmp_local: either operand NULL → negative. */ + ck_assert_int_lt(wolftftp_stricmp_local(NULL, "octet"), 0); + ck_assert_int_lt(wolftftp_stricmp_local("octet", NULL), 0); + /* stricmp_local: prefix mismatch flips < 0 vs > 0 based on tolower. */ + ck_assert_int_lt(wolftftp_stricmp_local("a", "b"), 0); + ck_assert_int_gt(wolftftp_stricmp_local("z", "a"), 0); + /* Unequal lengths: "abc" vs "ab" — the shorter wins as "shorter < longer". */ + ck_assert_int_lt(wolftftp_stricmp_local("ab", "abc"), 0); + ck_assert_int_gt(wolftftp_stricmp_local("abc", "ab"), 0); + + /* parse_u32: all rejection paths. */ + { + uint32_t v = 0xCAFEU; + ck_assert_int_eq(wolftftp_parse_u32(NULL, 100, &v), -1); + ck_assert_int_eq(wolftftp_parse_u32("", 100, &v), -1); + ck_assert_int_eq(wolftftp_parse_u32("12", 100, NULL), -1); + /* non-digit somewhere in the middle */ + ck_assert_int_eq(wolftftp_parse_u32("1a2", 100, &v), -1); + /* numeric overflow past max_value */ + ck_assert_int_eq(wolftftp_parse_u32("1000000", 999, &v), -1); + } + + /* copy_string: NULL / zero dst / oversized src. */ + { + char dst[8]; + ck_assert_int_eq(wolftftp_copy_string(NULL, 8, "x"), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_copy_string(dst, 0, "x"), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_copy_string(dst, sizeof(dst), NULL), + -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_copy_string(dst, sizeof(dst), ""), + -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_copy_string(dst, 4, "longer"), + -WOLFIP_EINVAL); + } + + /* cfg_defaults clamping: zero fields filled in, oversized clamped. */ + { + struct wolftftp_transfer_cfg cfg; + memset(&cfg, 0, sizeof(cfg)); + wolftftp_cfg_defaults(&cfg); + ck_assert_uint_eq(cfg.blksize, WOLFTFTP_DEFAULT_BLKSIZE); + ck_assert_uint_eq(cfg.timeout_s, WOLFTFTP_DEFAULT_TIMEOUT_S); + ck_assert_uint_eq(cfg.windowsize, 1U); + ck_assert_uint_eq(cfg.max_retries, WOLFTFTP_MAX_RETRIES); + memset(&cfg, 0, sizeof(cfg)); + cfg.blksize = WOLFTFTP_MAX_BLKSIZE * 2U; + cfg.windowsize = WOLFTFTP_MAX_WINDOWSIZE * 2U; + wolftftp_cfg_defaults(&cfg); + ck_assert_uint_eq(cfg.blksize, WOLFTFTP_MAX_BLKSIZE); + ck_assert_uint_eq(cfg.windowsize, WOLFTFTP_MAX_WINDOWSIZE); + } + + /* wolftftp_deadline: when (now_ms + timeout_s*1000) wraps exactly + * to 0, the helper must nudge to 1 so the "not armed" sentinel + * stays distinguishable. */ + { + struct wolftftp_negotiated neg = {0}; + neg.timeout_s = 1U; + /* now + 1000 = 0 → nudged to 1. */ + ck_assert_uint_eq(wolftftp_deadline(&neg, 0U - 1000U), 1U); + ck_assert_uint_eq(wolftftp_deadline(&neg, 0U), 1000U); + } + + /* Low-level send wrapper rejects each kind of bad arg. */ + { + struct wolftftp_transport_ops t; + struct wolftftp_endpoint r = {0}; + uint8_t b[1]; + memset(&t, 0, sizeof(t)); + ck_assert_int_eq(wolftftp_send(NULL, 0, &r, b, 1), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_send(&t, 0, &r, b, 1), -WOLFIP_EINVAL); + t.send = (wolftftp_udp_send_cb)1; /* non-NULL is enough */ + ck_assert_int_eq(wolftftp_send(&t, 0, NULL, b, 1), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_send(&t, 0, &r, NULL, 1), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_send(&t, 0, &r, b, 0), -WOLFIP_EINVAL); + } + + /* finish() defensives: NULL-safe and (server, session NULL) safe. */ + wolftftp_client_finish(NULL, 0); + wolftftp_server_finish(NULL, NULL, 0); +} +END_TEST + +START_TEST(test_tftp_builders_overflow_and_bad_args) +{ + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint8_t tiny[6]; + uint16_t out_len = 0; + uint8_t requested = 0; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_negotiated neg; + + /* build_request: NULL args. */ + ck_assert_int_eq(wolftftp_build_request(NULL, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &requested, &out_len), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + NULL, &cfg, 0, &requested, &out_len), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", NULL, 0, &requested, &out_len), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, NULL, &out_len), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &requested, NULL), -WOLFIP_EINVAL); + /* build_request: empty filename. */ + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "", &cfg, 0, &requested, &out_len), -WOLFIP_EINVAL); + /* build_request: filename too long. */ + { + char too_long[WOLFTFTP_MAX_FILENAME + 4]; + memset(too_long, 'a', sizeof(too_long) - 1); + too_long[sizeof(too_long) - 1] = '\0'; + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_RRQ, too_long, &cfg, 0, &requested, &out_len), + -WOLFIP_EINVAL); + } + /* build_request: caller-supplied buffer cannot even fit fixed + * header (opcode + "fw.bin\0" + "octet\0" + a tiny option). */ + ck_assert_int_eq(wolftftp_build_request(tiny, sizeof(tiny), + WOLFTFTP_OP_RRQ, "fw.bin", &cfg, 0, &requested, &out_len), + WOLFTFTP_ERR_PACKET); + /* build_request: header fits but the first option does not — pass + * a buffer just large enough for opcode + filename + "octet\0" + * but not for any option blob. */ + { + struct wolftftp_transfer_cfg opt_cfg = tftp_cfg_defaults(); + uint8_t narrow[2 + 7 + 6 + 1]; /* +1 leaves no room for option */ + out_len = 0; requested = 0; + ck_assert_int_eq(wolftftp_build_request(narrow, sizeof(narrow), + WOLFTFTP_OP_RRQ, "fw.bin", &opt_cfg, 0, &requested, &out_len), + WOLFTFTP_ERR_PACKET); + } + + /* build_oack: NULL args + capped buffer triggers append failure. */ + wolftftp_neg_defaults(&neg, &cfg); + neg.tsize = 12; neg.have_tsize = 1; + ck_assert_int_eq(wolftftp_build_oack(NULL, sizeof(pkt), &neg, + WOLFTFTP_OPT_BLKSIZE), -WOLFIP_EINVAL); + ck_assert_int_eq(wolftftp_build_oack(pkt, sizeof(pkt), NULL, + WOLFTFTP_OPT_BLKSIZE), -WOLFIP_EINVAL); + /* Force every append-opt error branch by giving the OACK builder + * a buffer that grows just-too-small as each option is appended. */ + ck_assert_int_eq(wolftftp_build_oack(tiny, sizeof(tiny), &neg, + WOLFTFTP_OPT_BLKSIZE), WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_oack(tiny, sizeof(tiny), &neg, + WOLFTFTP_OPT_TIMEOUT), WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_oack(tiny, sizeof(tiny), &neg, + WOLFTFTP_OPT_TSIZE), WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_oack(tiny, sizeof(tiny), &neg, + WOLFTFTP_OPT_WINDOWSIZE), WOLFTFTP_ERR_PACKET); + + /* build_data: NULL data, undersized buffer. */ + ck_assert_int_eq(wolftftp_build_data(NULL, sizeof(pkt), 1, pkt + 4, 1), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_data(pkt, sizeof(pkt), 1, NULL, 1), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_data(pkt, 3, 1, pkt + 4, 1), + WOLFTFTP_ERR_PACKET); + + /* build_error: NULL args, msg overflows max_len. */ + ck_assert_int_eq(wolftftp_build_error(NULL, sizeof(pkt), + WOLFTFTP_EBADOP, "x"), WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_error(pkt, sizeof(pkt), + WOLFTFTP_EBADOP, NULL), WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_error(pkt, 4, WOLFTFTP_EBADOP, "x"), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_build_error(pkt, 5, WOLFTFTP_EBADOP, "long"), + WOLFTFTP_ERR_PACKET); + + /* packet_opcode: NULL buf, short len. */ + ck_assert_int_eq(wolftftp_packet_opcode(NULL, 4), -1); + ck_assert_int_eq(wolftftp_packet_opcode(pkt, 1), -1); + + /* parse_data: NULL args, short frame, wrong opcode. */ + { + struct wolftftp_parsed_data d; + ck_assert_int_eq(wolftftp_parse_data(NULL, 10, &d), WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_parse_data(pkt, 10, NULL), WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_parse_data(pkt, 3, &d), WOLFTFTP_ERR_PACKET); + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + ck_assert_int_eq(wolftftp_parse_data(pkt, 10, &d), WOLFTFTP_ERR_PACKET); + } + + /* parse_request: NULL args, too short. */ + { + struct wolftftp_parsed_req r; + ck_assert_int_eq(wolftftp_parse_request(NULL, 10, &r), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_parse_request(pkt, 10, NULL), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_parse_request(pkt, 3, &r), + WOLFTFTP_ERR_PACKET); + /* Filename that runs the whole buffer with no terminating NUL. */ + memset(pkt, 'a', sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + ck_assert_int_eq(wolftftp_parse_request(pkt, 32, &r), + WOLFTFTP_ERR_PACKET); + } + + /* parse_oack: NULL args, wrong opcode, short len. */ + { + struct wolftftp_negotiated n; + wolftftp_neg_defaults(&n, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(NULL, 10, &n), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 10, NULL), + WOLFTFTP_ERR_PACKET); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 1, &n), + WOLFTFTP_ERR_PACKET); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); /* wrong */ + ck_assert_int_eq(wolftftp_parse_oack(pkt, 4, &n), + WOLFTFTP_ERR_PACKET); + } +} +END_TEST + +START_TEST(test_tftp_send_failure_propagation) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000070U, 0); + struct wolftftp_endpoint cli = tftp_remote(0x0A000071U, 8000); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + + /* Client: initial RRQ send fails → start_rrq returns the send error. */ + tftp_test_ctx_reset(&ctx); + ctx.send_fail = WOLFTFTP_ERR_IO; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), + WOLFTFTP_ERR_IO); + + /* Client poll: retransmit send fails — poll returns the error. */ + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + ctx.send_fail = WOLFTFTP_ERR_IO; + ck_assert_int_eq(wolftftp_client_poll(&client, 0), 0); /* arms */ + ck_assert_int_eq(wolftftp_client_poll(&client, 5000U), + WOLFTFTP_ERR_IO); + + /* Server: parse-request fail (non-octet mode) → server replies with + * an error frame; if THAT send fails, the function bubbles up the + * transport error. */ + tftp_test_ctx_reset(&ctx); + ctx.send_fail = WOLFTFTP_ERR_IO; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0netascii\0", 12); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &cli, + pkt, 14), WOLFTFTP_ERR_IO); + + /* Server: OACK send fails on start_request — start_request returns + * the send error and the session is reaped (slot returns to FREE). */ + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), WOLFTFTP_OP_RRQ, + "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert(opts != 0U); + ctx.send_fail = WOLFTFTP_ERR_IO; + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &cli, + pkt, req_len), WOLFTFTP_ERR_IO); + + /* Server: first DATA send fails for a no-option RRQ. The session + * is left in SEND_WAIT_ACK and a follow-up timeout will retry. */ + tftp_test_ctx_reset(&ctx); + memcpy(ctx.read_data, "abcdefg", 7); + ctx.read_len[0] = 7; + ctx.read_last[0] = 1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + { + struct wolftftp_transfer_cfg req_cfg; + memset(&req_cfg, 0, sizeof(req_cfg)); + req_cfg.blksize = WOLFTFTP_DEFAULT_BLKSIZE; + req_cfg.timeout_s = WOLFTFTP_DEFAULT_TIMEOUT_S; + req_cfg.windowsize = 1; + opts = 0; + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_RRQ, "fw.bin", &req_cfg, 0, &opts, &req_len), 0); + ck_assert_uint_eq(opts, 0U); + ctx.send_fail = WOLFTFTP_ERR_IO; + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &cli, + pkt, req_len), WOLFTFTP_ERR_IO); + } +} +END_TEST + +START_TEST(test_tftp_parse_option_ranges) +{ + uint8_t pkt[96]; + struct wolftftp_parsed_req req; + struct wolftftp_negotiated neg; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + + /* RRQ: blksize=4 (< 8) → rejected. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0octet\0blksize\0" "4\0", 19); + ck_assert_int_eq(wolftftp_parse_request(pkt, 21, &req), + WOLFTFTP_ERR_UNSUPPORTED); + + /* RRQ: timeout=0 → rejected (must be 1..255). */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0octet\0timeout\0" "0\0", 19); + ck_assert_int_eq(wolftftp_parse_request(pkt, 21, &req), + WOLFTFTP_ERR_UNSUPPORTED); + + /* RRQ: windowsize=0 → rejected. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_RRQ); + memcpy(pkt + 2, "fw\0octet\0windowsize\0" "0\0", 22); + ck_assert_int_eq(wolftftp_parse_request(pkt, 24, &req), + WOLFTFTP_ERR_UNSUPPORTED); + + /* OACK: blksize=4 → rejected. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, "blksize\0" "4\0", 10); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 12, &neg), + WOLFTFTP_ERR_UNSUPPORTED); + + /* OACK: timeout=0 → rejected. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, "timeout\0" "0\0", 10); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 12, &neg), + WOLFTFTP_ERR_UNSUPPORTED); + + /* OACK: windowsize=0 → rejected. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, "windowsize\0" "0\0", 13); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 15, &neg), + WOLFTFTP_ERR_UNSUPPORTED); + + /* OACK: unknown option → rejected. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, "mystery\0" "1\0", 10); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, 12, &neg), + WOLFTFTP_ERR_UNSUPPORTED); + + /* OACK: full valid option set (covers all 4 accepted branches). + * Each "key\0" "val\0" pair is two adjacent string literals; the + * concatenated payload is the raw on-wire option list (no extra + * implicit C-string NUL because we memcpy by explicit length). */ + { + const char payload[] = + "blksize\0" "32\0" + "timeout\0" "5\0" + "tsize\0" "100\0" + "windowsize\0" "2\0"; + size_t payload_len = sizeof(payload) - 1; /* drop implicit NUL */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_OACK); + memcpy(pkt + 2, payload, payload_len); + wolftftp_neg_defaults(&neg, &cfg); + ck_assert_int_eq(wolftftp_parse_oack(pkt, (uint16_t)(2 + payload_len), + &neg), 0); + } + ck_assert_uint_eq(neg.blksize, 32U); + ck_assert_uint_eq(neg.timeout_s, 5U); + ck_assert_uint_eq(neg.tsize, 100U); + ck_assert_uint_eq(neg.have_tsize, 1U); + ck_assert_uint_eq(neg.windowsize, 2U); +} +END_TEST + +START_TEST(test_tftp_client_unexpected_opcode_rejected) +{ + /* RRQ-receiving client must reject opcodes that aren't OACK, + * ERROR, or DATA (e.g. a stray RRQ/WRQ/ACK) — RFC 1350 doesn't + * mandate this exact code but we treat it as ERR_PACKET. */ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000080U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 4321); + uint8_t pkt[16]; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + + /* Send a stray ACK to the client — must be rejected as malformed. */ + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + wolftftp_write_u16(pkt + 2, 1); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, 4), WOLFTFTP_ERR_PACKET); +} +END_TEST + +START_TEST(test_tftp_client_invalid_first_data_does_not_lock_tid) +{ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000080U, 0); + struct wolftftp_endpoint attacker = tftp_remote(srv.ip, 4321); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 5678); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + + /* A malformed first DATA must be rejected without latching its TID. */ + memset(pkt, 0, sizeof(pkt)); + wolftftp_write_u16(pkt, WOLFTFTP_OP_DATA); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &attacker, pkt, 3), WOLFTFTP_ERR_PACKET); + ck_assert_uint_eq(client.tid_locked, 0U); + ck_assert_uint_eq(client.server.port, WOLFTFTP_PORT); + + memcpy(pkt + 4, "0123456789abcdef", 16); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 16); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_uint_eq(client.tid_locked, 1U); + ck_assert_uint_eq(client.server.port, tid.port); +} +END_TEST + +START_TEST(test_tftp_client_max_image_size_enforced_on_data) +{ + /* OACK didn't trip the tsize check (no tsize); enforcement + * happens on accumulated bytes during accept_data. Pin both the + * error-frame send and the FAIL state. */ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000081U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 5678); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + cfg.max_image_size = 8U; + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + /* Block 1: 16 bytes (full blksize=16 from tftp_cfg_defaults) — over + * cfg.max_image_size = 8 → client sends ENOSPACE error + ERR_SIZE. */ + memcpy(pkt + 4, "0123456789abcdef", 16); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 16); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), WOLFTFTP_ERR_SIZE); + ck_assert_uint_eq(client.state, WOLFTFTP_CLIENT_ERROR); +} +END_TEST + +START_TEST(test_tftp_client_duplicate_block_replays_last_ack) +{ + /* RFC 1350: if the receiver sees an out-of-order block matching + * the last-acked or previous-expected one, it must replay the + * most recent ACK rather than ignoring it or breaking state. + * Use windowsize=1 so the client ACKs every block immediately and + * last_tx holds an ACK we can compare. */ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000082U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 6500); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + int send_calls_after_first_ack; + + cfg.windowsize = 1; + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + /* Deliver block 1 full-blksize — client writes, ACKs, expects 2. */ + memcpy(pkt + 4, "0123456789ABCDEF", 16); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 16); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + send_calls_after_first_ack = ctx.send_calls; + /* That ACK is the most recent send, so we can index it directly. */ + ck_assert_uint_eq(wolftftp_read_u16( + ctx.sent[send_calls_after_first_ack - 1]), WOLFTFTP_OP_ACK); + + /* Now redeliver block 1 — the client should replay its cached + * ACK(1) without re-writing the data. */ + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 16); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), 0); + ck_assert_int_eq(ctx.send_calls, send_calls_after_first_ack + 1); + ck_assert_int_eq(ctx.write_calls, 1); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[send_calls_after_first_ack]), + WOLFTFTP_OP_ACK); + ck_assert_uint_eq(wolftftp_read_u16( + ctx.sent[send_calls_after_first_ack] + 2), 1U); +} +END_TEST + +START_TEST(test_tftp_client_open_sink_missing_callbacks) +{ + /* Client misconfigured without io.write must fail when the first + * DATA arrives (open_sink internally short-circuits). */ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000083U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 6700); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + io.write = NULL; /* deliberately drop write */ + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + memcpy(pkt + 4, "end", 3); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 3); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), WOLFTFTP_ERR_IO); + ck_assert_uint_eq(client.state, WOLFTFTP_CLIENT_ERROR); +} +END_TEST + +START_TEST(test_tftp_client_oack_tsize_exceeds_limit) +{ + /* OACK advertises tsize bigger than cfg.max_image_size — client + * must send ENOSPACE and finish with ERR_SIZE before any DATA. */ + struct tftp_test_ctx ctx; + struct wolftftp_client client; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint srv = tftp_remote(0x0A000084U, 0); + struct wolftftp_endpoint tid = tftp_remote(srv.ip, 6800); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int len; + + cfg.max_image_size = 1U; + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_client_init(&client, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_client_start_rrq(&client, &srv, "fw.bin"), 0); + wolftftp_neg_defaults(&client.neg, &cfg); + client.neg.tsize = 1024U; + client.neg.have_tsize = 1; + len = wolftftp_build_oack(pkt, sizeof(pkt), &client.neg, + WOLFTFTP_OPT_TSIZE); + ck_assert_int_eq(wolftftp_client_receive(&client, cfg.local_port, + &tid, pkt, (uint16_t)len), WOLFTFTP_ERR_SIZE); + ck_assert_uint_eq(client.state, WOLFTFTP_CLIENT_ERROR); +} +END_TEST + +START_TEST(test_tftp_server_io_missing_for_rrq_and_wrq) +{ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000090U, 7000); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + + /* RRQ but server has no io.read → "io unavailable" error + slot + * reaped without entering data phase. */ + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + io.read = NULL; + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_RRQ, "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(ctx.send_calls, 1); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0]), WOLFTFTP_OP_ERROR); + ck_assert_uint_eq(server.sessions[0].state, WOLFTFTP_SESSION_FREE); + + /* WRQ but server has no io.write → same path on the opposite leg. */ + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + io.write = NULL; + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_WRQ, "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(ctx.send_calls, 1); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0]), WOLFTFTP_OP_ERROR); + ck_assert_uint_eq(server.sessions[0].state, WOLFTFTP_SESSION_FREE); +} +END_TEST + +START_TEST(test_tftp_server_retries_exhausted_to_timeout) +{ + /* Pump server_poll until retries == cfg.max_retries, expect the + * session to transition into the reap path with ERR_TIMEOUT. */ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000091U, 7100); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + unsigned int i; + + cfg.max_retries = 2; + cfg.timeout_s = 1; + tftp_test_ctx_reset(&ctx); + memcpy(ctx.read_data, "abc", 3); + ctx.read_len[0] = 3; + ctx.read_last[0] = 1; + /* Read buffer for each retransmit replay; fill enough slots. */ + for (i = 1; i < 8; i++) { + ctx.read_len[i] = 3; + ctx.read_last[i] = 1; + } + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + { + struct wolftftp_transfer_cfg req_cfg; + memset(&req_cfg, 0, sizeof(req_cfg)); + req_cfg.blksize = WOLFTFTP_DEFAULT_BLKSIZE; + req_cfg.timeout_s = WOLFTFTP_DEFAULT_TIMEOUT_S; + req_cfg.windowsize = 1; + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_RRQ, "fw.bin", &req_cfg, 0, &opts, &req_len), 0); + } + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + /* Drive the timeout loop past retries. */ + (void)wolftftp_server_poll(&server, 0U); + (void)wolftftp_server_poll(&server, 2000U); /* retry 1 */ + (void)wolftftp_server_poll(&server, 4000U); /* retry 2 */ + (void)wolftftp_server_poll(&server, 6000U); /* retries exhausted → reap */ + ck_assert_uint_eq(server.sessions[0].state, WOLFTFTP_SESSION_FREE); + ck_assert_int_eq(ctx.close_status, WOLFTFTP_ERR_TIMEOUT); +} +END_TEST + +START_TEST(test_tftp_server_wrq_full_flow_with_options) +{ + /* Drive a WRQ with options end-to-end: OACK → DATA (windowed) → + * ACK → DATA → final-short → ACK → close. This wakes a lot of + * accept_wrq_data branches. */ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg; + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000092U, 7200); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + int len; + + memset(&cfg, 0, sizeof(cfg)); + cfg.blksize = 8; + cfg.timeout_s = 1; + cfg.windowsize = 2; + cfg.max_retries = 3; + cfg.max_image_size = 64; + + tftp_test_ctx_reset(&ctx); + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_WRQ, "fw.bin", &cfg, 12, &opts, &req_len), 0); + ck_assert(opts != 0U); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[0]), WOLFTFTP_OP_OACK); + + /* Block 1: full blksize, second in window not yet — no ACK sent. */ + memcpy(pkt + 4, "AAAAAAAA", 8); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 8); + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, (uint16_t)len), 0); + /* Block 2: completes the window → ACK(2). */ + memcpy(pkt + 4, "BBBBBBBB", 8); + len = wolftftp_build_data(pkt, sizeof(pkt), 2, pkt + 4, 8); + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, (uint16_t)len), 0); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[ctx.send_calls - 1]), + WOLFTFTP_OP_ACK); + ck_assert_uint_eq(wolftftp_read_u16(ctx.sent[ctx.send_calls - 1] + 2), 2U); + + /* Final short block 3 (4 bytes < blksize) → final ACK + close. */ + memcpy(pkt + 4, "CCCC", 4); + len = wolftftp_build_data(pkt, sizeof(pkt), 3, pkt + 4, 4); + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, (uint16_t)len), 0); + ck_assert_int_eq(ctx.close_calls, 1); + ck_assert_int_eq(ctx.close_status, 0); + ck_assert_mem_eq(ctx.write_buf, "AAAAAAAABBBBBBBBCCCC", 20); + /* Late duplicate of block 3 must replay ACK(3) (covers the + * accept_wrq_data duplicate branch). */ + memcpy(pkt + 4, "CCCC", 4); + len = wolftftp_build_data(pkt, sizeof(pkt), 3, pkt + 4, 4); + /* After close the slot is FREE, so the server treats this as an + * unknown TID — still a defined branch, not a crash. */ + (void)wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, (uint16_t)len); +} +END_TEST + +START_TEST(test_tftp_server_wrq_hash_failure_and_size_overflow) +{ + /* WRQ data write succeeds but io.hash_update fails — must finish + * the session as ERR_VERIFY. */ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg = tftp_cfg_defaults(); + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000093U, 7300); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + int len; + + tftp_test_ctx_reset(&ctx); + ctx.hash_fail = -1; + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_WRQ, "fw.bin", &cfg, 0, &opts, &req_len), 0); + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + memcpy(pkt + 4, "xxx", 3); + len = wolftftp_build_data(pkt, sizeof(pkt), 1, pkt + 4, 3); + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, (uint16_t)len), + WOLFTFTP_ERR_VERIFY); + ck_assert_uint_eq(server.sessions[0].state, WOLFTFTP_SESSION_FREE); +} +END_TEST + +START_TEST(test_tftp_server_rrq_ack_bad_then_recover) +{ + /* ACK that doesn't match last_acked / (next_block-1) / OACK-ack-0 + * is silently dropped. Then a valid ACK proceeds the transfer. */ + struct tftp_test_ctx ctx; + struct wolftftp_server server; + struct wolftftp_transfer_cfg cfg; + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_endpoint remote = tftp_remote(0x0A000094U, 7400); + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t req_len = 0; + uint8_t opts = 0; + + memset(&cfg, 0, sizeof(cfg)); + cfg.blksize = 8; + cfg.timeout_s = 1; + cfg.windowsize = 1; + cfg.max_retries = 3; + + tftp_test_ctx_reset(&ctx); + memcpy(ctx.read_data, "abcdefgh", 8); + ctx.read_len[0] = 8; /* full block */ + ctx.read_len[1] = 4; ctx.read_last[1] = 1; /* short final on retry */ + transport = tftp_transport_ops(&ctx); + io = tftp_io_ops(&ctx); + wolftftp_server_init(&server, &transport, &io, &cfg); + { + struct wolftftp_transfer_cfg req_cfg; + memset(&req_cfg, 0, sizeof(req_cfg)); + req_cfg.blksize = WOLFTFTP_DEFAULT_BLKSIZE; + req_cfg.timeout_s = WOLFTFTP_DEFAULT_TIMEOUT_S; + req_cfg.windowsize = 1; + ck_assert_int_eq(wolftftp_build_request(pkt, sizeof(pkt), + WOLFTFTP_OP_RRQ, "fw.bin", &req_cfg, 0, &opts, &req_len), 0); + } + ck_assert_int_eq(wolftftp_server_receive(&server, WOLFTFTP_PORT, &remote, + pkt, req_len), 0); + ck_assert_int_eq(ctx.send_calls, 1); + + /* ACK with bogus block number 99 → dropped silently, no new DATA. */ + wolftftp_write_u16(pkt, WOLFTFTP_OP_ACK); + wolftftp_write_u16(pkt + 2, 99); + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, 4), 0); + ck_assert_int_eq(ctx.send_calls, 1); + + /* ACK with malformed length → ERR_PACKET. */ + ck_assert_int_eq(wolftftp_server_receive(&server, + server.sessions[0].local_port, &remote, pkt, 3), + WOLFTFTP_ERR_PACKET); +} +END_TEST + +static void add_tftp_tests(TCase *tc_proto) +{ + tcase_add_test(tc_proto, test_tftp_helpers_and_builders); + tcase_add_test(tc_proto, test_tftp_parse_request_error_paths); + tcase_add_test(tc_proto, test_tftp_client_rrq_oack_and_data_success); + tcase_add_test(tc_proto, test_tftp_client_fallback_duplicate_and_tid_errors); + tcase_add_test(tc_proto, test_tftp_client_error_and_failure_paths); + tcase_add_test(tc_proto, test_tftp_client_poll_and_status_paths); + tcase_add_test(tc_proto, test_tftp_server_rrq_success_and_poll); + tcase_add_test(tc_proto, test_tftp_server_wrq_success_and_failures); + tcase_add_test(tc_proto, test_tftp_server_request_errors_and_timeouts); + tcase_add_test(tc_proto, test_tftp_server_session_reaped_after_completion); + tcase_add_test(tc_proto, test_tftp_client_honors_caller_server_port); + tcase_add_test(tc_proto, test_tftp_client_default_port_when_zero); + tcase_add_test(tc_proto, test_tftp_parse_tsize_rejects_non_numeric); + tcase_add_test(tc_proto, test_tftp_build_request_fits_max_options); + tcase_add_test(tc_proto, test_tftp_server_rrq_retransmit_replays_window); + tcase_add_test(tc_proto, test_tftp_client_poll_deadline_is_wrap_safe); + tcase_add_test(tc_proto, test_tftp_server_poll_deadline_is_wrap_safe); + tcase_add_test(tc_proto, + test_tftp_server_rrq_sends_zero_byte_terminator_on_exact_multiple); + tcase_add_test(tc_proto, + test_tftp_server_timeout_replays_oack_after_option_negotiation); + /* Coverage-targeted batches (see "Coverage gap closures" header). */ + tcase_add_test(tc_proto, test_tftp_helpers_null_and_bounds); + tcase_add_test(tc_proto, test_tftp_builders_overflow_and_bad_args); + tcase_add_test(tc_proto, test_tftp_send_failure_propagation); + tcase_add_test(tc_proto, test_tftp_parse_option_ranges); + tcase_add_test(tc_proto, test_tftp_client_unexpected_opcode_rejected); + tcase_add_test(tc_proto, test_tftp_client_invalid_first_data_does_not_lock_tid); + tcase_add_test(tc_proto, test_tftp_client_max_image_size_enforced_on_data); + tcase_add_test(tc_proto, test_tftp_client_duplicate_block_replays_last_ack); + tcase_add_test(tc_proto, test_tftp_client_open_sink_missing_callbacks); + tcase_add_test(tc_proto, test_tftp_client_oack_tsize_exceeds_limit); + tcase_add_test(tc_proto, test_tftp_server_io_missing_for_rrq_and_wrq); + tcase_add_test(tc_proto, test_tftp_server_retries_exhausted_to_timeout); + tcase_add_test(tc_proto, test_tftp_server_wrq_full_flow_with_options); + tcase_add_test(tc_proto, test_tftp_server_wrq_hash_failure_and_size_overflow); + tcase_add_test(tc_proto, test_tftp_server_rrq_ack_bad_then_recover); +} diff --git a/src/tftp/wolftftp.c b/src/tftp/wolftftp.c new file mode 100644 index 0000000..ee78cae --- /dev/null +++ b/src/tftp/wolftftp.c @@ -0,0 +1,1193 @@ +/* wolftftp.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +#include "wolftftp.h" + +#include +#include +#include + +#define WOLFTFTP_PKT_MAX (4U + WOLFTFTP_MAX_BLKSIZE) +#define WOLFTFTP_OPT_BLKSIZE 0x01U +#define WOLFTFTP_OPT_TIMEOUT 0x02U +#define WOLFTFTP_OPT_TSIZE 0x04U +#define WOLFTFTP_OPT_WINDOWSIZE 0x08U + +struct wolftftp_parsed_req { + uint16_t opcode; + char filename[WOLFTFTP_MAX_FILENAME]; + uint16_t blksize; + uint16_t timeout_s; + uint16_t windowsize; + uint32_t tsize; + uint8_t opts; +}; + +struct wolftftp_parsed_data { + uint16_t block; + const uint8_t *data; + uint16_t data_len; +}; + +static size_t wolftftp_strnlen_local(const char *s, size_t max_len) +{ + size_t i; + + if (s == NULL) + return 0; + for (i = 0; i < max_len; i++) { + if (s[i] == '\0') + return i; + } + return max_len; +} + +static int wolftftp_stricmp_local(const char *a, const char *b) +{ + unsigned char ca; + unsigned char cb; + + if (a == NULL || b == NULL) + return -1; + while (*a != '\0' || *b != '\0') { + ca = (unsigned char)tolower((unsigned char)*a); + cb = (unsigned char)tolower((unsigned char)*b); + if (ca != cb) + return (int)ca - (int)cb; + if (*a != '\0') + a++; + if (*b != '\0') + b++; + } + return 0; +} + +static uint16_t wolftftp_read_u16(const uint8_t *buf) +{ + return (uint16_t)(((uint16_t)buf[0] << 8) | buf[1]); +} + +static void wolftftp_write_u16(uint8_t *buf, uint16_t value) +{ + buf[0] = (uint8_t)(value >> 8); + buf[1] = (uint8_t)(value & 0xFFU); +} + +static int wolftftp_parse_u32(const char *value, uint32_t max_value, + uint32_t *out) +{ + uint32_t v = 0; + + if (value == NULL || out == NULL || *value == '\0') + return -1; + while (*value != '\0') { + if (*value < '0' || *value > '9') + return -1; + v = (v * 10U) + (uint32_t)(*value - '0'); + if (v > max_value) + return -1; + value++; + } + *out = v; + return 0; +} + +static int wolftftp_copy_string(char *dst, size_t dst_len, const char *src) +{ + size_t len; + + if (dst == NULL || dst_len == 0 || src == NULL) + return -WOLFIP_EINVAL; + len = wolftftp_strnlen_local(src, dst_len); + if (len == 0 || len >= dst_len) + return -WOLFIP_EINVAL; + memcpy(dst, src, len); + dst[len] = '\0'; + return 0; +} + +static void wolftftp_cfg_defaults(struct wolftftp_transfer_cfg *cfg) +{ + if (cfg->blksize == 0) + cfg->blksize = WOLFTFTP_DEFAULT_BLKSIZE; + if (cfg->timeout_s == 0) + cfg->timeout_s = WOLFTFTP_DEFAULT_TIMEOUT_S; + if (cfg->windowsize == 0) + cfg->windowsize = 1; + if (cfg->max_retries == 0) + cfg->max_retries = WOLFTFTP_MAX_RETRIES; + if (cfg->blksize > WOLFTFTP_MAX_BLKSIZE) + cfg->blksize = WOLFTFTP_MAX_BLKSIZE; + if (cfg->windowsize > WOLFTFTP_MAX_WINDOWSIZE) + cfg->windowsize = WOLFTFTP_MAX_WINDOWSIZE; +} + +static void wolftftp_neg_defaults(struct wolftftp_negotiated *neg, + const struct wolftftp_transfer_cfg *cfg) +{ + memset(neg, 0, sizeof(*neg)); + neg->blksize = cfg->blksize; + neg->timeout_s = cfg->timeout_s; + neg->windowsize = cfg->windowsize; +} + +static int wolftftp_send(struct wolftftp_transport_ops *transport, + uint16_t local_port, const struct wolftftp_endpoint *remote, + const uint8_t *buf, uint16_t len) +{ + if (transport == NULL || transport->send == NULL || remote == NULL || + buf == NULL || len == 0) + return -WOLFIP_EINVAL; + return transport->send(transport->arg, local_port, remote, buf, len); +} + +static void wolftftp_client_finish(struct wolftftp_client *client, int status) +{ + if (client == NULL) + return; + client->last_status = status; + if (client->io.close != NULL && client->handle != NULL) + client->io.close(client->io.arg, client->handle, status); + client->handle = NULL; + client->deadline_ms = 0; + client->last_tx_len = 0; + if (status == 0) + client->state = WOLFTFTP_CLIENT_COMPLETE; + else + client->state = WOLFTFTP_CLIENT_ERROR; +} + +static void wolftftp_server_finish(struct wolftftp_server *server, + struct wolftftp_server_session *session, int status) +{ + if (session == NULL) + return; + session->last_status = status; + if (status == 0) { + if (session->state != WOLFTFTP_SESSION_FREE) + session->state = WOLFTFTP_SESSION_COMPLETE; + } else { + session->state = WOLFTFTP_SESSION_ERROR; + } + if (server != NULL && server->io.close != NULL && session->handle != NULL) + server->io.close(server->io.arg, session->handle, status); + session->handle = NULL; + /* Reap the slot in both success and failure paths so that further + * transfers can be accepted; we have no public API that observes + * COMPLETE state. */ + memset(session, 0, sizeof(*session)); +} + +static int wolftftp_append_opt(uint8_t *buf, uint16_t *off, uint16_t max_len, + const char *key, const char *value) +{ + size_t klen; + size_t vlen; + + klen = strlen(key) + 1U; + vlen = strlen(value) + 1U; + if ((uint32_t)(*off) + klen + vlen > max_len) + return WOLFTFTP_ERR_PACKET; + memcpy(buf + *off, key, klen); + *off = (uint16_t)(*off + klen); + memcpy(buf + *off, value, vlen); + *off = (uint16_t)(*off + vlen); + return 0; +} + +static int wolftftp_build_request(uint8_t *buf, uint16_t max_len, uint16_t opcode, + const char *filename, const struct wolftftp_transfer_cfg *cfg, uint32_t tsize, + uint8_t *requested_opts, uint16_t *out_len) +{ + uint16_t off = 0; + char value[16]; + int ret; + size_t name_len; + + if (buf == NULL || filename == NULL || cfg == NULL || requested_opts == NULL || + out_len == NULL) + return -WOLFIP_EINVAL; + + name_len = wolftftp_strnlen_local(filename, WOLFTFTP_MAX_FILENAME); + if (name_len == 0 || name_len >= WOLFTFTP_MAX_FILENAME) + return -WOLFIP_EINVAL; + if (max_len < (uint16_t)(4U + name_len + 6U)) + return WOLFTFTP_ERR_PACKET; + + wolftftp_write_u16(buf, opcode); + off = 2; + memcpy(buf + off, filename, name_len + 1U); + off = (uint16_t)(off + name_len + 1U); + memcpy(buf + off, "octet", 6); + off = (uint16_t)(off + 6U); + *requested_opts = 0; + + if (cfg->blksize != WOLFTFTP_DEFAULT_BLKSIZE) { + (void)snprintf(value, sizeof(value), "%u", cfg->blksize); + ret = wolftftp_append_opt(buf, &off, max_len, "blksize", value); + if (ret != 0) + return ret; + *requested_opts |= WOLFTFTP_OPT_BLKSIZE; + } + if (cfg->timeout_s != WOLFTFTP_DEFAULT_TIMEOUT_S) { + (void)snprintf(value, sizeof(value), "%u", cfg->timeout_s); + ret = wolftftp_append_opt(buf, &off, max_len, "timeout", value); + if (ret != 0) + return ret; + *requested_opts |= WOLFTFTP_OPT_TIMEOUT; + } + if (cfg->windowsize > 1U) { + (void)snprintf(value, sizeof(value), "%u", cfg->windowsize); + ret = wolftftp_append_opt(buf, &off, max_len, "windowsize", value); + if (ret != 0) + return ret; + *requested_opts |= WOLFTFTP_OPT_WINDOWSIZE; + } + if (tsize != 0U || cfg->max_image_size != 0U) { + (void)snprintf(value, sizeof(value), "%lu", (unsigned long)tsize); + ret = wolftftp_append_opt(buf, &off, max_len, "tsize", value); + if (ret != 0) + return ret; + *requested_opts |= WOLFTFTP_OPT_TSIZE; + } + + *out_len = off; + return 0; +} + +static int wolftftp_parse_request(const uint8_t *buf, uint16_t len, + struct wolftftp_parsed_req *req) +{ + const char *p; + size_t slen; + const char *key; + const char *value; + uint32_t number; + + if (buf == NULL || req == NULL || len < 4) + return WOLFTFTP_ERR_PACKET; + memset(req, 0, sizeof(*req)); + req->opcode = wolftftp_read_u16(buf); + if (req->opcode != WOLFTFTP_OP_RRQ && req->opcode != WOLFTFTP_OP_WRQ) + return WOLFTFTP_ERR_PACKET; + + p = (const char *)(buf + 2); + slen = wolftftp_strnlen_local(p, len - 2U); + if (slen == 0 || slen >= WOLFTFTP_MAX_FILENAME || (uint16_t)(2U + slen) >= len) + return WOLFTFTP_ERR_PACKET; + memcpy(req->filename, p, slen); + req->filename[slen] = '\0'; + p += slen + 1U; + slen = wolftftp_strnlen_local(p, (size_t)(buf + len - (const uint8_t *)p)); + if (slen == 0 || wolftftp_stricmp_local(p, "octet") != 0) + return WOLFTFTP_ERR_UNSUPPORTED; + p += slen + 1U; + + while ((const uint8_t *)p < buf + len) { + key = p; + slen = wolftftp_strnlen_local(key, + (size_t)(buf + len - (const uint8_t *)key)); + if (slen == 0 || (const uint8_t *)(key + slen) >= buf + len) + return WOLFTFTP_ERR_PACKET; + value = key + slen + 1U; + slen = wolftftp_strnlen_local(value, + (size_t)(buf + len - (const uint8_t *)value)); + /* Same `>=` check as the key side: when strnlen_local saturates + * to max_len there is no NUL inside the buffer, so passing the + * unterminated value through to parse_u32 / stricmp would walk + * past buf+len. */ + if (slen == 0 || (const uint8_t *)(value + slen) >= buf + len) + return WOLFTFTP_ERR_PACKET; + if (wolftftp_stricmp_local(key, "blksize") == 0) { + if (wolftftp_parse_u32(value, WOLFTFTP_MAX_BLKSIZE, &number) != 0 || + number < 8U) + return WOLFTFTP_ERR_UNSUPPORTED; + req->blksize = (uint16_t)number; + req->opts |= WOLFTFTP_OPT_BLKSIZE; + } else if (wolftftp_stricmp_local(key, "timeout") == 0) { + if (wolftftp_parse_u32(value, 255U, &number) != 0 || number == 0U) + return WOLFTFTP_ERR_UNSUPPORTED; + req->timeout_s = (uint16_t)number; + req->opts |= WOLFTFTP_OPT_TIMEOUT; + } else if (wolftftp_stricmp_local(key, "tsize") == 0) { + if (wolftftp_parse_u32(value, 0xFFFFFFFFUL, &number) != 0) + return WOLFTFTP_ERR_UNSUPPORTED; + req->tsize = number; + req->opts |= WOLFTFTP_OPT_TSIZE; + } else if (wolftftp_stricmp_local(key, "windowsize") == 0) { + if (wolftftp_parse_u32(value, WOLFTFTP_MAX_WINDOWSIZE, &number) != 0 || + number == 0U) + return WOLFTFTP_ERR_UNSUPPORTED; + req->windowsize = (uint16_t)number; + req->opts |= WOLFTFTP_OPT_WINDOWSIZE; + } else { + return WOLFTFTP_ERR_UNSUPPORTED; + } + p = value + slen + 1U; + } + + return 0; +} + +static int wolftftp_build_ack(uint8_t *buf, uint16_t block) +{ + wolftftp_write_u16(buf, WOLFTFTP_OP_ACK); + wolftftp_write_u16(buf + 2, block); + return 4; +} + +static int wolftftp_build_error(uint8_t *buf, uint16_t max_len, uint16_t code, + const char *msg) +{ + size_t mlen; + + if (buf == NULL || msg == NULL || max_len < 5) + return WOLFTFTP_ERR_PACKET; + mlen = strlen(msg) + 1U; + if ((uint32_t)mlen + 4U > max_len) + return WOLFTFTP_ERR_PACKET; + wolftftp_write_u16(buf, WOLFTFTP_OP_ERROR); + wolftftp_write_u16(buf + 2, code); + memcpy(buf + 4, msg, mlen); + return (int)(4U + mlen); +} + +static int wolftftp_build_data(uint8_t *buf, uint16_t max_len, uint16_t block, + const uint8_t *data, uint16_t data_len) +{ + if (buf == NULL || data == NULL || max_len < (uint16_t)(4U + data_len)) + return WOLFTFTP_ERR_PACKET; + wolftftp_write_u16(buf, WOLFTFTP_OP_DATA); + wolftftp_write_u16(buf + 2, block); + if (data_len > 0) + memcpy(buf + 4, data, data_len); + return (int)(4U + data_len); +} + +static int wolftftp_build_oack(uint8_t *buf, uint16_t max_len, + const struct wolftftp_negotiated *neg, uint8_t opts) +{ + uint16_t off = 2; + char value[16]; + int ret; + + if (buf == NULL || neg == NULL) + return -WOLFIP_EINVAL; + wolftftp_write_u16(buf, WOLFTFTP_OP_OACK); + if ((opts & WOLFTFTP_OPT_BLKSIZE) != 0U) { + (void)snprintf(value, sizeof(value), "%u", neg->blksize); + ret = wolftftp_append_opt(buf, &off, max_len, "blksize", value); + if (ret != 0) + return ret; + } + if ((opts & WOLFTFTP_OPT_TIMEOUT) != 0U) { + (void)snprintf(value, sizeof(value), "%u", neg->timeout_s); + ret = wolftftp_append_opt(buf, &off, max_len, "timeout", value); + if (ret != 0) + return ret; + } + if ((opts & WOLFTFTP_OPT_TSIZE) != 0U) { + (void)snprintf(value, sizeof(value), "%lu", (unsigned long)neg->tsize); + ret = wolftftp_append_opt(buf, &off, max_len, "tsize", value); + if (ret != 0) + return ret; + } + if ((opts & WOLFTFTP_OPT_WINDOWSIZE) != 0U) { + (void)snprintf(value, sizeof(value), "%u", neg->windowsize); + ret = wolftftp_append_opt(buf, &off, max_len, "windowsize", value); + if (ret != 0) + return ret; + } + return off; +} + +static int wolftftp_parse_oack(const uint8_t *buf, uint16_t len, + struct wolftftp_negotiated *neg) +{ + const char *p; + size_t slen; + const char *key; + const char *value; + uint32_t number; + + if (buf == NULL || neg == NULL || len < 2) + return WOLFTFTP_ERR_PACKET; + if (wolftftp_read_u16(buf) != WOLFTFTP_OP_OACK) + return WOLFTFTP_ERR_PACKET; + p = (const char *)(buf + 2); + while ((const uint8_t *)p < buf + len) { + key = p; + slen = wolftftp_strnlen_local(key, + (size_t)(buf + len - (const uint8_t *)key)); + if (slen == 0 || (const uint8_t *)(key + slen) >= buf + len) + return WOLFTFTP_ERR_PACKET; + value = key + slen + 1U; + slen = wolftftp_strnlen_local(value, + (size_t)(buf + len - (const uint8_t *)value)); + /* Same `>=` check as the key side: when strnlen_local saturates + * to max_len there is no NUL inside the buffer, so passing the + * unterminated value through to parse_u32 / stricmp would walk + * past buf+len. */ + if (slen == 0 || (const uint8_t *)(value + slen) >= buf + len) + return WOLFTFTP_ERR_PACKET; + if (wolftftp_stricmp_local(key, "blksize") == 0) { + if (wolftftp_parse_u32(value, WOLFTFTP_MAX_BLKSIZE, &number) != 0 || + number < 8U) + return WOLFTFTP_ERR_UNSUPPORTED; + neg->blksize = (uint16_t)number; + } else if (wolftftp_stricmp_local(key, "timeout") == 0) { + if (wolftftp_parse_u32(value, 255U, &number) != 0 || number == 0U) + return WOLFTFTP_ERR_UNSUPPORTED; + neg->timeout_s = (uint16_t)number; + } else if (wolftftp_stricmp_local(key, "tsize") == 0) { + if (wolftftp_parse_u32(value, 0xFFFFFFFFUL, &number) != 0) + return WOLFTFTP_ERR_UNSUPPORTED; + neg->tsize = number; + neg->have_tsize = 1; + } else if (wolftftp_stricmp_local(key, "windowsize") == 0) { + if (wolftftp_parse_u32(value, WOLFTFTP_MAX_WINDOWSIZE, &number) != 0 || + number == 0U) + return WOLFTFTP_ERR_UNSUPPORTED; + neg->windowsize = (uint16_t)number; + } else { + return WOLFTFTP_ERR_UNSUPPORTED; + } + p = value + slen + 1U; + } + return 0; +} + +static int wolftftp_parse_data(const uint8_t *buf, uint16_t len, + struct wolftftp_parsed_data *data) +{ + if (buf == NULL || data == NULL || len < 4) + return WOLFTFTP_ERR_PACKET; + if (wolftftp_read_u16(buf) != WOLFTFTP_OP_DATA) + return WOLFTFTP_ERR_PACKET; + data->block = wolftftp_read_u16(buf + 2); + data->data = buf + 4; + data->data_len = (uint16_t)(len - 4U); + return 0; +} + +static int wolftftp_packet_opcode(const uint8_t *buf, uint16_t len) +{ + if (buf == NULL || len < 2) + return -1; + return wolftftp_read_u16(buf); +} + +static uint32_t wolftftp_deadline(const struct wolftftp_negotiated *neg, + uint32_t now_ms) +{ + uint32_t d = now_ms + ((uint32_t)neg->timeout_s * 1000U); + /* 0 is reserved as the "not armed" sentinel; nudge past it. */ + if (d == 0U) + d = 1U; + return d; +} + +static int wolftftp_send_client_error(struct wolftftp_client *client, + const struct wolftftp_endpoint *remote, uint16_t code, const char *msg) +{ + int len; + uint8_t buf[64]; + + len = wolftftp_build_error(buf, sizeof(buf), code, msg); + if (len < 0) + return len; + return wolftftp_send(&client->transport, client->cfg.local_port, remote, buf, + (uint16_t)len); +} + +static int wolftftp_send_server_error(struct wolftftp_server *server, + uint16_t local_port, const struct wolftftp_endpoint *remote, uint16_t code, + const char *msg) +{ + int len; + uint8_t buf[64]; + + len = wolftftp_build_error(buf, sizeof(buf), code, msg); + if (len < 0) + return len; + return wolftftp_send(&server->transport, local_port, remote, buf, (uint16_t)len); +} + +void wolftftp_client_init(struct wolftftp_client *client, + const struct wolftftp_transport_ops *transport, + const struct wolftftp_io_ops *io, + const struct wolftftp_transfer_cfg *cfg) +{ + memset(client, 0, sizeof(*client)); + if (transport != NULL) + client->transport = *transport; + if (io != NULL) + client->io = *io; + if (cfg != NULL) + client->cfg = *cfg; + wolftftp_cfg_defaults(&client->cfg); + wolftftp_neg_defaults(&client->neg, &client->cfg); + client->state = WOLFTFTP_CLIENT_IDLE; +} + +static int wolftftp_client_open_sink(struct wolftftp_client *client) +{ + uint32_t size_hint = client->advertised_size; + + if (client->io.open == NULL || client->io.write == NULL) + return WOLFTFTP_ERR_IO; + return client->io.open(client->io.arg, client->filename, 1, &size_hint, + &client->handle); +} + +int wolftftp_client_start_rrq(struct wolftftp_client *client, + const struct wolftftp_endpoint *server, const char *filename) +{ + int ret; + + if (client == NULL || server == NULL) + return -WOLFIP_EINVAL; + if (client->state != WOLFTFTP_CLIENT_IDLE && client->state != WOLFTFTP_CLIENT_COMPLETE && + client->state != WOLFTFTP_CLIENT_ERROR) + return WOLFTFTP_ERR_STATE; + ret = wolftftp_copy_string(client->filename, sizeof(client->filename), filename); + if (ret != 0) + return ret; + client->server = *server; + if (client->server.port == 0U) + client->server.port = WOLFTFTP_PORT; + client->tid_locked = 0; + client->expected_block = 1; + client->last_acked_block = 0; + client->window_count = 0; + client->next_offset = 0; + client->total_size = 0; + client->advertised_size = 0; + client->final_seen = 0; + client->handle = NULL; + client->retries = 0; + wolftftp_neg_defaults(&client->neg, &client->cfg); + ret = wolftftp_build_request(client->last_tx, sizeof(client->last_tx), + WOLFTFTP_OP_RRQ, filename, &client->cfg, 0, &client->requested_opts, + &client->last_tx_len); + if (ret != 0) + return ret; + ret = wolftftp_send(&client->transport, client->cfg.local_port, &client->server, + client->last_tx, client->last_tx_len); + if (ret != 0) + return ret; + client->request_sent = 1; + client->state = WOLFTFTP_CLIENT_WAIT_FIRST; + client->last_status = 0; + client->deadline_ms = 0; + return 0; +} + +static int wolftftp_client_accept_data(struct wolftftp_client *client, + const struct wolftftp_parsed_data *data) +{ + int ret; + + if (data->block == client->expected_block) { + if (client->cfg.max_image_size != 0U && + (client->total_size + data->data_len) > client->cfg.max_image_size) { + (void)wolftftp_send_client_error(client, &client->server, + WOLFTFTP_ENOSPACE, "image too large"); + wolftftp_client_finish(client, WOLFTFTP_ERR_SIZE); + return WOLFTFTP_ERR_SIZE; + } + if (client->handle == NULL) { + ret = wolftftp_client_open_sink(client); + if (ret != 0) { + (void)wolftftp_send_client_error(client, &client->server, + WOLFTFTP_EACCESS, "open failed"); + wolftftp_client_finish(client, WOLFTFTP_ERR_IO); + return WOLFTFTP_ERR_IO; + } + } + ret = client->io.write(client->io.arg, client->handle, client->next_offset, + data->data, data->data_len); + if (ret != 0) { + (void)wolftftp_send_client_error(client, &client->server, + WOLFTFTP_EACCESS, "write failed"); + wolftftp_client_finish(client, WOLFTFTP_ERR_IO); + return WOLFTFTP_ERR_IO; + } + if (client->io.hash_update != NULL && data->data_len > 0) { + ret = client->io.hash_update(client->io.arg, client->handle, data->data, + data->data_len); + if (ret != 0) { + (void)wolftftp_send_client_error(client, &client->server, + WOLFTFTP_EUNDEF, "hash failed"); + wolftftp_client_finish(client, WOLFTFTP_ERR_VERIFY); + return WOLFTFTP_ERR_VERIFY; + } + } + client->next_offset += data->data_len; + client->total_size += data->data_len; + client->expected_block++; + client->window_count++; + client->final_seen = (uint8_t)(data->data_len < client->neg.blksize); + if (client->window_count >= client->neg.windowsize || client->final_seen) { + client->last_tx_len = (uint16_t)wolftftp_build_ack(client->last_tx, + data->block); + ret = wolftftp_send(&client->transport, client->cfg.local_port, + &client->server, client->last_tx, client->last_tx_len); + if (ret != 0) + return ret; + client->last_acked_block = data->block; + client->window_count = 0; + } + if (client->final_seen != 0U) { + if (client->io.verify != NULL) { + ret = client->io.verify(client->io.arg, client->handle, + client->total_size); + if (ret != 0) { + wolftftp_client_finish(client, WOLFTFTP_ERR_VERIFY); + return WOLFTFTP_ERR_VERIFY; + } + } + wolftftp_client_finish(client, 0); + } else { + client->state = WOLFTFTP_CLIENT_RECV_DATA; + } + return 0; + } + if (data->block == client->last_acked_block || data->block == (uint16_t)(client->expected_block - 1U)) { + if (client->last_tx_len != 0U) { + return wolftftp_send(&client->transport, client->cfg.local_port, + &client->server, client->last_tx, client->last_tx_len); + } + return 0; + } + return 0; +} + +int wolftftp_client_receive(struct wolftftp_client *client, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len) +{ + int opcode; + struct wolftftp_parsed_data data; + struct wolftftp_negotiated neg; + int ret; + + if (client == NULL || remote == NULL || buf == NULL) + return -WOLFIP_EINVAL; + if (local_port != client->cfg.local_port) + return 0; + if (client->state != WOLFTFTP_CLIENT_WAIT_FIRST && + client->state != WOLFTFTP_CLIENT_RECV_DATA) + return WOLFTFTP_ERR_STATE; + if (remote->ip != client->server.ip) + return 0; + if (client->tid_locked != 0U && remote->port != client->server.port) { + (void)wolftftp_send_client_error(client, remote, WOLFTFTP_EBADTID, + "unknown tid"); + return WOLFTFTP_ERR_TID; + } + + opcode = wolftftp_packet_opcode(buf, len); + if (opcode == WOLFTFTP_OP_OACK) { + client->server.port = remote->port; + client->tid_locked = 1; + wolftftp_neg_defaults(&neg, &client->cfg); + ret = wolftftp_parse_oack(buf, len, &neg); + if (ret != 0) { + wolftftp_client_finish(client, ret); + return ret; + } + if (neg.have_tsize != 0U) { + client->advertised_size = neg.tsize; + if (client->cfg.max_image_size != 0U && neg.tsize > client->cfg.max_image_size) { + (void)wolftftp_send_client_error(client, &client->server, + WOLFTFTP_ENOSPACE, "image too large"); + wolftftp_client_finish(client, WOLFTFTP_ERR_SIZE); + return WOLFTFTP_ERR_SIZE; + } + } + client->neg = neg; + client->last_tx_len = (uint16_t)wolftftp_build_ack(client->last_tx, 0); + ret = wolftftp_send(&client->transport, client->cfg.local_port, &client->server, + client->last_tx, client->last_tx_len); + if (ret != 0) + return ret; + client->last_acked_block = 0; + client->state = WOLFTFTP_CLIENT_RECV_DATA; + client->deadline_ms = 0; + client->retries = 0; + return 0; + } + if (opcode == WOLFTFTP_OP_ERROR) { + wolftftp_client_finish(client, WOLFTFTP_ERR_IO); + return WOLFTFTP_ERR_IO; + } + if (opcode != WOLFTFTP_OP_DATA) + return WOLFTFTP_ERR_PACKET; + + ret = wolftftp_parse_data(buf, len, &data); + if (ret != 0) + return ret; + + if (client->tid_locked == 0U) { + client->server.port = remote->port; + client->tid_locked = 1; + } else if (remote->port != client->server.port) { + (void)wolftftp_send_client_error(client, remote, WOLFTFTP_EBADTID, + "unknown tid"); + return WOLFTFTP_ERR_TID; + } + if (client->state == WOLFTFTP_CLIENT_WAIT_FIRST) { + wolftftp_neg_defaults(&client->neg, &client->cfg); + client->advertised_size = 0; + client->state = WOLFTFTP_CLIENT_RECV_DATA; + } + client->deadline_ms = 0; + client->retries = 0; + return wolftftp_client_accept_data(client, &data); +} + +int wolftftp_client_poll(struct wolftftp_client *client, uint32_t now_ms) +{ + int ret; + + if (client == NULL) + return -WOLFIP_EINVAL; + if (client->state != WOLFTFTP_CLIENT_WAIT_FIRST && + client->state != WOLFTFTP_CLIENT_RECV_DATA) + return 0; + if (client->last_tx_len == 0U) + return 0; + if (client->deadline_ms == 0U) { + client->deadline_ms = wolftftp_deadline(&client->neg, now_ms); + return 0; + } + if (client->deadline_ms != 0U && + (int32_t)(now_ms - client->deadline_ms) < 0) + return 0; + if (client->retries >= client->cfg.max_retries) { + wolftftp_client_finish(client, WOLFTFTP_ERR_TIMEOUT); + return WOLFTFTP_ERR_TIMEOUT; + } + ret = wolftftp_send(&client->transport, client->cfg.local_port, &client->server, + client->last_tx, client->last_tx_len); + if (ret == 0) + client->retries++; + client->deadline_ms = wolftftp_deadline(&client->neg, now_ms); + return ret; +} + +int wolftftp_client_status(const struct wolftftp_client *client) +{ + if (client == NULL) + return -WOLFIP_EINVAL; + return client->last_status; +} + +void wolftftp_server_init(struct wolftftp_server *server, + const struct wolftftp_transport_ops *transport, + const struct wolftftp_io_ops *io, + const struct wolftftp_transfer_cfg *cfg) +{ + memset(server, 0, sizeof(*server)); + if (transport != NULL) + server->transport = *transport; + if (io != NULL) + server->io = *io; + if (cfg != NULL) + server->cfg = *cfg; + wolftftp_cfg_defaults(&server->cfg); + server->listen_port = WOLFTFTP_PORT; + server->transfer_port_base = WOLFTFTP_SERVER_PORT_BASE; +} + +static struct wolftftp_server_session *wolftftp_server_find_session( + struct wolftftp_server *server, uint16_t local_port, + const struct wolftftp_endpoint *remote) +{ + unsigned int i; + + for (i = 0; i < WOLFTFTP_SERVER_MAX_SESSIONS; i++) { + if (server->sessions[i].state != WOLFTFTP_SESSION_FREE && + server->sessions[i].local_port == local_port && + server->sessions[i].remote.ip == remote->ip && + server->sessions[i].remote.port == remote->port) { + return &server->sessions[i]; + } + } + return NULL; +} + +static struct wolftftp_server_session *wolftftp_server_alloc_session( + struct wolftftp_server *server) +{ + unsigned int i; + + for (i = 0; i < WOLFTFTP_SERVER_MAX_SESSIONS; i++) { + if (server->sessions[i].state == WOLFTFTP_SESSION_FREE) { + memset(&server->sessions[i], 0, sizeof(server->sessions[i])); + server->sessions[i].local_port = (uint16_t)(server->transfer_port_base + i); + return &server->sessions[i]; + } + } + return NULL; +} + +static int wolftftp_server_send_last(struct wolftftp_server *server, + struct wolftftp_server_session *session, const uint8_t *buf, uint16_t len) +{ + return wolftftp_send(&server->transport, session->local_port, &session->remote, + buf, len); +} + +static int wolftftp_server_send_window(struct wolftftp_server *server, + struct wolftftp_server_session *session) +{ + uint8_t pkt[WOLFTFTP_PKT_MAX]; + uint16_t out_len; + int is_last; + int ret; + uint16_t i; + uint16_t data_len; + + if (server->io.read == NULL) + return WOLFTFTP_ERR_IO; + /* Snapshot the pre-send state so a retransmit can replay the same + * window instead of advancing into already-sent-but-unacked blocks. */ + session->window_start_offset = session->next_offset; + session->window_start_total = session->total_size; + session->window_start_block = session->next_block; + session->window_start_final = session->final_seen; + for (i = 0; i < session->neg.windowsize; i++) { + out_len = 0; + is_last = 0; + ret = server->io.read(server->io.arg, session->handle, session->next_offset, + pkt + 4, session->neg.blksize, &out_len, &is_last); + if (ret != 0) + return WOLFTFTP_ERR_IO; + data_len = out_len; + ret = wolftftp_build_data(pkt, sizeof(pkt), session->next_block, pkt + 4, + data_len); + if (ret < 0) + return ret; + ret = wolftftp_server_send_last(server, session, pkt, (uint16_t)ret); + if (ret != 0) + return ret; + session->next_offset += data_len; + session->total_size += data_len; + session->window_count++; + session->next_block++; + if (data_len < session->neg.blksize) { + /* A short (possibly 0-byte) DATA is the EOF marker per + * RFC 1350. */ + session->final_seen = 1; + break; + } + if (is_last != 0) { + /* Reader claims no more bytes but the last read filled an + * entire block; we still owe the peer an explicit 0-byte + * DATA so EOF is unambiguous. Break the window now so the + * next ACK triggers another send_window that picks up the + * trailing short/empty read and finalizes the transfer. */ + break; + } + } + session->state = WOLFTFTP_SESSION_SEND_WAIT_ACK; + return 0; +} + +static int wolftftp_server_start_request(struct wolftftp_server *server, + const struct wolftftp_endpoint *remote, const struct wolftftp_parsed_req *req) +{ + struct wolftftp_server_session *session; + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int ret; + uint32_t size_hint; + + session = wolftftp_server_alloc_session(server); + if (session == NULL) + return wolftftp_send_server_error(server, server->listen_port, remote, + WOLFTFTP_EUNDEF, "no slots"); + + session->remote = *remote; + session->is_write = (uint8_t)(req->opcode == WOLFTFTP_OP_WRQ); + session->state = session->is_write ? WOLFTFTP_SESSION_RECV_DATA : + WOLFTFTP_SESSION_SEND_WAIT_ACK; + session->next_block = 1; + session->last_acked_block = 0; + wolftftp_neg_defaults(&session->neg, &server->cfg); + if ((req->opts & WOLFTFTP_OPT_BLKSIZE) != 0U) + session->neg.blksize = req->blksize; + if ((req->opts & WOLFTFTP_OPT_TIMEOUT) != 0U) + session->neg.timeout_s = req->timeout_s; + if ((req->opts & WOLFTFTP_OPT_WINDOWSIZE) != 0U) + session->neg.windowsize = req->windowsize; + session->neg.have_tsize = (uint8_t)((req->opts & WOLFTFTP_OPT_TSIZE) != 0U); + session->neg.tsize = req->tsize; + (void)wolftftp_copy_string(session->filename, sizeof(session->filename), + req->filename); + + if (server->io.open == NULL || + (!session->is_write && server->io.read == NULL) || + (session->is_write && server->io.write == NULL)) { + memset(session, 0, sizeof(*session)); + return wolftftp_send_server_error(server, server->listen_port, remote, + WOLFTFTP_EACCESS, "io unavailable"); + } + size_hint = session->neg.have_tsize != 0U ? session->neg.tsize : 0U; + ret = server->io.open(server->io.arg, req->filename, session->is_write, + &size_hint, &session->handle); + if (ret != 0) { + memset(session, 0, sizeof(*session)); + return wolftftp_send_server_error(server, server->listen_port, remote, + WOLFTFTP_ENOTFOUND, "open failed"); + } + if (!session->is_write) { + session->file_size = size_hint; + if ((req->opts & WOLFTFTP_OPT_TSIZE) != 0U) { + session->neg.have_tsize = 1; + session->neg.tsize = size_hint; + } + } + if (req->opts != 0U) { + ret = wolftftp_build_oack(pkt, sizeof(pkt), &session->neg, req->opts); + if (ret < 0) { + wolftftp_server_finish(server, session, ret); + return ret; + } + session->options_sent = 1; + /* Remember which options the OACK actually carried so the + * timeout retransmit can rebuild the same OACK byte-for-byte + * if it gets lost in flight. */ + session->oack_opts = req->opts; + ret = wolftftp_server_send_last(server, session, pkt, (uint16_t)ret); + if (ret == 0) + session->deadline_ms = 0; + return ret; + } + if (session->is_write) { + ret = wolftftp_build_ack(pkt, 0); + if (ret < 0) { + wolftftp_server_finish(server, session, ret); + return ret; + } + ret = wolftftp_server_send_last(server, session, pkt, (uint16_t)ret); + if (ret == 0) + session->deadline_ms = 0; + return ret; + } + ret = wolftftp_server_send_window(server, session); + if (ret == 0) + session->deadline_ms = 0; + return ret; +} + +static int wolftftp_server_accept_wrq_data(struct wolftftp_server *server, + struct wolftftp_server_session *session, const struct wolftftp_parsed_data *data) +{ + uint8_t pkt[8]; + int ret; + + if (data->block == session->next_block) { + /* First DATA block on a WRQ-with-options is the implicit ACK + * of the OACK; clear options_sent so the timeout path stops + * trying to replay the OACK now that we have entered the + * data phase. */ + session->options_sent = 0; + if (server->cfg.max_image_size != 0U && + (session->total_size + data->data_len) > server->cfg.max_image_size) { + (void)wolftftp_send_server_error(server, session->local_port, + &session->remote, WOLFTFTP_ENOSPACE, "image too large"); + wolftftp_server_finish(server, session, WOLFTFTP_ERR_SIZE); + return WOLFTFTP_ERR_SIZE; + } + ret = server->io.write(server->io.arg, session->handle, session->next_offset, + data->data, data->data_len); + if (ret != 0) { + wolftftp_server_finish(server, session, WOLFTFTP_ERR_IO); + return WOLFTFTP_ERR_IO; + } + if (server->io.hash_update != NULL && data->data_len > 0) { + ret = server->io.hash_update(server->io.arg, session->handle, data->data, + data->data_len); + if (ret != 0) { + wolftftp_server_finish(server, session, WOLFTFTP_ERR_VERIFY); + return WOLFTFTP_ERR_VERIFY; + } + } + session->next_offset += data->data_len; + session->total_size += data->data_len; + session->window_count++; + session->next_block++; + session->final_seen = (uint8_t)(data->data_len < session->neg.blksize); + if (session->window_count >= session->neg.windowsize || session->final_seen != 0U) { + ret = wolftftp_build_ack(pkt, data->block); + if (ret < 0) + return ret; + ret = wolftftp_server_send_last(server, session, pkt, (uint16_t)ret); + if (ret != 0) + return ret; + session->last_acked_block = data->block; + session->window_count = 0; + session->deadline_ms = 0; + session->retries = 0; + } + if (session->final_seen != 0U) { + if (server->io.verify != NULL) { + ret = server->io.verify(server->io.arg, session->handle, + session->total_size); + if (ret != 0) { + wolftftp_server_finish(server, session, WOLFTFTP_ERR_VERIFY); + return WOLFTFTP_ERR_VERIFY; + } + } + wolftftp_server_finish(server, session, 0); + } + return 0; + } + if (data->block == session->last_acked_block || data->block == (uint16_t)(session->next_block - 1U)) { + ret = wolftftp_build_ack(pkt, data->block); + if (ret < 0) + return ret; + return wolftftp_server_send_last(server, session, pkt, (uint16_t)ret); + } + return 0; +} + +int wolftftp_server_receive(struct wolftftp_server *server, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len) +{ + struct wolftftp_server_session *session; + struct wolftftp_parsed_req req; + struct wolftftp_parsed_data data; + int opcode; + int ret; + + if (server == NULL || remote == NULL || buf == NULL) + return -WOLFIP_EINVAL; + opcode = wolftftp_packet_opcode(buf, len); + if (local_port == server->listen_port) { + ret = wolftftp_parse_request(buf, len, &req); + if (ret == WOLFTFTP_ERR_UNSUPPORTED) { + return wolftftp_send_server_error(server, server->listen_port, remote, + WOLFTFTP_EBADOPT, "unsupported"); + } + if (ret != 0) + return wolftftp_send_server_error(server, server->listen_port, remote, + WOLFTFTP_EBADOP, "bad request"); + return wolftftp_server_start_request(server, remote, &req); + } + + session = wolftftp_server_find_session(server, local_port, remote); + if (session == NULL) { + return wolftftp_send_server_error(server, local_port, remote, + WOLFTFTP_EBADTID, "unknown tid"); + } + if (session->is_write != 0U) { + if (opcode == WOLFTFTP_OP_ERROR) { + wolftftp_server_finish(server, session, WOLFTFTP_ERR_IO); + return WOLFTFTP_ERR_IO; + } + ret = wolftftp_parse_data(buf, len, &data); + if (ret != 0) + return ret; + return wolftftp_server_accept_wrq_data(server, session, &data); + } + + if (opcode == WOLFTFTP_OP_ERROR) { + wolftftp_server_finish(server, session, WOLFTFTP_ERR_IO); + return WOLFTFTP_ERR_IO; + } + if (opcode != WOLFTFTP_OP_ACK || len < 4) + return WOLFTFTP_ERR_PACKET; + if (wolftftp_read_u16(buf + 2) != session->last_acked_block && + wolftftp_read_u16(buf + 2) != (uint16_t)(session->next_block - 1U) && + !(session->options_sent != 0U && wolftftp_read_u16(buf + 2) == 0U)) { + return 0; + } + session->last_acked_block = wolftftp_read_u16(buf + 2); + session->window_count = 0; + session->options_sent = 0; + session->deadline_ms = 0; + session->retries = 0; + if (session->final_seen != 0U && session->last_acked_block == (uint16_t)(session->next_block - 1U)) { + wolftftp_server_finish(server, session, 0); + return 0; + } + ret = wolftftp_server_send_window(server, session); + if (ret == 0) + session->deadline_ms = 0; + return ret; +} + +int wolftftp_server_poll(struct wolftftp_server *server, uint32_t now_ms) +{ + unsigned int i; + uint8_t pkt[WOLFTFTP_PKT_MAX]; + int ret; + + if (server == NULL) + return -WOLFIP_EINVAL; + for (i = 0; i < WOLFTFTP_SERVER_MAX_SESSIONS; i++) { + struct wolftftp_server_session *session = &server->sessions[i]; + if (session->state == WOLFTFTP_SESSION_FREE || + session->state == WOLFTFTP_SESSION_COMPLETE) + continue; + if (session->deadline_ms == 0U) { + session->deadline_ms = wolftftp_deadline(&session->neg, now_ms); + continue; + } + if ((int32_t)(now_ms - session->deadline_ms) < 0) + continue; + if (session->retries >= server->cfg.max_retries) { + wolftftp_server_finish(server, session, WOLFTFTP_ERR_TIMEOUT); + continue; + } + if (session->options_sent != 0U) { + /* OACK was the last packet on the wire and has not been + * acked. RFC 2347: when option negotiation was used the + * server MUST replay the OACK on timeout — not a bare + * ACK(0) / ACK(last_acked_block), which the client would + * either ignore or treat as a fatal "illegal operation" + * and abort with EBADOP. */ + ret = wolftftp_build_oack(pkt, sizeof(pkt), &session->neg, + session->oack_opts); + if (ret > 0) { + (void)wolftftp_server_send_last(server, session, pkt, + (uint16_t)ret); + } + } else if (session->is_write != 0U) { + ret = wolftftp_build_ack(pkt, session->last_acked_block); + if (ret >= 0) { + (void)wolftftp_server_send_last(server, session, pkt, (uint16_t)ret); + } + } else { + /* Replay the last unacked window verbatim instead of + * sending fresh blocks. */ + session->next_offset = session->window_start_offset; + session->total_size = session->window_start_total; + session->next_block = session->window_start_block; + session->final_seen = session->window_start_final; + (void)wolftftp_server_send_window(server, session); + } + session->retries++; + session->deadline_ms = wolftftp_deadline(&session->neg, now_ms); + } + return 0; +} diff --git a/src/tftp/wolftftp.h b/src/tftp/wolftftp.h new file mode 100644 index 0000000..eba2e4d --- /dev/null +++ b/src/tftp/wolftftp.h @@ -0,0 +1,269 @@ +/* wolftftp.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +#ifndef WOLFTFTP_H +#define WOLFTFTP_H + +#include "wolfip.h" + +#include + +#ifndef WOLFIP_ENABLE_TFTP +#define WOLFIP_ENABLE_TFTP 0 +#endif + +#ifndef WOLFTFTP_PORT +#define WOLFTFTP_PORT 69U +#endif + +#ifndef WOLFTFTP_DEFAULT_TIMEOUT_S +#define WOLFTFTP_DEFAULT_TIMEOUT_S 1U +#endif + +#ifndef WOLFTFTP_MAX_RETRIES +#define WOLFTFTP_MAX_RETRIES 5U +#endif + +#ifndef WOLFTFTP_MAX_FILENAME +#define WOLFTFTP_MAX_FILENAME 128U +#endif + +#ifndef WOLFTFTP_MAX_BLKSIZE +#define WOLFTFTP_MAX_BLKSIZE 1428U +#endif + +#ifndef WOLFTFTP_DEFAULT_BLKSIZE +#define WOLFTFTP_DEFAULT_BLKSIZE 512U +#endif + +#ifndef WOLFTFTP_MAX_WINDOWSIZE +#define WOLFTFTP_MAX_WINDOWSIZE 8U +#endif + +#ifndef WOLFTFTP_SERVER_MAX_SESSIONS +#define WOLFTFTP_SERVER_MAX_SESSIONS 4U +#endif + +#ifndef WOLFTFTP_SERVER_PORT_BASE +#define WOLFTFTP_SERVER_PORT_BASE 20000U +#endif + +/* Worst-case RRQ/WRQ on the wire: + * opcode(2) + filename(MAX_FILENAME, null-terminated) + "octet\0"(6) + * + blksize/value(13) + timeout/value(12) + windowsize/value(13) + * + tsize/value(17) = 63 + MAX_FILENAME. The constant below adds a + * generous margin so future options do not silently truncate. */ +#define WOLFTFTP_REQ_BUF_MAX (WOLFTFTP_MAX_FILENAME + 128U) + +#define WOLFTFTP_ERR_IO (-1000) +#define WOLFTFTP_ERR_STATE (-1001) +#define WOLFTFTP_ERR_PACKET (-1002) +#define WOLFTFTP_ERR_TIMEOUT (-1003) +#define WOLFTFTP_ERR_SIZE (-1004) +#define WOLFTFTP_ERR_VERIFY (-1005) +#define WOLFTFTP_ERR_UNSUPPORTED (-1006) +#define WOLFTFTP_ERR_TID (-1007) +#define WOLFTFTP_ERR_NO_SLOT (-1008) + +enum wolftftp_opcode { + WOLFTFTP_OP_RRQ = 1, + WOLFTFTP_OP_WRQ = 2, + WOLFTFTP_OP_DATA = 3, + WOLFTFTP_OP_ACK = 4, + WOLFTFTP_OP_ERROR = 5, + WOLFTFTP_OP_OACK = 6 +}; + +enum wolftftp_error_code { + WOLFTFTP_EUNDEF = 0, + WOLFTFTP_ENOTFOUND = 1, + WOLFTFTP_EACCESS = 2, + WOLFTFTP_ENOSPACE = 3, + WOLFTFTP_EBADOP = 4, + WOLFTFTP_EBADTID = 5, + WOLFTFTP_EEXISTS = 6, + WOLFTFTP_ENOUSER = 7, + WOLFTFTP_EBADOPT = 8 +}; + +enum wolftftp_client_state { + WOLFTFTP_CLIENT_IDLE = 0, + WOLFTFTP_CLIENT_WAIT_FIRST, + WOLFTFTP_CLIENT_RECV_DATA, + WOLFTFTP_CLIENT_COMPLETE, + WOLFTFTP_CLIENT_ERROR +}; + +enum wolftftp_session_state { + WOLFTFTP_SESSION_FREE = 0, + WOLFTFTP_SESSION_SEND_WAIT_ACK, + WOLFTFTP_SESSION_RECV_DATA, + WOLFTFTP_SESSION_COMPLETE, + WOLFTFTP_SESSION_ERROR +}; + +struct wolftftp_endpoint { + uint32_t ip; + uint16_t port; +}; + +struct wolftftp_transfer_cfg { + uint16_t local_port; + uint16_t blksize; + uint16_t timeout_s; + uint16_t windowsize; + uint16_t max_retries; + uint32_t max_image_size; +}; + +struct wolftftp_negotiated { + uint16_t blksize; + uint16_t timeout_s; + uint16_t windowsize; + uint32_t tsize; + uint8_t have_tsize; +}; + +typedef int (*wolftftp_udp_send_cb)(void *arg, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len); +typedef int (*wolftftp_open_cb)(void *arg, const char *name, int is_write, + uint32_t *size_hint, void **handle); +/* wolftftp_read_cb: + * Out-params: + * *out_len: number of bytes copied into buf (<= max_len). + * *is_last: hint that no further reads will produce more data. The + * server may still call read once more to obtain the + * trailing 0-byte block required by RFC 1350 when the + * final useful read happened to fill an entire blksize. + * Callbacks that simply return *out_len == 0 at EOF do + * not need to set this flag. */ +typedef int (*wolftftp_read_cb)(void *arg, void *handle, uint32_t offset, + uint8_t *buf, uint16_t max_len, uint16_t *out_len, int *is_last); +typedef int (*wolftftp_write_cb)(void *arg, void *handle, uint32_t offset, + const uint8_t *buf, uint16_t len); +typedef int (*wolftftp_hash_update_cb)(void *arg, void *handle, + const uint8_t *buf, uint16_t len); +typedef int (*wolftftp_verify_cb)(void *arg, void *handle, uint32_t total_size); +typedef void (*wolftftp_close_cb)(void *arg, void *handle, int status); + +struct wolftftp_io_ops { + wolftftp_open_cb open; + wolftftp_read_cb read; + wolftftp_write_cb write; + wolftftp_hash_update_cb hash_update; + wolftftp_verify_cb verify; + wolftftp_close_cb close; + void *arg; +}; + +struct wolftftp_transport_ops { + wolftftp_udp_send_cb send; + void *arg; +}; + +struct wolftftp_client { + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_transfer_cfg cfg; + struct wolftftp_negotiated neg; + struct wolftftp_endpoint server; + void *handle; + uint8_t last_tx[WOLFTFTP_REQ_BUF_MAX]; + uint16_t last_tx_len; + uint32_t next_offset; + uint32_t total_size; + uint32_t advertised_size; + uint32_t deadline_ms; + uint16_t expected_block; + uint16_t last_acked_block; + uint16_t window_count; + uint16_t retries; + int last_status; + enum wolftftp_client_state state; + uint8_t requested_opts; + uint8_t final_seen; + uint8_t request_sent; + uint8_t tid_locked; + char filename[WOLFTFTP_MAX_FILENAME]; +}; + +struct wolftftp_server_session { + struct wolftftp_endpoint remote; + struct wolftftp_negotiated neg; + void *handle; + uint32_t next_offset; + uint32_t total_size; + uint32_t file_size; + uint32_t deadline_ms; + /* Snapshot of next_offset/total_size/next_block at the start of the + * last RRQ window send, used to replay the window on a retransmit + * instead of advancing into unacknowledged territory. */ + uint32_t window_start_offset; + uint32_t window_start_total; + uint16_t window_start_block; + uint8_t window_start_final; + uint16_t local_port; + uint16_t next_block; + uint16_t last_acked_block; + uint16_t window_count; + uint16_t retries; + uint8_t is_write; + uint8_t options_sent; + /* Bitmask of options that were actually negotiated and emitted in + * the pending OACK. We keep it around so a lost OACK can be + * replayed verbatim from the timeout path, instead of falling + * back to ACK(0) which is not protocol-correct after option + * negotiation (RFC 2347). */ + uint8_t oack_opts; + uint8_t final_seen; + int last_status; + enum wolftftp_session_state state; + char filename[WOLFTFTP_MAX_FILENAME]; +}; + +struct wolftftp_server { + struct wolftftp_transport_ops transport; + struct wolftftp_io_ops io; + struct wolftftp_transfer_cfg cfg; + uint16_t listen_port; + uint16_t transfer_port_base; + struct wolftftp_server_session sessions[WOLFTFTP_SERVER_MAX_SESSIONS]; +}; + +void wolftftp_client_init(struct wolftftp_client *client, + const struct wolftftp_transport_ops *transport, + const struct wolftftp_io_ops *io, + const struct wolftftp_transfer_cfg *cfg); +int wolftftp_client_start_rrq(struct wolftftp_client *client, + const struct wolftftp_endpoint *server, const char *filename); +int wolftftp_client_receive(struct wolftftp_client *client, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len); +int wolftftp_client_poll(struct wolftftp_client *client, uint32_t now_ms); +int wolftftp_client_status(const struct wolftftp_client *client); + +void wolftftp_server_init(struct wolftftp_server *server, + const struct wolftftp_transport_ops *transport, + const struct wolftftp_io_ops *io, + const struct wolftftp_transfer_cfg *cfg); +int wolftftp_server_receive(struct wolftftp_server *server, uint16_t local_port, + const struct wolftftp_endpoint *remote, const uint8_t *buf, uint16_t len); +int wolftftp_server_poll(struct wolftftp_server *server, uint32_t now_ms); + +#endif diff --git a/src/wolfip.c b/src/wolfip.c index c0b458f..8a1b3c0 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1136,6 +1136,12 @@ struct tcpsocket { /* UDP socket */ struct udpsocket { struct fifo rxbuf, txbuf; + /* POSIX UDP sockets are unconnected by default: sendto(addr) sets + * only the destination of that datagram, not a persistent RX + * filter. Only an explicit wolfIP_sock_connect() narrows incoming + * delivery to a specific peer. udp_try_recv consults this flag + * before honouring dst_port/remote_ip as match constraints. */ + uint8_t connected; #ifdef IP_MULTICAST struct udp_mcast_join mcast[WOLFIP_UDP_MCAST_MEMBERSHIPS]; uint8_t mcast_ttl; @@ -2279,19 +2285,27 @@ static void udp_try_recv(struct wolfIP *s, unsigned int if_idx, for (i = 0; i < MAX_UDPSOCKETS; i++) { struct tsocket *t = &s->udpsockets[i]; uint32_t expected_len; + /* Only connected UDP sockets restrict by the peer's + * ip/port. Unconnected sockets (sendto-only or pure listeners) + * must accept datagrams from any source, per POSIX. This is + * required by protocols where the server's reply originates + * from a different port than the one the request was sent to + * (TFTP TID change, RFC 1350; certain DHCP relay setups). */ + int peer_match = (t->sock.udp.connected == 0) || + ((t->dst_port == 0 || t->dst_port == ee16(udp->src_port)) && + (t->remote_ip == 0 || t->remote_ip == src_ip)); int addr_match = (((t->local_ip == 0) && DHCP_IS_RUNNING(s)) || - (t->local_ip == dst_ip && (t->remote_ip == 0 || t->remote_ip == src_ip))); + (t->local_ip == dst_ip && peer_match)); #ifdef IP_MULTICAST if (wolfIP_ip_is_multicast(dst_ip)) { addr_match = udp_socket_has_mcast(t, if_idx, dst_ip) && - (t->remote_ip == 0 || t->remote_ip == src_ip || + (t->sock.udp.connected == 0 || + t->remote_ip == 0 || t->remote_ip == src_ip || t->remote_ip == dst_ip); } #endif - if (t->src_port == ee16(udp->dst_port) && - (t->dst_port == 0 || t->dst_port == ee16(udp->src_port)) && - addr_match) { + if (t->src_port == ee16(udp->dst_port) && addr_match) { if (t->local_ip == 0) t->if_idx = (uint8_t)if_idx; @@ -5355,34 +5369,50 @@ int wolfIP_sock_connect(struct wolfIP *s, int sockfd, const struct wolfIP_sockad sin = (const struct wolfIP_sockaddr_in *)addr; if (IS_SOCKET_UDP(sockfd)) { struct ipconf *conf; + uint16_t new_dst_port; + ip4 new_remote_ip; + uint8_t new_if_idx; + ip4 new_local_ip; + if (SOCKET_UNMARK(sockfd) >= MAX_UDPSOCKETS) return -WOLFIP_EINVAL; - ts = &s->udpsockets[SOCKET_UNMARK(sockfd)]; if (addrlen < sizeof(struct wolfIP_sockaddr_in)) return -WOLFIP_EINVAL; if (sin->sin_family != AF_INET) return -WOLFIP_EINVAL; - ts->dst_port = ee16(sin->sin_port); - ts->remote_ip = ee32(sin->sin_addr.s_addr); + + /* Resolve everything into locals first; the socket's + * dst_port/remote_ip/connected fields are observed by + * udp_try_recv to gate the peer RX filter, so they must not + * be left mutated if any validation below fails. */ + new_dst_port = ee16(sin->sin_port); + new_remote_ip = ee32(sin->sin_addr.s_addr); if (ts->bound_local_ip != IPADDR_ANY) { int bound_match = 0; - unsigned int bound_if = wolfIP_if_for_local_ip(s, ts->bound_local_ip, &bound_match); + unsigned int bound_if = wolfIP_if_for_local_ip(s, + ts->bound_local_ip, &bound_match); if (!bound_match) return -WOLFIP_EINVAL; - ts->if_idx = (uint8_t)bound_if; - ts->local_ip = ts->bound_local_ip; + new_if_idx = (uint8_t)bound_if; + new_local_ip = ts->bound_local_ip; } else { - if_idx = wolfIP_route_for_ip(s, ts->remote_ip); + if_idx = wolfIP_route_for_ip(s, new_remote_ip); conf = wolfIP_ipconf_at(s, if_idx); - ts->if_idx = (uint8_t)if_idx; + new_if_idx = (uint8_t)if_idx; if (conf && conf->ip != IPADDR_ANY) - ts->local_ip = conf->ip; + new_local_ip = conf->ip; else { struct ipconf *primary = wolfIP_primary_ipconf(s); - ts->local_ip = (primary && primary->ip != IPADDR_ANY) ? primary->ip : IPADDR_ANY; + new_local_ip = (primary && primary->ip != IPADDR_ANY) + ? primary->ip : IPADDR_ANY; } } + ts->dst_port = new_dst_port; + ts->remote_ip = new_remote_ip; + ts->if_idx = new_if_idx; + ts->local_ip = new_local_ip; + ts->sock.udp.connected = 1; return 0; } else if (IS_SOCKET_ICMP(sockfd)) { struct ipconf *conf; diff --git a/tools/scripts/tftpd-hpa-wolfip.conf b/tools/scripts/tftpd-hpa-wolfip.conf new file mode 100644 index 0000000..7e068b5 --- /dev/null +++ b/tools/scripts/tftpd-hpa-wolfip.conf @@ -0,0 +1,30 @@ +# Reference tftpd-hpa configuration for the wolfIP TFTP interop test. +# +# This file is a documentation template, not a runtime input: it +# documents the canonical settings the interop test exercises so the +# same daemon can be dropped in as a system service later. The +# interop test (src/test/test_tftp_interop.c) does NOT parse this +# file — it embeds the equivalent flags directly in its execl() of +# /usr/sbin/in.tftpd. Keep this file and the test's hardcoded flags +# in sync by hand when either changes. +# +# Follows the /etc/default/tftpd-hpa format shipped by the Debian / +# Ubuntu tftpd-hpa package. + +# Run in.tftpd as this user. The interop test runs as root (it needs to +# set up the tap interface) and recent in.tftpd refuses to start as +# root unless this is set explicitly. +TFTP_USERNAME="root" + +# Root directory served over TFTP. The test recreates this dir before +# launching tftpd-hpa and drops the canonical fixture file inside it. +TFTP_DIRECTORY="/tmp/wolfip-tftp-root" + +# Bind address for in.tftpd. Must match HOST_STACK_IP in config.h so the +# wolfIP stack can reach the daemon over the tap link, and uses a +# non-privileged port so the daemon can run without further setup. +TFTP_ADDRESS="10.10.10.1:6969" + +# Standalone listen mode (no inetd), verbose logging on stderr, allow +# the wolfIP client to send block/timeout/tsize/windowsize options. +TFTP_OPTIONS="--listen --foreground --verbose"