diff options
| -rw-r--r-- | cmd/Kconfig | 14 | ||||
| -rw-r--r-- | cmd/lwip/Makefile | 1 | ||||
| -rw-r--r-- | cmd/lwip/tftpsrv.c | 11 | ||||
| -rw-r--r-- | cmd/net.c | 4 | ||||
| -rw-r--r-- | configs/qemu_arm64_lwip_defconfig | 1 | ||||
| -rw-r--r-- | doc/usage/cmd/tftpsrv.rst | 73 | ||||
| -rw-r--r-- | include/net-lwip.h | 1 | ||||
| -rw-r--r-- | lib/lwip/u-boot/lwipopts.h | 11 | ||||
| -rw-r--r-- | net/lwip/tftp.c | 188 | ||||
| -rw-r--r-- | test/py/tests/test_net.py | 94 |
10 files changed, 385 insertions, 13 deletions
diff --git a/cmd/Kconfig b/cmd/Kconfig index 35ce42f8397..ff90a87024c 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -2135,12 +2135,6 @@ config CMD_TFTPPUT help TFTP put command, for uploading files to a server -config CMD_TFTPSRV - bool "tftpsrv" - depends on CMD_TFTPBOOT - help - Act as a TFTP server and boot the first received file - config NET_TFTP_VARS bool "Control TFTP timeout and count through environment" depends on CMD_TFTPBOOT @@ -2287,6 +2281,14 @@ config CMD_TFTPBOOT help tftpboot - load file via network using TFTP protocol +config CMD_TFTPSRV + bool "tftpsrv" + depends on CMD_TFTPBOOT + help + Act as a TFTP server and receive the first incoming file into + memory. The command returns successfully after the transfer so + boot scripts can boot the received image from the load address. + config CMD_WGET bool "wget" default y if SANDBOX || ARCH_QEMU diff --git a/cmd/lwip/Makefile b/cmd/lwip/Makefile index 90df1f5511c..245683a9672 100644 --- a/cmd/lwip/Makefile +++ b/cmd/lwip/Makefile @@ -4,4 +4,5 @@ obj-$(CONFIG_CMD_NFS) += nfs.o obj-$(CONFIG_CMD_PING) += ping.o obj-$(CONFIG_CMD_SNTP) += sntp.o obj-$(CONFIG_CMD_TFTPBOOT) += tftp.o +obj-$(CONFIG_CMD_TFTPSRV) += tftpsrv.o obj-$(CONFIG_CMD_WGET) += wget.o diff --git a/cmd/lwip/tftpsrv.c b/cmd/lwip/tftpsrv.c new file mode 100644 index 00000000000..6370c900489 --- /dev/null +++ b/cmd/lwip/tftpsrv.c @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include <command.h> +#include <net.h> + +U_BOOT_CMD(tftpsrv, 2, 1, do_tftpsrv, + "act as a TFTP server and receive the first file", + "[loadAddress]\n" + "Listen for an incoming TFTP transfer and receive a file into memory.\n" + "The transfer is aborted if a transfer has not been started after\n" + "about 50 seconds or if Ctrl-C is pressed."); diff --git a/cmd/net.c b/cmd/net.c index f6f556f36ae..0f0b2386d87 100644 --- a/cmd/net.c +++ b/cmd/net.c @@ -89,9 +89,9 @@ static int do_tftpsrv(struct cmd_tbl *cmdtp, int flag, int argc, U_BOOT_CMD( tftpsrv, 2, 1, do_tftpsrv, - "act as a TFTP server and boot the first received file", + "act as a TFTP server and receive the first file", "[loadAddress]\n" - "Listen for an incoming TFTP transfer, receive a file and boot it.\n" + "Listen for an incoming TFTP transfer and receive a file into memory.\n" "The transfer is aborted if a transfer has not been started after\n" "about 50 seconds or if Ctrl-C is pressed." ); diff --git a/configs/qemu_arm64_lwip_defconfig b/configs/qemu_arm64_lwip_defconfig index a974970c3d3..06ed5a131db 100644 --- a/configs/qemu_arm64_lwip_defconfig +++ b/configs/qemu_arm64_lwip_defconfig @@ -7,6 +7,7 @@ CONFIG_NET_LWIP=y CONFIG_CMD_DNS=y CONFIG_CMD_NFS=y CONFIG_CMD_SNTP=y +CONFIG_CMD_TFTPSRV=y CONFIG_CMD_WGET=y CONFIG_EFI_HTTP_BOOT=y CONFIG_WGET_HTTPS=y diff --git a/doc/usage/cmd/tftpsrv.rst b/doc/usage/cmd/tftpsrv.rst new file mode 100644 index 00000000000..d067afdba57 --- /dev/null +++ b/doc/usage/cmd/tftpsrv.rst @@ -0,0 +1,73 @@ +.. SPDX-License-Identifier: GPL-2.0+ + +.. index:: + single: tftpsrv (command) + +tftpsrv command +=============== + +Synopsis +-------- + +:: + + tftpsrv [loadAddress] + +Description +----------- + +The tftpsrv command listens for an incoming TFTP write request and receives +the first transferred file into memory. + +loadAddress + memory address where the received file is stored. If not provided, the + address is taken from the *loadaddr* environment variable or the default + image load address. + +After a successful transfer, the *fileaddr* and *filesize* environment +variables describe the received file. The command returns successfully after +the transfer has completed. It does not boot the file automatically; boot +scripts can use commands such as bootm, booti or bootefi to boot from the +load address. + +The transfer is aborted if no transfer has started after about 50 seconds or +if Ctrl-C is pressed. + +Example +------- + +In the example the following steps are executed: + +* setup the board network address +* receive a FIT image from a host +* boot the received FIT image + +:: + + => setenv autoload no + => dhcp + BOOTP broadcast 1 + DHCP client bound to address 192.168.1.40 (7 ms) + => tftpsrv $loadaddr + Using ethernet@1c30000 device + Listening for TFTP transfer on 192.168.1.40 + Load address: 0x42000000 + Loading: ################################################################# + 6.5 MiB/s + done + Bytes transferred = 1048576 (100000 hex) + => bootm $fileaddr + +On the host, send the file to the board while U-Boot is listening: + +:: + + $ curl --upload-file image.fit tftp://192.168.1.40/image.fit + +Configuration +------------- + +The command is only available if CONFIG_CMD_TFTPSRV=y. + +The command is supported by both the legacy network stack and the lwIP network +stack. diff --git a/include/net-lwip.h b/include/net-lwip.h index 5d0627eb271..8e59a2299e0 100644 --- a/include/net-lwip.h +++ b/include/net-lwip.h @@ -53,6 +53,7 @@ bool wget_validate_uri(char *uri); int do_dns(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]); int do_nfs(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]); +int do_tftpsrv(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]); int do_wget(struct cmd_tbl *cmdtp, int flag, int argc, char * const argv[]); #endif /* __NET_LWIP_H__ */ diff --git a/lib/lwip/u-boot/lwipopts.h b/lib/lwip/u-boot/lwipopts.h index 8dae004f1a2..1c1adc57c6b 100644 --- a/lib/lwip/u-boot/lwipopts.h +++ b/lib/lwip/u-boot/lwipopts.h @@ -72,6 +72,17 @@ #define PBUF_POOL_RESERVE 4 #define TFTP_BLOCKSIZE_THRESHOLD 4096 +/* + * Match the legacy U-Boot TFTP filename buffer. The legacy + * CONFIG_TFTP_FILE_NAME_MAX_LEN value is a buffer size including the trailing + * NUL, while lwIP's TFTP_MAX_FILENAME_LEN is the usable string length. + */ +#ifdef CONFIG_TFTP_FILE_NAME_MAX_LEN +#define TFTP_MAX_FILENAME_LEN (CONFIG_TFTP_FILE_NAME_MAX_LEN - 1) +#else +#define TFTP_MAX_FILENAME_LEN 127 +#endif + #if defined(CONFIG_TFTP_BLOCKSIZE) && (CONFIG_TFTP_BLOCKSIZE > TFTP_BLOCKSIZE_THRESHOLD) #define PBUF_POOL_SIZE (((CONFIG_TFTP_BLOCKSIZE + (IP_FRAG_MTU_USABLE - 1)) / \ IP_FRAG_MTU_USABLE) + PBUF_POOL_HEADROOM) diff --git a/net/lwip/tftp.c b/net/lwip/tftp.c index 571c38172f9..d6a9f29a260 100644 --- a/net/lwip/tftp.c +++ b/net/lwip/tftp.c @@ -11,6 +11,7 @@ #include <linux/delay.h> #include <linux/kconfig.h> #include <lwip/apps/tftp_client.h> +#include <lwip/apps/tftp_server.h> #include <lwip/timeouts.h> #include <mapmem.h> #include <net.h> @@ -19,6 +20,8 @@ #define PROGRESS_PRINT_STEP_BYTES (10 * 1024) /* Max time to wait for first data packet from server */ #define NO_RSP_TIMEOUT_MS 10000 +/* Max time to wait for an incoming TFTP write request */ +#define TFTPSRV_LISTEN_TIMEOUT_MS 50000 enum done_state { NOT_DONE = 0, @@ -34,8 +37,31 @@ struct tftp_ctx { ulong hash_count; ulong start_time; enum done_state done; + bool is_server; + bool wrq_accepted; + char fname[TFTP_MAX_FILENAME_LEN + 1]; }; +/* + * The lwIP TFTP server open callback has no user-data argument. Keep the + * current server context here so tftp_open() can return it. + */ +static struct tftp_ctx *tftpsrv_active_ctx; + +static void transfer_timeout(void *arg) +{ + struct tftp_ctx *ctx = (struct tftp_ctx *)arg; + + printf("Timeout!\n"); + ctx->done = FAILURE; +} + +static void restart_transfer_timeout(struct tftp_ctx *ctx) +{ + sys_untimeout(transfer_timeout, ctx); + sys_timeout(TFTP_TIMEOUT_MSECS, transfer_timeout, ctx); +} + /** * store_block() - copy received data * @@ -71,7 +97,7 @@ static int store_block(struct tftp_ctx *ctx, void *src, u16_t len) ctx->size += len; ctx->block_count++; - tftp_tsize = tftp_client_get_tsize(); + tftp_tsize = ctx->is_server ? 0 : tftp_client_get_tsize(); if (tftp_tsize) { pos = clamp(ctx->size, 0UL, tftp_tsize); @@ -92,7 +118,20 @@ static int store_block(struct tftp_ctx *ctx, void *src, u16_t len) static void *tftp_open(const char *fname, const char *mode, u8_t is_write) { - return NULL; + struct tftp_ctx *ctx = tftpsrv_active_ctx; + + if (!IS_ENABLED(CONFIG_CMD_TFTPSRV) || !ctx || !is_write) + return NULL; + + ctx->wrq_accepted = true; + ctx->start_time = get_timer(0); + snprintf(ctx->fname, sizeof(ctx->fname), "%s", fname); + restart_transfer_timeout(ctx); + + printf("\nReceiving '%s' mode '%s'\n", fname, mode); + puts("Loading: "); + + return ctx; } static void tftp_close(void *handle) @@ -101,13 +140,15 @@ static void tftp_close(void *handle) ulong tftp_tsize; ulong elapsed; + sys_untimeout(transfer_timeout, ctx); + if (ctx->done == FAILURE || ctx->done == ABORTED) { /* Closing after an error or Ctrl-C */ return; } ctx->done = SUCCESS; - tftp_tsize = tftp_client_get_tsize(); + tftp_tsize = ctx->is_server ? 0 : tftp_client_get_tsize(); if (tftp_tsize) { /* Print hash marks for the last packet received */ while (ctx->hash_count < 49) { @@ -142,9 +183,14 @@ static int tftp_write(void *handle, struct pbuf *p) struct tftp_ctx *ctx = handle; struct pbuf *q; - for (q = p; q; q = q->next) - if (store_block(ctx, q->payload, q->len) < 0) + for (q = p; q; q = q->next) { + if (store_block(ctx, q->payload, q->len) < 0) { + ctx->done = FAILURE; return -1; + } + } + + restart_transfer_timeout(ctx); return 0; } @@ -204,6 +250,9 @@ static int tftp_loop(struct udevice *udev, ulong addr, char *fname, ctx.block_count = 0; ctx.hash_count = 0; ctx.daddr = addr; + ctx.is_server = false; + ctx.wrq_accepted = false; + ctx.fname[0] = '\0'; printf("Using %s device\n", udev->name); printf("TFTP from server %s; our IP address is %s\n", @@ -258,6 +307,135 @@ static int tftp_loop(struct udevice *udev, ulong addr, char *fname, return -1; } +static void no_request(void *arg) +{ + struct tftp_ctx *ctx = (struct tftp_ctx *)arg; + + if (ctx->wrq_accepted) + return; + + printf("Timeout!\n"); + ctx->done = FAILURE; +} + +static int tftpsrv_loop(struct udevice *udev, ulong addr) +{ + struct netif *netif; + struct tftp_ctx ctx; + const char *ipaddr; + int ret = -1; + err_t err; + + if (addr == 0) + return -1; + + ipaddr = env_get("ipaddr"); + if (!ipaddr || !*ipaddr) { + log_err("error: ipaddr has to be set\n"); + return -1; + } + + netif = net_lwip_new_netif(udev); + if (!netif) + return -1; + + memset(&ctx, 0, sizeof(ctx)); + ctx.done = NOT_DONE; + ctx.daddr = addr; + ctx.is_server = true; + + printf("Using %s device\n", udev->name); + printf("Listening for TFTP transfer on %s\n", ipaddr); + printf("Load address: 0x%lx\n", ctx.daddr); + + tftpsrv_active_ctx = &ctx; + err = tftp_init_server(&tftp_context); + if (err != ERR_OK) { + log_err("tftp_init_server err: %d\n", err); + goto out_remove_netif; + } + + ctx.start_time = get_timer(0); + sys_timeout(TFTPSRV_LISTEN_TIMEOUT_MS, no_request, &ctx); + while (!ctx.done) { + net_lwip_rx(udev, netif); + if (ctrlc()) { + printf("\nAbort\n"); + ctx.done = ABORTED; + break; + } + } + sys_untimeout(no_request, &ctx); + sys_untimeout(transfer_timeout, &ctx); + + tftp_cleanup(); + + if (ctx.done == SUCCESS) { + if (env_set_hex("fileaddr", addr)) { + log_err("fileaddr not updated\n"); + goto out_remove_netif; + } + efi_set_bootdev("Net", "", ctx.fname, map_sysmem(addr, 0), + ctx.size); + ret = 0; + } + +out_remove_netif: + tftpsrv_active_ctx = NULL; + net_lwip_remove_netif(netif); + + return ret; +} + +int do_tftpsrv(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]) +{ + int ret = CMD_RET_SUCCESS; + char *end; + ulong laddr; + ulong addr; + + if (!IS_ENABLED(CONFIG_CMD_TFTPSRV)) + return CMD_RET_FAILURE; + + laddr = env_get_ulong("loadaddr", 16, image_load_addr); + + switch (argc) { + case 1: + break; + case 2: + addr = hextoul(argv[1], &end); + if (end == argv[1] || *end) { + ret = CMD_RET_USAGE; + goto out; + } + laddr = addr; + break; + default: + ret = CMD_RET_USAGE; + goto out; + } + + if (!laddr) { + log_err("error: no load address\n"); + ret = CMD_RET_FAILURE; + goto out; + } + + if (net_lwip_eth_start() < 0) { + ret = CMD_RET_FAILURE; + goto out; + } + + if (tftpsrv_loop(eth_get_dev(), laddr) < 0) + ret = CMD_RET_FAILURE; + else + image_load_addr = laddr; + net_lwip_eth_stop(); + +out: + return ret; +} + int do_tftpb(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]) { int ret = CMD_RET_SUCCESS; diff --git a/test/py/tests/test_net.py b/test/py/tests/test_net.py index 27cdd73fd49..a2007c2fd3a 100644 --- a/test/py/tests/test_net.py +++ b/test/py/tests/test_net.py @@ -59,6 +59,19 @@ For example: 'fnu': 'ubtest-upload.bin', } + # Details regarding a file that may be written to U-Boot using the tftpsrv + # command. This variable may be omitted or set to None if tftpsrv testing + # is not possible or desired. The test uses host-side curl TFTP support to + # upload a generated file to U-Boot. The optional tftpsrv_url entry may be + # used when the host must use a forwarded address instead of U-Boot's + # ipaddr value. + env__net_tftpsrv_file = { + 'fn': 'ubtest-tftpsrv.bin', + 'addr': 0x10000000, + 'size': 4096, + 'timeout': 50000, + } + # Details regarding a file that may be read from a NFS server. This variable # may be omitted or set to None if NFS testing is not possible or desired. env__net_nfs_readable_file = { @@ -89,6 +102,8 @@ import utils import uuid import datetime import re +import tempfile +import zlib net_set_up = False net6_set_up = False @@ -460,3 +475,82 @@ def test_net_tftpput(ubman): output = ubman.run_command("crc32 $fileaddr $filesize") assert expected_tftpb_crc in output + + [email protected]("cmd_crc32") [email protected]("cmd_tftpsrv") [email protected]("curl") +def test_net_tftpsrv(ubman): + """Test the tftpsrv command. + + A file is generated on the host, uploaded to U-Boot using TFTP and then + validated in U-Boot using its size and CRC32. + + The details of the file to upload are provided by the boardenv_* file; + see the comment at the beginning of this file. + """ + + if not net_set_up: + pytest.skip("Network not initialized") + + f = ubman.config.env.get("env__net_tftpsrv_file", None) + if not f: + pytest.skip("No tftpsrv file to write") + + curl_version = utils.run_and_log(ubman, ["curl", "--version"]) + if "tftp" not in curl_version.split(): + pytest.skip("curl does not support TFTP") + + addr = f.get("addr", None) + if not addr: + addr = utils.find_ram_base(ubman) + + timeout = f.get("timeout", ubman.p.timeout) + timeout_secs = max(1, (timeout + 999) // 1000) + size = f.get("size", 4096) + fn = f.get("fn", "ubtest-tftpsrv.bin") + url = f.get("tftpsrv_url", None) + data = bytes([i % 251 for i in range(size)]) + crc = "%08x" % (zlib.crc32(data) & 0xffffffff) + + ip = ubman.run_command("echo $ipaddr").strip() + if not ip: + pytest.skip("No U-Boot IP address") + if not url: + url = "tftp://%s/%s" % (ip, fn) + + with tempfile.NamedTemporaryFile() as tmp: + tmp.write(data) + tmp.flush() + + done = False + with ubman.temporary_timeout(timeout): + try: + ubman.run_command("tftpsrv %x" % addr, + wait_for_prompt=False) + ubman.wait_for("Listening for TFTP transfer") + utils.run_and_log( + ubman, + [ + "curl", + "--fail", + "--max-time", + str(timeout_secs), + "--upload-file", + tmp.name, + url, + ], + ) + ubman.wait_for("Bytes transferred = %d" % size) + ubman.wait_for(ubman.prompt) + done = True + finally: + if not done: + ubman.ctrlc() + ubman.drain_console() + + output = ubman.run_command("echo $filesize") + assert "%x" % size in output + + output = ubman.run_command("crc32 $fileaddr $filesize") + assert crc in output |
