From 2034fa510113531876e7a9926f41daab246bdf89 Mon Sep 17 00:00:00 2001 From: Benjamin Valentin Date: Tue, 10 Aug 2021 17:37:22 +0200 Subject: [PATCH] sys/net/application_layer: add telnet server module --- makefiles/pseudomodules.inc.mk | 1 + makefiles/stdio.inc.mk | 6 + sys/Makefile | 3 + sys/Makefile.dep | 5 + sys/auto_init/auto_init.c | 6 + sys/include/net/telnet.h | 95 ++++++ sys/net/application_layer/Kconfig | 6 + sys/net/application_layer/telnet/Kconfig | 28 ++ sys/net/application_layer/telnet/Makefile | 1 + .../application_layer/telnet/stdio_telnet.c | 86 +++++ .../application_layer/telnet/telnet_server.c | 303 ++++++++++++++++++ 11 files changed, 540 insertions(+) create mode 100644 sys/include/net/telnet.h create mode 100644 sys/net/application_layer/telnet/Kconfig create mode 100644 sys/net/application_layer/telnet/Makefile create mode 100644 sys/net/application_layer/telnet/stdio_telnet.c create mode 100644 sys/net/application_layer/telnet/telnet_server.c diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index ef14d491be..2531e39681 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -210,6 +210,7 @@ PSEUDOMODULES += stdio_cdc_acm PSEUDOMODULES += stdio_ethos PSEUDOMODULES += stdio_nimble_debug PSEUDOMODULES += stdio_uart_rx +PSEUDOMODULES += stdio_telnet PSEUDOMODULES += stm32_eth PSEUDOMODULES += stm32_eth_auto PSEUDOMODULES += stm32_eth_link_up diff --git a/makefiles/stdio.inc.mk b/makefiles/stdio.inc.mk index a5dd528670..a6dbca1d65 100644 --- a/makefiles/stdio.inc.mk +++ b/makefiles/stdio.inc.mk @@ -7,6 +7,7 @@ STDIO_MODULES = \ stdio_rtt \ stdio_semihosting \ stdio_uart \ + stdio_telnet \ # ifneq (,$(filter newlib picolibc,$(USEMODULE))) @@ -63,6 +64,11 @@ ifneq (,$(filter stdio_semihosting,$(USEMODULE))) FEATURES_REQUIRED_ANY += cpu_core_cortexm|arch_riscv endif +ifneq (,$(filter stdio_telnet,$(USEMODULE))) + DEFAULT_MODULE += auto_init_telnet + USEMODULE += telnet +endif + # enable stdout buffering for modules that benefit from sending out buffers in larger chunks ifneq (,$(filter picolibc,$(USEMODULE))) ifneq (,$(filter stdio_cdc_acm stdio_ethos slipdev_stdio stdio_semihosting,$(USEMODULE))) diff --git a/sys/Makefile b/sys/Makefile index c1fa9b8f64..2a008ed975 100644 --- a/sys/Makefile +++ b/sys/Makefile @@ -11,6 +11,9 @@ endif ifneq (,$(filter cipher_modes,$(USEMODULE))) DIRS += crypto/modes endif +ifneq (,$(filter telnet,$(USEMODULE))) + DIRS += net/application_layer/telnet +endif ifneq (,$(filter constfs,$(USEMODULE))) DIRS += fs/constfs endif diff --git a/sys/Makefile.dep b/sys/Makefile.dep index edd4685321..c42d9da876 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -324,6 +324,11 @@ ifneq (,$(filter sema_inv,$(USEMODULE))) USEMODULE += atomic_utils endif +ifneq (,$(filter telnet,$(USEMODULE))) + USEMODULE += pipe + USEMODULE += sock_tcp +endif + ifneq (,$(filter luid,$(USEMODULE))) FEATURES_OPTIONAL += periph_cpuid endif diff --git a/sys/auto_init/auto_init.c b/sys/auto_init/auto_init.c index 81dc5ea665..f315c07099 100644 --- a/sys/auto_init/auto_init.c +++ b/sys/auto_init/auto_init.c @@ -305,6 +305,12 @@ void auto_init(void) gnrc_ipv6_auto_subnets_init(); } + if (IS_USED(MODULE_AUTO_INIT_TELNET)) { + LOG_DEBUG("auto_init TELNET server\n"); + extern void telnet_server_start(void); + telnet_server_start(); + } + if (IS_USED(MODULE_AUTO_INIT_MULTIMEDIA)) { LOG_DEBUG("auto_init MULTIMEDIA\n"); if (IS_USED(MODULE_DFPLAYER)) { diff --git a/sys/include/net/telnet.h b/sys/include/net/telnet.h new file mode 100644 index 0000000000..fa02d28832 --- /dev/null +++ b/sys/include/net/telnet.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2021 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup net_telnet basic Telnet server implementation + * @ingroup net_ipv6 + * @brief Telnet server functions + * @{ + * + * @file + * @brief minimal Telnet server ([RFC 854](https://tools.ietf.org/html/rfc854)) implementation + * @note This implementation only supports a single client and no options. + * + * @warning Telnet is entirely unencrypted! Do not use it on public networks. + * This is intended to aid debugging on networks that are isolated from the Internet. + * + * @author Benjamin Valentin + */ +#ifndef NET_TELNET_H +#define NET_TELNET_H + +#include "net/sock/tcp.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The port for the Telnet server to listen on + */ +#ifndef CONFIG_TELNET_PORT +#define CONFIG_TELNET_PORT (23) +#endif + +/** + * @brief Start the Telnet server thread + * + * @return 0 on success, error otherwise + */ +int telnet_server_start(void); + +/** + * @brief Write data to the telnet client + * + * @param[in] buffer The buffer to send to the client + * @param[in] len The length of the buffer + * + * @return 0 on success, error otherwise + */ +int telnet_server_write(const void* buffer, size_t len); + +/** + * @brief Read data from the telnet client, will block until data is available. + * + * @param[out] buffer The buffer to write data from the client + * @param[in] count Number of bytes to read + * + * @return number of bytes read, error otherwise + */ +int telnet_server_read(void* buffer, size_t count); + +/** + * @brief Callback function that gets called when a telnet client connects + * but before stdio is redirected. + * + * @param[in] sock Socket of the client that just connected + * only use with @ref sock_tcp_get_local + */ +void telnet_cb_pre_connected(sock_tcp_t *sock); + +/** + * @brief Callback function that gets called when a telnet client connects + * after stdio is redirected. + * + * @param[in] sock Socket of the client that just connected + * only use with @ref sock_tcp_get_local + */ +void telnet_cb_connected(sock_tcp_t *sock); + +/** + * @brief Callback function that gets called after a telnet client disconnected. + */ +void telnet_cb_disconneced(void); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_TELNET_H */ +/** @} */ diff --git a/sys/net/application_layer/Kconfig b/sys/net/application_layer/Kconfig index 6118a0aa9c..3f690d17f2 100644 --- a/sys/net/application_layer/Kconfig +++ b/sys/net/application_layer/Kconfig @@ -21,3 +21,9 @@ rsource "asymcute/Kconfig" rsource "emcute/Kconfig" endmenu # MQTT-SN + +menu "Telnet" + +rsource "telnet/Kconfig" + +endmenu # Telnet diff --git a/sys/net/application_layer/telnet/Kconfig b/sys/net/application_layer/telnet/Kconfig new file mode 100644 index 0000000000..4d706a060c --- /dev/null +++ b/sys/net/application_layer/telnet/Kconfig @@ -0,0 +1,28 @@ +# Copyright (c) 2021 ML!PA Consulting GmbH +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# +menuconfig KCONFIG_USEMODULE_TELNET + bool "Configure telnet server" + depends on USEMODULE_TELNET + help + Configure telnet module using Kconfig. If not set default values and + CFLAGS will be used. + +if KCONFIG_USEMODULE_TELNET + +config TELNET_PORT + int "Server port" + default 23 + help + Server port, the default is the one specified in RFC 855. + +config TELNET_TCP_QUEUE_SIZE + int "TCP Queue Size" + default 1 + help + Maximum number of incoming TCP connections. + +endif # KCONFIG_USEMODULE_TELNET diff --git a/sys/net/application_layer/telnet/Makefile b/sys/net/application_layer/telnet/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/sys/net/application_layer/telnet/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/sys/net/application_layer/telnet/stdio_telnet.c b/sys/net/application_layer/telnet/stdio_telnet.c new file mode 100644 index 0000000000..99e96b5ac9 --- /dev/null +++ b/sys/net/application_layer/telnet/stdio_telnet.c @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys + * @{ + * + * @file + * @brief STDIO over Telnet implementation + * + * This file implements STDIO via a Telnet server with fallback UART output + * + * @author Benjamin Valentin + * + * @} + */ + +#include + +#include "board.h" +#include "kernel_defines.h" +#include "net/telnet.h" +#if IS_USED(MODULE_PERIPH_UART) +#include "stdio_uart.h" +#include "periph/uart.h" +#endif +#if IS_USED(MODULE_VFS) +#include "vfs.h" +#endif +#ifdef CPU_NATIVE +#include "native_internal.h" +#endif + +#define ENABLE_DEBUG 0 +#include "debug.h" + +static inline void _init_fallback(void) +{ +#if defined(STDIO_UART_DEV) && IS_USED(MODULE_PERIPH_UART) + uart_init(STDIO_UART_DEV, STDIO_UART_BAUDRATE, NULL, NULL); +#endif +} + +static inline int _write_fallback(const void* buffer, size_t len) +{ +#if defined(CPU_NATIVE) + real_write(STDOUT_FILENO, buffer, len); +#elif defined(STDIO_UART_DEV) && IS_USED(MODULE_PERIPH_UART) + uart_write(STDIO_UART_DEV, buffer, len); +#else + (void)buffer; +#endif + + return len; +} + +void stdio_init(void) +{ + _init_fallback(); + +#if IS_USED(MODULE_VFS) + vfs_bind_stdio(); +#endif +} + +ssize_t stdio_read(void* buffer, size_t count) +{ + return telnet_server_read(buffer, count); +} + +ssize_t stdio_write(const void* buffer, size_t len) +{ + int res = telnet_server_write(buffer, len); + + /* write to UART if no client is connected */ + if (res == -ENOTCONN) { + return _write_fallback(buffer, len); + } + + return res; +} diff --git a/sys/net/application_layer/telnet/telnet_server.c b/sys/net/application_layer/telnet/telnet_server.c new file mode 100644 index 0000000000..e27e76d4d8 --- /dev/null +++ b/sys/net/application_layer/telnet/telnet_server.c @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2021 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @brief Telnet server implementation + * + * @author Benjamin Valentin + * + * @} + */ + +#include +#include +#include "net/sock/tcp.h" +#include "net/telnet.h" +#include "pipe.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +#ifndef CONFIG_TELNET_TCP_QUEUE_SIZE +#define CONFIG_TELNET_TCP_QUEUE_SIZE (1) +#endif + +static char _stdin_pipe_buf[16]; +static ringbuffer_t _stdin_ringbuffer; +static pipe_t _stdin_pipe; + +static sock_tcp_queue_t sock_queue; +static sock_tcp_t socks[CONFIG_TELNET_TCP_QUEUE_SIZE]; +static sock_tcp_t *client; + +static char telnet_stack[THREAD_STACKSIZE_DEFAULT]; + +#define SOCK_TCP_TIMEOUT_MS 50 + +enum { + TELNET_CMD_EOF = 236, + TELNET_CMD_SE = 240, + TELNET_CMD_NOP, + TELNET_CMD_DATA_MARK, + TELNET_CMD_BRK, + TELNET_CMD_IP, + TELNET_CMD_AO, + TELNET_CMD_AYT, + TELNET_CMD_EC, + TELNET_CMD_EL, + TELNET_CMD_GA, + TELNET_CMD_SB = 250, + TELNET_CMD_WILL, + TELNET_CMD_WONT, + TELNET_CMD_DO, + TELNET_CMD_DONT, + TELNET_CMD_IAC +}; + +enum { + TELNET_OPT_BINARY = 0, + TELNET_OPT_ECHO = 1, + TELNET_OPT_SUP_GO_AHEAD = 3, + TELNET_OPT_STATUS = 5, +}; + +static bool connected; +static mutex_t connected_mutex = MUTEX_INIT_LOCKED; +static mutex_t sock_mutex; + +__attribute__((weak)) +void telnet_cb_pre_connected(sock_tcp_t *sock) +{ + (void)sock; +} + +__attribute__((weak)) +void telnet_cb_connected(sock_tcp_t *sock) +{ + (void)sock; +} + +__attribute__((weak)) +void telnet_cb_disconneced(void) +{ +} + +static void _acquire(void) +{ + mutex_lock(&sock_mutex); +} + +static void _release(void) +{ + mutex_unlock(&sock_mutex); +} + +static void _connected(void) +{ + telnet_cb_pre_connected(client); + + connected = true; + mutex_unlock(&connected_mutex); + + telnet_cb_connected(client); +} + +static void _disconnect(void) +{ + mutex_trylock(&connected_mutex); + connected = false; + + telnet_cb_disconneced(); +} + +static int _write_buffer(const void* buffer, size_t len) +{ + int res = 0; + const char *buf = buffer; + _acquire(); + + while (len) { + /* telnet expects \r\n line endings */ + /* https://datatracker.ietf.org/doc/html/rfc5198#appendix-C */ + const char *nl = memchr(buf, '\n', len); + if (nl) { + const char cr = '\r'; + size_t before_nl = nl - buf; + + /* write string before \n */ + res = sock_tcp_write(client, buf, before_nl); + if (res < 0) { + break; + } + + /* insert \r */ + res = sock_tcp_write(client, &cr, 1); + if (res < 0) { + break; + } + + buf = nl; + len -= before_nl; + } + + res = sock_tcp_write(client, buf, len); + if (res < 0) { + break; + } + len -= res; + buf += res; + } + _release(); + + return res < 0 ? res : 0; +} + +static uint8_t _will(uint8_t option) +{ + switch (option) { + /* agree to suppress go-ahead packets */ + /* see RFC 858 */ + case TELNET_OPT_SUP_GO_AHEAD: return TELNET_CMD_DO; + } + + return TELNET_CMD_WONT; +} + +static void _process_cmd(uint8_t cmd, uint8_t option) +{ + DEBUG("cmd: %u, option: %u\n", cmd, option); + switch (cmd) { + case TELNET_CMD_WILL: + { + uint8_t reply[] = {TELNET_CMD_IAC, _will(option), option}; + _write_buffer(reply, sizeof(reply)); + break; + } + } +} + +static void *telnet_thread(void *arg) +{ + (void)arg; + + static uint8_t rx_buf[64]; + + while (1) { + ssize_t res = sock_tcp_accept(&sock_queue, &client, SOCK_NO_TIMEOUT); + if (res < 0) { + DEBUG("accept error: %s\n", strerror(-res)); + continue; + } + + DEBUG("connected\n"); + _connected(); + + bool is_cmd = false; + uint8_t is_option = 0; + while (1) { + _acquire(); + res = sock_tcp_read(client, rx_buf, sizeof(rx_buf), SOCK_TCP_TIMEOUT_MS); + _release(); + if (res == -ETIMEDOUT) { + continue; + } + + if (res < 0) { + break; + } + + for (int i = 0; i < res; ++i) { + uint8_t c = rx_buf[i]; + + if (is_cmd) { + is_cmd = false; + switch (c) { + case TELNET_CMD_IAC: + goto write; + case TELNET_CMD_EOF: + goto disco; + default: + is_option = c; + } + continue; + } + + if (is_option) { + _process_cmd(is_option, c); + is_option = 0; + continue; + } + + if (c == TELNET_CMD_IAC) { + is_cmd = true; + continue; + } + + if (c == 0) { + continue; + } +write: + pipe_write(&_stdin_pipe, &c, 1); + } + } +disco: + _disconnect(); + sock_tcp_disconnect(client); + + if (res < 0) { + DEBUG("telnet: read: %s\n", strerror(res)); + } + } + + return NULL; +} + +int telnet_server_write(const void* buffer, size_t len) +{ + if (connected) { + int res = _write_buffer(buffer, len); + return res ? res : (int)len; + } + return -ENOTCONN; +} + +int telnet_server_read(void* buffer, size_t count) +{ + /* block until a connection is established */ + mutex_lock(&connected_mutex); + int res = pipe_read(&_stdin_pipe, buffer, count); + if (connected) { + mutex_unlock(&connected_mutex); + } + return res; +} + +int telnet_server_start(void) +{ + sock_tcp_ep_t ep = SOCK_IPV6_EP_ANY; + ep.port = CONFIG_TELNET_PORT; + + int res = sock_tcp_listen(&sock_queue, &ep, socks, ARRAY_SIZE(socks), 0); + if (res) { + return res; + } + + /* init RX ringbuffer */ + ringbuffer_init(&_stdin_ringbuffer, _stdin_pipe_buf, sizeof(_stdin_pipe_buf)); + pipe_init(&_stdin_pipe, &_stdin_ringbuffer, NULL); + + /* initiate telnet server */ + thread_create(telnet_stack, sizeof(telnet_stack), + THREAD_PRIORITY_MAIN - 1, THREAD_CREATE_STACKTEST, + telnet_thread, NULL, "telnet"); + + return 0; +}