mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-08-01 20:29:00 +00:00
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>
381 lines
11 KiB
C
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, ¶m);
|
|
param.sched_priority = config->t_priority;
|
|
|
|
posix_spawnattr_init(&attr);
|
|
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDPARAM);
|
|
posix_spawnattr_setschedparam(&attr, ¶m);
|
|
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;
|
|
}
|