nuttx-apps/netutils/telnetd/telnetd_daemon.c
Ricard Rosson 01ff92f794 netutils/telnetd: survive transient connection errors instead of exiting
Any single failed connection killed the whole daemon:

- A peer that resets before the accept completes (a port scanner, or a
  simple "nc -z" probe) surfaces as an accept4() error such as
  ECONNABORTED, which took the errout path and closed the listener.
- The per-connection error paths after a successful accept (setsockopt,
  opening /dev/telnet, SIOCTELNET, opening the session device, spawning
  the session) likewise exited the daemon instead of dropping the one
  connection.

Either way, one bad or aborted connection and the telnet console is
dead until reboot - a trivial remote way to take out the NuttX console
on any reachable network.  Verified on hardware: one "nc -zv" probe
permanently killed the listener.

Treat these as per-connection failures: drop the connection and keep
accepting.  Genuinely unrecoverable accept4() errors (EBADF, ENOTSOCK,
EINVAL, EOPNOTSUPP - a bad listen socket) still exit loudly, and a
short pause on repeated transient failures avoids busy-spinning while
an interface is down.  Daemon setup errors (socket/bind/listen) exit
as before.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ricard Rosson <ricard@groundbits.com>
2026-07-23 19:00:43 +01:00

381 lines
11 KiB
C

/****************************************************************************
* apps/netutils/telnetd/telnetd_daemon.c
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sched.h>
#include <spawn.h>
#include <errno.h>
#include <nuttx/debug.h>
#include <nuttx/net/telnet.h>
#include "netutils/telnetd.h"
#include "netutils/netlib.h"
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: telnetd_daemon
*
* Description:
* This function is the Telnet daemon. It does not return (unless an
* error occurs).
*
* Parameters:
* Standard task start up arguments.
*
* Return:
* Does not return unless an error occurs.
*
****************************************************************************/
int telnetd_daemon(FAR const struct telnetd_config_s *config)
{
union
{
struct sockaddr generic;
#ifdef CONFIG_NET_IPv4
struct sockaddr_in ipv4;
#endif
#ifdef CONFIG_NET_IPv6
struct sockaddr_in6 ipv6;
#endif
} addr;
#if defined(CONFIG_SCHED_HAVE_PARENT) && defined(CONFIG_ENABLE_ALL_SIGNALS)
struct sigaction sa;
sigset_t blockset;
#endif
socklen_t addrlen;
int listensd;
int acceptsd;
#ifdef CONFIG_NET_SOCKOPTS
int optval;
#endif
#if defined(CONFIG_SCHED_HAVE_PARENT) && defined(CONFIG_ENABLE_ALL_SIGNALS)
/* Call sigaction with the SA_NOCLDWAIT flag so that we do not transform
* children into "zombies" when they terminate: Child exit status will
* not be retained.
*
* NOTE: If the SA_NOCLDWAIT flag is set when establishing a handler for
* SIGCHLD, POSIX.1 leaves it unspecified whether a SIGCHLD signal is
* generated when a child process terminates. On both Linux and NuttX, a
* SIGCHLD signal will be generated in this case.
*/
sa.sa_handler = SIG_IGN;
sa.sa_flags = SA_NOCLDWAIT;
if (sigaction(SIGCHLD, &sa, NULL) < 0)
{
nerr("ERROR: sigaction failed: %d\n", errno);
goto errout;
}
/* Block receipt of the SIGCHLD signal */
sigemptyset(&blockset);
sigaddset(&blockset, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &blockset, NULL) < 0)
{
nerr("ERROR: sigprocmask failed: %d\n", errno);
goto errout;
}
#endif /* CONFIG_SCHED_HAVE_PARENT && CONFIG_ENABLE_ALL_SIGNALS */
/* Create a new TCP socket to use to listen for connections */
listensd = socket(config->d_family, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (listensd < 0)
{
nerr("ERROR: socket() failed for family %u: %d\n",
config->d_family, errno);
goto errout;
}
/* If the daemon was started without standard streams (e.g. spawned by
* nsh_telnetstart() before a USB console exists), socket() may have
* returned a descriptor in 0..2. The "go silent" close(0)..close(2)
* at the top of the accept loop below would then destroy the listen
* socket: every subsequent accept4() fails and the daemon serves
* nothing. Move the descriptor above the standard-stream range.
*/
if (listensd <= 2)
{
int highsd = fcntl(listensd, F_DUPFD_CLOEXEC, 3);
if (highsd < 0)
{
nerr("ERROR: F_DUPFD_CLOEXEC failed: %d\n", errno);
goto errout_with_socket;
}
close(listensd);
listensd = highsd;
}
#ifdef CONFIG_NET_SOCKOPTS
/* Set socket to reuse address */
optval = 1;
if (setsockopt(listensd, SOL_SOCKET, SO_REUSEADDR,
&optval, sizeof(int)) < 0)
{
nerr("ERROR: setsockopt SO_REUSEADDR failure: %d\n", errno);
goto errout_with_socket;
}
#endif
/* Bind the socket to a local address */
#ifdef CONFIG_NET_IPv4
if (config->d_family == AF_INET)
{
addr.ipv4.sin_family = AF_INET;
addr.ipv4.sin_port = config->d_port;
addr.ipv4.sin_addr.s_addr = INADDR_ANY;
addrlen = sizeof(struct sockaddr_in);
}
else
#endif
#ifdef CONFIG_NET_IPv6
if (config->d_family == AF_INET6)
{
addr.ipv6.sin6_family = AF_INET6;
addr.ipv6.sin6_port = config->d_port;
addrlen = sizeof(struct sockaddr_in6);
memset(addr.ipv6.sin6_addr.s6_addr, 0,
sizeof(addr.ipv6.sin6_addr.s6_addr));
}
else
#endif
{
nerr("ERROR: Unsupported address family: %u", config->d_family);
goto errout_with_socket;
}
if (bind(listensd, &addr.generic, addrlen) < 0)
{
nerr("ERROR: bind failure: %d\n", errno);
goto errout_with_socket;
}
/* Listen for connections on the bound TCP socket */
if (listen(listensd, 5) < 0)
{
nerr("ERROR: listen failure %d\n", errno);
goto errout_with_socket;
}
/* Begin accepting connections */
for (; ; )
{
struct telnet_session_s session;
#ifdef CONFIG_NET_SOLINGER
struct linger ling;
#endif
int drvrfd;
/* Now go silent. */
close(0);
close(1);
close(2);
ninfo("Accepting connections on port %d\n", ntohs(config->d_port));
addrlen = sizeof(addr);
acceptsd = accept4(listensd, &addr.generic, &addrlen, SOCK_CLOEXEC);
if (acceptsd < 0)
{
int err = errno;
/* A bad listen socket can never recover - exiting loudly beats
* spinning forever while serving nothing.
*/
if (err == EBADF || err == ENOTSOCK || err == EINVAL ||
err == EOPNOTSUPP)
{
nerr("ERROR: accept failed fatally: %d\n", err);
goto errout_with_socket;
}
/* Everything else is transient: a peer that reset before the
* accept completed (a port scan, nc -z, ECONNABORTED), a
* signal, or the interface bouncing. None of those may take
* the daemon down; the pause keeps a persistent transient
* (interface down) from busy-spinning.
*/
nerr("ERROR: accept failed: %d\n", err);
if (err != EINTR)
{
usleep(100 * 1000);
}
continue;
}
#ifdef CONFIG_NET_SOLINGER
/* Configure to "linger" until all data is sent when the socket is
* closed
*/
ling.l_onoff = 1;
ling.l_linger = 30; /* timeout is seconds */
if (setsockopt(acceptsd, SOL_SOCKET, SO_LINGER,
&ling, sizeof(struct linger)) < 0)
{
/* Per-connection failure (e.g. the peer already reset the
* connection - a port scan does this). Drop the connection and
* keep accepting; exiting here lets any probe kill the daemon.
*/
nerr("ERROR: setsockopt failed: %d\n", errno);
close(acceptsd);
continue;
}
#endif
/* Open the Telnet factory */
drvrfd = open("/dev/telnet", O_RDONLY);
if (drvrfd < 0)
{
nerr("ERROR: open(/dev/telnet) failed: %d\n", errno);
close(acceptsd);
continue;
}
/* Create a character device to "wrap" the accepted socket descriptor */
ninfo("Creating the telnet driver\n");
session.ts_sd = acceptsd;
session.ts_devpath[0] = '\0';
if (ioctl(drvrfd, SIOCTELNET,
(unsigned long)((uintptr_t)&session)) < 0)
{
nerr("ERROR: SIOCTELNET failed: %d\n", errno);
close(drvrfd);
close(acceptsd);
continue;
}
close(drvrfd);
/* Open the driver */
ninfo("Opening the telnet driver at %s\n", session.ts_devpath);
drvrfd = open(session.ts_devpath, O_RDWR);
if (drvrfd < 0)
{
/* The socket now belongs to the telnet driver instance; just
* keep accepting.
*/
nerr("ERROR: Failed to open %s: %d\n", session.ts_devpath, errno);
continue;
}
/* Use this driver as stdin, stdout, and stderror */
dup2(drvrfd, 0);
dup2(drvrfd, 1);
dup2(drvrfd, 2);
/* And we can close our original driver fd */
if (drvrfd > 2)
{
close(drvrfd);
}
/* Create a task to handle the connection. The created task
* will inherit the new stdin, stdout, and stderr.
*/
ninfo("Starting the telnet session\n");
#ifndef CONFIG_BUILD_KERNEL
if (config->t_entry)
{
pid_t pid = task_create("Telnet session",
config->t_priority, config->t_stacksize,
config->t_entry, NULL);
if (pid >= 0)
{
continue;
}
}
#endif
#ifdef CONFIG_LIBC_EXECFUNCS
if (config->t_path)
{
struct sched_param param;
posix_spawnattr_t attr;
pid_t pid;
int ret;
sched_getparam(0, &param);
param.sched_priority = config->t_priority;
posix_spawnattr_init(&attr);
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDPARAM);
posix_spawnattr_setschedparam(&attr, &param);
posix_spawnattr_setstacksize(&attr, config->t_stacksize);
ret = posix_spawnp(&pid, config->t_path, NULL,
&attr, config->t_argv, NULL);
if (ret > 0)
{
nerr("ERROR: Failed start the telnet session: %d\n", ret);
continue;
}
}
#endif
}
errout_with_socket:
close(listensd);
errout:
return errno;
}