← Commit list

dc6e92c99

Denys Vlasenko
Denys Vlasenko
2026-02-21
SUCCESS Duration: 144s

Subject

libbb: add generic "fd polling and I/O" loop, use it in telnetd

Binary

No binary (build failed)

Architecture & Compiler Flags

Architecture: x86_64-linux-gnu Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0
gcc -Wp,-MD,applets/.usage.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer      -Iinclude -Iinclude -o applets/usage applets/usage.c

Build Log

(log yok)

Commit Diff GitHub →

include/libbb.h+73 -13
@@ -451,6 +451,11 @@ void *mmap_read(int fd, size_t size) FAST_FUNC;
void *mmap_anon(size_t size) FAST_FUNC;
void *xmmap_anon(size_t size) FAST_FUNC;
+#if defined(__x86_64__) || defined(i386)
+/* 0x7f would be better, but it causes alignment problems */
+# define ARCH_GLOBAL_PTR_OFF 0x80
+#endif
+
#if defined(__x86_64__) || defined(i386)
# define BB_ARCH_FIXED_PAGESIZE 4096
#elif defined(__arm__) /* only 32bit, 64bit ARM has variable page size */
@@ -462,11 +467,6 @@ void *xmmap_anon(size_t size) FAST_FUNC;
//sparc64,alpha,openrisc: fixed 8k pages
#endif
-#if defined(__x86_64__) || defined(i386)
-/* 0x7f would be better, but it causes alignment problems */
-# define ARCH_GLOBAL_PTR_OFF 0x80
-#endif
-
#if defined BB_ARCH_FIXED_PAGESIZE
# define IF_VARIABLE_ARCH_PAGESIZE(...) /*nothing*/
# define bb_getpagesize() BB_ARCH_FIXED_PAGESIZE
@@ -703,6 +703,74 @@ struct fd_pair { int rd; int wr; };
#define piped_pair(pair) pipe(&((pair).rd))
#define xpiped_pair(pair) xpipe(&((pair).rd))
+
+int parse_datestr(const char *date_str, struct tm *ptm) FAST_FUNC;
+time_t validate_tm_time(const char *date_str, struct tm *ptm) FAST_FUNC;
+char *strftime_HHMMSS(char *buf, unsigned len, time_t *tp) FAST_FUNC;
+char *strftime_YYYYMMDDHHMMSS(char *buf, unsigned len, time_t *tp) FAST_FUNC;
+void xgettimeofday(struct timeval *tv) FAST_FUNC;
+void xsettimeofday(const struct timeval *tv) FAST_FUNC;
+
+
+/* Generic I/O loop */
+struct connection;
+typedef struct ioloop_state {
+ struct connection *conns;
+ unsigned flags;
+ unsigned max_timeout;
+ unsigned current_iteration_timeout;
+ unsigned last_timeout;
+ int (*pre_poll_callback)(struct ioloop_state*);
+} ioloop_state_t;
+
+#define STRUCT_CONNECTION \
+ struct connection *next; \
+ ioloop_state_t *io; \
+ int read_fd; \
+ int write_fd; \
+ int (*have_buffer_to_read_into)(void *this); \
+ int (*have_data_to_write)(void *this); \
+ int (*read)(void *this); \
+ int (*write)(void *this); \
+
+typedef struct connection {
+ STRUCT_CONNECTION
+} connection_t;
+
+static ALWAYS_INLINE void free_connection(connection_t *conn)
+{
+ free(conn);
+}
+
+void FAST_FUNC conn_close_fds(connection_t *conn);
+void FAST_FUNC conn_close_fds_remove_and_free(connection_t *conn);
+static ALWAYS_INLINE ioloop_state_t *new_ioloop_state(void)
+{
+ ioloop_state_t *io = xzalloc(sizeof(*io));
+ return io;
+}
+static ALWAYS_INLINE void free_ioloop_state(ioloop_state_t *io)
+{
+ free(io);
+}
+static ALWAYS_INLINE void ioloop_decrease_current_timeout(ioloop_state_t *io, unsigned n)
+{
+ if (io->current_iteration_timeout > n)
+ io->current_iteration_timeout = n;
+}
+
+void FAST_FUNC ioloop_insert_conn(ioloop_state_t *io, connection_t *conn);
+void FAST_FUNC ioloop_remove_conn(ioloop_state_t *io, connection_t *conn);
+enum {
+ IOLOOP_FLAG_EXIT_IF_TIMEOUT = (1 << 0),
+ //IOLOOP_FLAG_EXIT_IF_ALL_NOT_READY = (1 << 1),
+ /* ioloop_run return values */
+ IOLOOP_NO_CONNS = 0,
+ IOLOOP_TIMEOUT = 1,
+};
+int FAST_FUNC ioloop_run(ioloop_state_t *io);
+
+
/* Useful for having small structure members/global variables */
typedef int8_t socktype_t;
typedef int8_t family_t;
@@ -731,14 +799,6 @@ struct BUG_too_small {
};
-int parse_datestr(const char *date_str, struct tm *ptm) FAST_FUNC;
-time_t validate_tm_time(const char *date_str, struct tm *ptm) FAST_FUNC;
-char *strftime_HHMMSS(char *buf, unsigned len, time_t *tp) FAST_FUNC;
-char *strftime_YYYYMMDDHHMMSS(char *buf, unsigned len, time_t *tp) FAST_FUNC;
-void xgettimeofday(struct timeval *tv) FAST_FUNC;
-void xsettimeofday(const struct timeval *tv) FAST_FUNC;
-
-
int xsocket(int domain, int type, int protocol) FAST_FUNC;
void xbind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen) FAST_FUNC;
void xlisten(int s, int backlog) FAST_FUNC;
libbb/ioloop.c+262 -0
@@ -0,0 +1,262 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * Copyright (C) 2026 Denys Vlasenko <[email protected]>
+ *
+ * Licensed under GPLv2, see file LICENSE in this source tree.
+ */
+//kbuild:lib-$(CONFIG_TELNETD) += ioloop.o
+
+#include "libbb.h"
+
+#define DEBUG 0
+
+#if DEBUG
+# define dbg(...) bb_error_msg(__VA_ARGS__)
+static char *bin_to_hex(const void *hash_value, unsigned hash_length)
+{
+ /* xzalloc zero-terminates */
+ char *hex_value = xzalloc((hash_length * 2) + 1);
+ bin2hex(hex_value, (char*)hash_value, hash_length);
+ return auto_string(hex_value);
+}
+#else
+# define dbg(...) ((void)0)
+#endif
+
+void FAST_FUNC ioloop_insert_conn(ioloop_state_t *io, connection_t *conn)
+{
+ conn->io = io;
+ conn->next = io->conns;
+ io->conns = conn;
+}
+
+void FAST_FUNC ioloop_remove_conn(ioloop_state_t *io, connection_t *conn)
+{
+ connection_t **pp = &io->conns;
+ while (*pp) {
+ if (*pp == conn) {
+ *pp = conn->next;
+ return;
+ }
+ pp = &(*pp)->next;
+ }
+}
+
+void FAST_FUNC conn_close_fds(connection_t *conn)
+{
+ if (conn->read_fd >= 0)
+ close(conn->read_fd);
+ if (conn->write_fd >= 0 && conn->write_fd != conn->read_fd)
+ close(conn->write_fd);
+ conn->write_fd = -1;
+ conn->read_fd = -1;
+}
+
+void FAST_FUNC conn_close_fds_remove_and_free(connection_t *conn)
+{
+ conn_close_fds(conn);
+ ioloop_remove_conn(conn->io, conn);
+ free_connection(conn);
+}
+
+#ifdef UNUSED
+void FAST_FUNC ioloop_close_fd_in_all_conns(ioloop_state_t *io, int fd)
+{
+ connection_t *conn;
+
+ close(fd);
+ conn = io->conns;
+ while (conn) {
+ if (conn->read_fd == fd)
+ conn->read_fd = -1;
+ if (conn->write_fd == fd)
+ conn->write_fd = -1;
+ conn = conn->next;
+ }
+}
+#endif
+
+// have_data_to_write() - Do we have data to write?
+// May return error if knows that write side is closed, even if it has free buffer space.
+// > 0: Has data to write (or can generate such data)
+// In this case, write_fd must be valid! (it's a bug if it is < 0, can crash)
+// 0: No data to write currently
+// < 0: error, I probably freed myself (do not use my structure in this iteration,
+// on next iteration, if I indeed freed myself, you won't find me in the list).
+//
+// have_buffer_to_read_into() - Is there a buffer to read into?
+// May return error if knows that write side is closed, even if it has free buffer space.
+// > 0: Healthy, has buffer space, please poll read_fd
+// In this case, read_fd must be valid! (it's a bug if it is < 0)
+// 0: One of:
+// Buffer is full (hopefully write() will free some)
+// Got error/EOF, want to drain write buffer first
+// < 0: Error/EOF, I probably freed myself (do not use my structure)
+//
+// Essentially: add your "this connection is dead, close+ioloop_remove_conn(this)+free(this)" code
+// to either of the above two functions.
+// Either would work equally well from ioloop POV.
+//
+// write() - Perform write
+// Even if poll shows that write_fd become ready, the method will not be called if conn->write_fd become < 0.
+// However, changing it to another fd >= 0 is a bug.
+// >= 0: No error (ioloop() does not handle 0 specially)
+// < 0: Error occurred, I probably freed myself (do not use my structure)
+//
+// Error returns from read() and write() will skip the opposite op.
+// (Which one happens first depends on ioloop_run() code, as I write this, write()
+// is done first - therefore read() can't skip write()).
+// IOW: if you "remove(this)" in either of them you MUST return < 0.
+//
+// Write function, if it performs some processing, needs to account for the possibility
+// that any "incomplete data, can't write, wait for more" logic should also check whether
+// "more" is even possible: your other code may know that the read side is at EOF,
+// and arrange for it to not be polled. Thus a hang: read not reading, write waits for more date.
+//
+// read() - Perform read
+// Will not be called if read_fd become < 0.
+// >= 0: No error (0 is EOF, ioloop() does not handle 0 specially)
+// < 0: Error occurred, I probably freed myself (do not use my structure)
+//
+// Putting "this connection is dead, close+remove+free" code into read function
+// is often inconvenient: if you got EOF/error on read, you still want to poll write side
+// and try to write out the buffered data to it. Which means read() can't "remove+free".
+// Instead, you can remember EOF/error and make future have_buffer_to_read_into() respond 0.
+// One way is to close (if possible) read_fd, set it to -1 and use as a flag.
+//
+// If need to support one-sided close, such as when HTTP/1.x client sends us
+// "GET / HTTP/1.1\r\n\r\n" and closes its writing side with shutdown(SHUT_WR),
+// the idiom is that when read() sees EOF, it sets conn->read_fd to -1
+// and subsequently have_buffer_to_read_into() always return 0 (no more attempts to read);
+// write() flushes all remaining data to conn->write_fd and then signals EOF
+// to write_fd: shutdown(SHUT_WR) for sockets, close() for pipes
+// (how to do this for ptys!?).
+// After this, have_data_to_write() can return 0 if fd has to stay open (socket)
+// or can return -1 and free itself if fd is closed.
+
+static ALWAYS_INLINE int have_data_to_write(connection_t *conn)
+{
+ return conn->have_data_to_write(conn);
+}
+static ALWAYS_INLINE int have_buffer_to_read_into(connection_t *conn)
+{
+ return conn->have_buffer_to_read_into(conn);
+}
+static ALWAYS_INLINE int write_from_buf(connection_t *conn)
+{
+ return conn->write(conn);
+}
+static ALWAYS_INLINE int read_to_buf(connection_t *conn)
+{
+ return conn->read(conn);
+}
+
+int FAST_FUNC ioloop_run(ioloop_state_t *io)
+{
+ connection_t *conn;
+ unsigned poll_timeout_us;
+ int maxfd;
+ int count;
+ struct timeval tv;
+ struct timeval *tv_ptr;
+ fd_set rdfdset, wrfdset;
+
+ io->current_iteration_timeout = io->max_timeout ? io->max_timeout : UINT_MAX;
+//bb_error_msg("io->current_iteration_timeout:%d", io->current_iteration_timeout);
+ again:
+ FD_ZERO(&rdfdset);
+ FD_ZERO(&wrfdset);
+
+ conn = io->conns;
+ if (!conn)
+ return IOLOOP_NO_CONNS;
+
+ maxfd = -1;
+ while (conn) {
+ connection_t *next;
+ int rcw, rcr;
+
+ next = conn->next; /* in case conn is freed */
+ rcw = have_data_to_write(conn);
+ if (rcw < 0) {
+ /* often indicates that conn is gone (freed), do not use it anymore */
+ goto next;
+ }
+ /* Do not add conn->write_fd to waiting set yet,
+ * the *reader* may decide to abort (return rcr < 0)!
+ * Check it first:
+ */
+ rcr = have_buffer_to_read_into(conn);
+ if (rcr < 0)
+ goto next;
+ if (rcw > 0) {
+ FD_SET(conn->write_fd, &wrfdset);
+//FIXME: what if fd > 1023
+ if (conn->write_fd > maxfd)
+ maxfd = conn->write_fd;
+ }
+ if (rcr > 0) {
+ FD_SET(conn->read_fd, &rdfdset);
+ if (conn->read_fd > maxfd)
+ maxfd = conn->read_fd;
+ }
+ next:
+ conn = next;
+ }
+ if (!io->conns)
+ return IOLOOP_NO_CONNS; /* all conns removed themselves before we reached polling */
+
+ poll_timeout_us = io->current_iteration_timeout;
+ /* May be useful to know after loop exit: */
+ io->last_timeout = poll_timeout_us;
+ dbg("poll_timeout_us:%d", poll_timeout_us);
+
+ tv_ptr = NULL; /* infinite timeout */
+ if (poll_timeout_us != UINT_MAX) {
+ tv.tv_sec = poll_timeout_us / 1000000;
+ tv.tv_usec = poll_timeout_us % 1000000;
+ tv_ptr = &tv;
+ }
+ maxfd++;
+ count = select(maxfd, maxfd ? &rdfdset : NULL, maxfd ? &wrfdset : NULL, NULL, tv_ptr);
+ dbg("select:%d", count);
+
+ /* Any function in the loop can change it, modifying next select() timeout */
+ io->current_iteration_timeout = io->max_timeout ? io->max_timeout : UINT_MAX;
+ dbg("io->current_iteration_timeout:%d", io->current_iteration_timeout);
+
+ if (count <= 0) {
+ /* 0: timeout */
+ if (count == 0 && (io->flags & IOLOOP_FLAG_EXIT_IF_TIMEOUT))
+ return IOLOOP_TIMEOUT;
+ /* < 0: EINTR or ENOMEM */
+ goto again;
+ }
+
+ conn = io->conns;
+ while (conn) {
+ connection_t *next = conn->next; /* in case conn freed */
+ int rcw;
+
+ /* We do check for valid fd >=0: this means that conns
+ * may prevent I/O of other conns by setting fds to -1.
+ */
+ dbg("conn->write_fd:%d ?", conn->write_fd);
+ if (conn->write_fd >= 0 && FD_ISSET(conn->write_fd, &wrfdset)) {
+ dbg("conn->write_fd:%d ready", conn->write_fd);
+ rcw = write_from_buf(conn);
+ if (rcw < 0) {
+ /* often indicates that conn is gone (freed), do not use it anymore */
+ goto next1;
+ }
+ }
+ dbg("conn->read_fd:%d ?", conn->read_fd);
+ if (conn->read_fd >= 0 && FD_ISSET(conn->read_fd, &rdfdset)) {
+ dbg("conn->read_fd:%d ready", conn->read_fd);
+ read_to_buf(conn);
+ }
+ next1:
+ conn = next;
+ }
+ goto again;
+}
networking/telnet.txt+53 -0
@@ -0,0 +1,53 @@
+// 2-byte sequences:
+// IAC SE (240) End of subnegotiation parameters
+// IAC NOP (241) No operation
+// IAC DM (242) Data Mark (used with TCP urgent pointer)
+// IAC BRK (243) Break (simulate serial line break) - perhaps tcsendbreak(master_ptyfd, 0) [untested]?
+// IAC IP (244) Interrupt Process ("send SIGINT to foreground process group a-la ^C")
+// IAC AO (245) Abort Output
+// IAC AYT (246) Are You There
+// IAC EC (247) Erase Character
+// IAC EL (248) Erase Line
+// IAC GA (249) Go Ahead
+// IAC IAC (255) I'm sending literal byte 255
+// 3-byte sequences:
+// IAC WILL <option> (251) sender declares willingness to enable option
+// IAC WONT <option> (252) sender refuses / disables option
+// IAC DO <option> (253) sender requests peer to send option, or enable option on peer's side
+// IAC DONT <option> (254) sender requests peer to NOT send option, or disable option on peer's side
+// Example:
+// IAC DO 31 (request NAWS)
+// IAC WILL 24 (offer TERMINAL-TYPE)
+//
+// Variable length: subnegotiation (250)
+// IAC SB <option> <data...> IAC SE
+// option = single byte
+// data = zero or more bytes
+// Subnegotiation can not be nested, and can not have 2- and 3-byte commands inside.
+// Inside subnegotiation data, IAC IAC represents literal 255.
+// An IAC byte inside SB:
+// IAC IAC: literal byte 255
+// IAC SE: end of subnegotiation
+// IAC <anything else>: protocol error
+// Common options that use subnegotiation:
+// TERMINAL-TYPE (24)
+// IAC SB 24 SEND(1) IAC SE
+// IAC SB 24 IS(0) "xterm-256color" IAC SE
+// NAWS - Negotiate About Window Size (31)
+// IAC SB 31 <width_hi> <width_lo> <height_hi> <height_lo> IAC SE
+// width and height are 16-bit big endian. HOWTO:
+// Server: IAC DO NAWS
+// Client: IAC WILL NAWS
+// Client: IAC SB NAWS <w> <h> IAC SE
+// ...sometime later: client's window is resized by user...
+// Client: IAC SB NAWS <w> <h> IAC SE
+// TERMINAL-SPEED (32)
+// IAC SB 32 IS(0) "rx,tx" IAC SE
+// REMOTE-FLOW-CONTROL (33)
+// LINEMODE (34)
+// X-DISPLAY-LOCATION (35)
+// ENVIRON (36)
+// AUTHENTICATION (37)
+// ENCRYPT (38)
+// NEW-ENVIRON (39)
+// COM-PORT-OPTION (44)
networking/telnetd.c+1311 -558
@@ -61,6 +61,14 @@
//config:
//config: with all that done, telnetd _should_ work....
//config:
+//config:config FEATURE_TELNETD_SELFTEST_DEBUG
+//config: bool "Include self-test (telnetd -@)"
+//config: default n
+//config: depends on TELNETD
+//config: help
+//config: Include self-test code for pty/vhangup() behavior.
+//config: Useful for development and validation on new platforms.
+//config:
//config:config FEATURE_TELNETD_STANDALONE
//config: bool "Support standalone telnetd (not inetd only)"
//config: default y
@@ -120,48 +128,51 @@
//usage: )
//usage: )
-#define DEBUG 0
-
#include "libbb.h"
#include "common_bufsiz.h"
#include <syslog.h>
+#include <arpa/telnet.h>
+
+#define DEBUG 0
+#define DEBUG_CLOSING 0
#if DEBUG
-# define TELCMDS
-# define TELOPTS
+# define dbg(...) bb_error_msg(__VA_ARGS__)
+#else
+# define dbg(...) ((void)0)
+#endif
+#if DEBUG_CLOSING
+# define dbg_close(...) bb_error_msg(__VA_ARGS__)
+#else
+# define dbg_close(...) ((void)0)
#endif
-#include <arpa/telnet.h>
+#define SELFTEST_VHANGUP 1
+#define SELFTEST_NET2PTY 1
+#define SELFTEST_PTY2NET 1
-struct tsession {
- struct tsession *next;
- pid_t shell_pid;
- int sockfd_read;
- int sockfd_write;
- int ptyfd;
- smallint buffered_IAC_for_pty;
-
- /* two circular buffers */
- /*char *buf1, *buf2;*/
-/*#define TS_BUF1(ts) ts->buf1*/
-/*#define TS_BUF2(ts) TS_BUF2(ts)*/
-#define TS_BUF1(ts) ((unsigned char*)(ts + 1))
-#define TS_BUF2(ts) (((unsigned char*)(ts + 1)) + BUFSIZE)
- int rdidx1, wridx1, size1;
- int rdidx2, wridx2, size2;
-};
+#define SAVE_NET2PTY_EXPECTED 0
+#define SAVE_NET2PTY_ACTUAL 0
-/* Two buffers are directly after tsession in malloced memory.
- * Make whole thing fit in 4k */
-enum { BUFSIZE = (4 * 1024 - sizeof(struct tsession)) / 2 };
+#define SAVE_PTY2NET_EXPECTED 0
+#define SAVE_PTY2NET_ACTUAL 0
+/* Must match getopt32 string */
+enum {
+ OPT_WATCHCHILD = (1 << 2), /* -K */
+ OPT_INETD = (1 << 3) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -i */
+ OPT_PORT = (1 << 4) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -p PORT */
+ OPT_FOREGROUND = (1 << 6) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -F */
+ OPT_SYSLOG = (1 << 7) * ENABLE_FEATURE_TELNETD_INETD_WAIT, /* -S */
+ OPT_WAIT = (1 << 8) * ENABLE_FEATURE_TELNETD_INETD_WAIT, /* -w SEC */
+};
/* Globals */
struct globals {
- struct tsession *sessions;
+ ioloop_state_t io;
const char *loginpath;
const char *issuefile;
- int maxfd;
+ IF_FEATURE_TELNETD_INETD_WAIT(unsigned sec_linger;)
} FIX_ALIASING;
#define G (*(struct globals*)bb_common_bufsiz1)
#define INIT_G() do { \
@@ -170,303 +181,626 @@ struct globals {
G.issuefile = "/etc/issue.net"; \
} while (0)
+#define TELNET_CONNECTION \
+ STRUCT_CONNECTION \
+ pid_t shell_pid;
-/* Write some buf1 data to pty, processing IACs.
- * Update wridx1 and size1. Return < 0 on error.
- * Buggy if IAC is present but incomplete: skips them.
+typedef struct telnet_conn {
+ TELNET_CONNECTION
+} telnet_conn_t;
+
+struct net_to_pty;
+
+typedef struct pty_to_net {
+ TELNET_CONNECTION
+ struct net_to_pty *sibling; /* must be directly after TELNET_CONNECTION */
+ int rdidx, wridx, size;
+ unsigned deadline_us;
+ unsigned eio_count;
+//TODO: these two can be unified into one:
+ smallint respond_to_AYT;
+ char extra_byte_to_write;
+ /* circular buffer follows */
+#define BUF2NET(ts) ((unsigned char*)(ts + 1))
+} pty_to_net_t ALIGN8;
+/* The buffer is directly after malloced memory, aligned to 64 bits */
+enum { TO_NET_BUFSIZE = 2 * 1024 };
+enum { TO_NET_BUFMASK = TO_NET_BUFSIZE - 1 };
+#define BUF2NET_INC(n, inc) ((n) = ((n) + (inc)) & TO_NET_BUFMASK)
+
+typedef struct net_to_pty {
+ TELNET_CONNECTION
+ pty_to_net_t *sibling; /* must be directly after TELNET_CONNECTION */
+ int rdidx, wridx, size;
+ unsigned deadline_us;
+ smallint skip_LF_or_NUL;
+ /* circular buffer follows */
+#define BUF2PTY(ts) ((unsigned char*)(ts + 1))
+} net_to_pty_t ALIGN8;
+/* The buffer is directly after malloced memory, aligned to 64 bits */
+enum { TO_PTY_BUFSIZE = 2 * 1024 };
+enum { TO_PTY_BUFMASK = TO_PTY_BUFSIZE - 1 };
+#define BUF2PTY_INC(n, inc) ((n) = ((n) + (inc)) & TO_PTY_BUFMASK)
+#define BUF2PTY_DEC(n, dec) ((n) = ((n) - (dec)) & TO_PTY_BUFMASK)
+
+/* The mutual pipes are linked by to_pty, to_net.
+ * The rules:
+ * = always test for !NULL before use.
+ * = if you are freeing one of the structs, set
+ * "me->sibling->sibling = NULL".
+ * Use one of these helpers:
*/
-static ssize_t
-safe_write_to_pty_decode_iac(struct tsession *ts)
+static void remove_and_free_to_pty(net_to_pty_t *ts)
{
- unsigned wr;
- ssize_t rc;
- unsigned char *buf;
- unsigned char *found;
+ dbg_close("remove_and_free:%p rd:%d wr:%d sibling:%p",
+ ts, ts->read_fd, ts->write_fd, ts->sibling);
+ if (ts->sibling) {
+ /* "you have no sibling now" */
+ ts->sibling->sibling = NULL;
+ }
+ conn_close_fds_remove_and_free((void*)ts);
+}
+static void ALWAYS_INLINE remove_and_free_to_net(pty_to_net_t *ts)
+{
+ /* Works because ->sibling have the same offset */
+ remove_and_free_to_pty((void*)ts);
+}
- buf = TS_BUF1(ts) + ts->wridx1;
- wr = MIN(BUFSIZE - ts->wridx1, ts->size1);
- /* wr is at least 1 here */
+// Theory of operation
+// (AKA "when should I close fds? when should I detach from ioloop?").
+// The fds are named read)fs and write_fd, but for clarity let's call them netfd and ptyfd.
+// net_to_pty::have_data_to_write
+// net_to_pty::have_buffer_to_read_into
+// if ptyfd < 0: //sibling told us ptyfd is down?
+// if sibling && sibling->netfd >= 0: netfd = -1; //do not close netfd, sibling uses it (if sibling exists)!
+// close_and_detach;
+// if netfd < 0: return 0;
+// net_to_pty::write
+// if ptyfd write detects error (not EAGAIN):
+// //if (sibling):
+// // sibling->ptyfd = -1; // let sibling know ptyfd is closed
+// // if sibling->netfd >= 0: netfd = -1; // do not close netfd, sibling uses it!
+// //close_and_detach;
+// let pty_to_net detect this (ignore the error)
+// net_to_pty::read
+// if netfd read detects EOF or error (there is no way to tell pty the difference):
+// if no sibling: close(netfd);
+// netfd = -1; //no longer try to read
+// return 0; //but do not detach yet
+
+static int net_to_pty__have_data_to_write(void *this)
+{
+ //connection_t *conn = this;
+ net_to_pty_t *ts = this;
+ unsigned char *buf;
+ int size, increment;
- if (ts->buffered_IAC_for_pty) {
- /* Last time we stopped on a "dangling" IAC byte.
- * We removed it from the buffer back then.
- * Now pretend it's still there, and jump to IAC processing.
- */
- ts->buffered_IAC_for_pty = 0;
- wr++;
- ts->size1++;
- buf--; /* Yes, this can point before the buffer. It's ok */
- ts->wridx1--;
- goto handle_iac;
- }
+ if (ts->write_fd < 0) /* did slave close its pty? */
+ return 0;
+ again:
+ size = ts->size;
+ if (size == 0)
+ return 0;
- found = memchr(buf, IAC, wr);
- if (found != buf) {
- /* There is a "prefix" of non-IAC chars.
- * Write only them, and return.
- */
- if (found)
- wr = found - buf;
- /* We map \r\n ==> \r for pragmatic reasons:
- * many client implementations send \r\n when
- * the user hits the CarriageReturn key.
- * See RFC 1123 3.3.1 Telnet End-of-Line Convention.
- */
- rc = wr;
- found = memchr(buf, '\r', wr);
- if (found)
- rc = found - buf + 1;
- rc = safe_write(ts->ptyfd, buf, rc);
- if (rc <= 0)
- return rc;
- if (rc < wr /* don't look past available data */
- && buf[rc-1] == '\r' /* need this: imagine that write was _short_ */
- && (buf[rc] == '\n' || buf[rc] == '\0')
- ) {
- rc++;
+ buf = BUF2PTY(ts) + ts->wridx;
+ if (ts->skip_LF_or_NUL) {
+ /* last char we wrote was '\r' */
+ ts->skip_LF_or_NUL = 0;
+ /* we have the next one: examine */
+ if (buf[0] == '\n' || buf[0] == '\0') {
+ increment = 1; /* drop it now */
+ inc:
+ BUF2PTY_INC(ts->wridx, increment);
+ ts->size -= increment;
+ goto again;
}
- goto update_and_return;
+ /* else: not '\r\n' or '\rNUL' */
}
-
- /* buf starts with IAC char. Process that sequence.
- * Example: we get this from our own (bbox) telnet client:
- * read(5, "\377\374\1""\377\373\37""\377\372\37\0\262\0@\377\360""\377\375\1""\377\375\3"):
- * IAC WONT ECHO, IAC WILL NAWS, IAC SB NAWS <cols> <rows> IAC SE, IAC DO SGA
- * Another example (telnet-0.17 from old-netkit):
- * read(4, "\377\375\3""\377\373\30""\377\373\37""\377\373 ""\377\373!""\377\373\"""\377\373'"
- * "\377\375\5""\377\373#""\377\374\1""\377\372\37\0\257\0I\377\360""\377\375\1"):
- * IAC DO SGA, IAC WILL TTYPE, IAC WILL NAWS, IAC WILL TSPEED, IAC WILL LFLOW, IAC WILL LINEMODE, IAC WILL NEW_ENVIRON,
- * IAC DO STATUS, IAC WILL XDISPLOC, IAC WONT ECHO, IAC SB NAWS <cols> <rows> IAC SE, IAC DO ECHO
- */
- if (wr <= 1) {
- /* Only the single IAC byte is in the buffer, eat it
- * and set a flag "process the rest of the sequence
- * next time we are here".
- */
- //bb_error_msg("dangling IAC!");
- ts->buffered_IAC_for_pty = 1;
- rc = 1;
- goto update_and_return;
- }
-
- handle_iac:
- /* 2-byte commands (240..250 and 255):
- * IAC IAC (255) Literal 255. Supported.
- * IAC SE (240) End of subnegotiation. Treated as NOP.
- * IAC NOP (241) NOP. Supported.
- * IAC BRK (243) Break. Like serial line break. TODO via tcsendbreak()?
- * IAC AYT (246) Are you there.
- * These don't look useful:
- * IAC DM (242) Data mark. What is this?
- * IAC IP (244) Suspend, interrupt or abort the process. (Ancient cousin of ^C).
- * IAC AO (245) Abort output. "You can continue running, but do not send me the output".
- * IAC EC (247) Erase character. The receiver should delete the last received char.
- * IAC EL (248) Erase line. The receiver should delete everything up tp last newline.
- * IAC GA (249) Go ahead. For half-duplex lines: "now you talk".
- * Implemented only as part of NAWS:
- * IAC SB (250) Subnegotiation of an option follows.
- */
- if (buf[1] == IAC) {
- /* Literal 255 (emacs M-DEL) */
- //bb_error_msg("255!");
- rc = safe_write(ts->ptyfd, &buf[1], 1);
- /*
- * If we went through buffered_IAC_for_pty==1 path,
- * bailing out on error like below messes up the buffer.
- * EAGAIN is highly unlikely here, other errors will be
- * repeated on next write, let's just skip error check.
- */
-#if 0
- if (rc <= 0)
- return rc;
-#endif
- rc = 2;
- goto update_and_return;
+ if (buf[0] != IAC) /* very likely */
+ return size;
+ if (size == 1) { /* we only have one char: IAC */
+ cant_write:
+ if (ts->read_fd < 0)
+ ts->size = 0; /* EOF was seen: ignore incomplete IAC, prevent infinite loop */
+ //bb_error_msg("cant_write");
+ return 0; /* "can't write" */
}
- if (buf[1] == AYT) {
- if (ts->size2 == 0) { /* if nothing buffered yet... */
- /* Send back evidence that AYT was seen */
- unsigned char *buf2 = TS_BUF2(ts);
- buf2[0] = IAC;
- buf2[1] = NOP;
- ts->wridx2 = 0;
- ts->rdidx2 = ts->size2 = 2;
+
+ /* size is >= 2 and buf[0] is IAC */
+ //dbg_iac("size:%d %02x %02x", size, buf[0], buf[1]);
+
+ /* Cannot process IACs which wrap around buffer's end, need 7 contiguous chars */
+ if (ts->wridx > TO_PTY_BUFSIZE - 8) {
+ uint64_t v64;
+ if (ts->wridx < ts->rdidx) {
+ /* |.......WRxRD.| */
+ /* Buffer is not wrapped yet */
+ } else {
+ /* Possible situations: */
+ /* |xRD.......WRx| wrapped */
+ /* |xxxxxxxxRDWRx| wrapped and full! rdidx = wridx */
+ /* Rotate entire buffer 8 bytes back */
+ v64 = *(uint64_t*)BUF2PTY(ts);
+ memmove(BUF2PTY(ts), BUF2PTY(ts) + 8, TO_PTY_BUFSIZE - 8);
+ *(uint64_t*)(BUF2PTY(ts) + TO_PTY_BUFSIZE - 8) = v64;
+ ts->wridx -= 8; /* can't underflow */
+ buf -= 8;
+ BUF2PTY_DEC(ts->rdidx, 8); /* can underflow, use DEC() */
}
- rc = 2;
- goto update_and_return;
}
+
+ if (buf[1] == IAC) /* IAC-IAC: we have something to write */
+ return size;
+
if (buf[1] >= 240 && buf[1] <= 249) {
+ /* 2-byte commands (240..250 and 255):
+ * IAC IAC (255) Literal 255. Supported.
+ * IAC SE (240) End of subnegotiation. Treated as NOP.
+ * IAC NOP (241) NOP. Supported.
+ * IAC BRK (243) Break. Like serial line break. TODO via tcsendbreak()?
+ * IAC AYT (246) Are you there.
+ * These don't look useful:
+ * IAC DM (242) Data mark. What is this?
+ * IAC IP (244) Suspend, interrupt or abort the process. (Ancient cousin of ^C).
+ * IAC AO (245) Abort output. "You can continue running, but do not send me the output".
+ * IAC EC (247) Erase character. The receiver should delete the last received char.
+ * IAC EL (248) Erase line. The receiver should delete everything up tp last newline.
+ * IAC GA (249) Go ahead. For half-duplex lines: "now you talk".
+ */
+ if (buf[1] == AYT && ts->sibling) /* notify other pipe that AYT was seen */
+ ts->sibling->respond_to_AYT = 1;
/* NOP (241). Ignore (putty keepalive, etc) */
/* All other 2-byte commands also treated as NOPs here */
- rc = 2;
- goto update_and_return;
+ increment = 2;
+ goto inc;
}
-
- if (wr <= 2) {
-/* BUG: only 2 bytes of the IAC is in the buffer, we just eat them.
- * This is not a practical problem since >2 byte IACs are seen only
- * in initial negotiation, when buffer is empty
- */
- rc = 2;
- goto update_and_return;
+ if (size <= 2) {
+ /* only 2 bytes of the longer IAC is in the buffer */
+ goto cant_write; /* incomplete, can't process */
}
-
if (buf[1] == SB) {
if (buf[2] == TELOPT_NAWS) {
- /* IAC SB, TELOPT_NAWS, 4-byte, IAC SE */
+ /* IAC, SB, TELOPT_NAWS, 4-byte, IAC SE */
struct winsize ws;
- if (wr <= 6) {
-/* BUG: incomplete, can't process */
- rc = wr;
- goto update_and_return;
- }
+ if (size <= 6) /* incomplete, can't process */
+ goto cant_write;
memset(&ws, 0, sizeof(ws)); /* pixel sizes are set to 0 */
ws.ws_col = (buf[3] << 8) | buf[4];
ws.ws_row = (buf[5] << 8) | buf[6];
- ioctl(ts->ptyfd, TIOCSWINSZ, (char *)&ws);
- rc = 7;
+//TODO: sanity check: are they nonzero?
+ ioctl(ts->write_fd, TIOCSWINSZ, (char *)&ws);
+ increment = 7;
+ goto inc;
/* trailing IAC SE will be eaten separately, as 2-byte NOP */
- goto update_and_return;
}
+//fixme: skip them correctly
/* else: other subnegs not supported yet */
}
/* Assume it is a 3-byte WILL/WONT/DO/DONT 251..254 command and skip it */
-#if DEBUG
- fprintf(stderr, "Ignoring IAC %s,%s\n",
- TELCMD(buf[1]), TELOPT(buf[2]));
-#endif
- rc = 3;
-
- update_and_return:
- ts->wridx1 += rc;
- if (ts->wridx1 >= BUFSIZE) /* actually == BUFSIZE */
- ts->wridx1 = 0;
- ts->size1 -= rc;
- /*
- * Hack. We cannot process IACs which wrap around buffer's end.
- * Since properly fixing it requires writing bigger code,
- * we rely instead on this code making it virtually impossible
- * to have wrapped IAC (people don't type at 2k/second).
- * It also allows for bigger reads in common case.
- */
- if (ts->size1 == 0) { /* very typical */
- //bb_error_msg("zero size1");
- ts->rdidx1 = 0;
- ts->wridx1 = 0;
- return rc;
- }
- wr = ts->wridx1;
- if (wr != 0 && wr < ts->rdidx1) {
- /* Buffer is not wrapped yet.
- * We can easily move it to the beginning.
+ dbg("ignoring IAC,%02x,%02x", buf[1], buf[2]);
+ increment = 3;
+ goto inc;
+}
+
+/* Write some buf data to pty, processing IACs.
+ * Update wridx and size. Return < 0 on error.
+ */
+static int net_to_pty__write(void *this)
+{
+ //connection_t *conn = this;
+ net_to_pty_t *ts = this;
+ unsigned wr;
+ ssize_t rc;
+ unsigned char *buf;
+ unsigned char *found;
+
+ if (ts->write_fd < 0) /* did slave close its pty? */
+ return 0;
+
+ buf = BUF2PTY(ts) + ts->wridx;
+ wr = MIN(TO_PTY_BUFSIZE - ts->wridx, ts->size);
+ /* wr is at least 1 here */
+
+ found = memchr(buf, IAC, wr);
+ if (found == buf) {
+ /* The first char is IAC.
+ * have_data_to_write() ensures we are only called this way
+ * if there are two IACs.
+ * It also ensures the buffer is not wrapping within 7 chars.
+ * Write one IAC. If that works, skip both.
*/
- //bb_error_msg("moved %d", wr);
- memmove(TS_BUF1(ts), TS_BUF1(ts) + wr, ts->size1);
- ts->rdidx1 -= wr;
- ts->wridx1 = 0;
+//TODO: advance ptr by one, and memchr up to _next_ IAC (if any):
+//this would write more data
+ rc = safe_write(ts->write_fd, buf, 1);
+ if (rc > 0) {
+ //dbg_iac("wrote: IAC");
+ ts->wridx += 2; /* this can't overflow */
+ ts->size -= 2;
+ }
+ /* Leave it to pty2net side to deal with closing pty on errors
+ * (it'll see them when it tries to read pty).
+ */
+ return 0; /* "didn't write anything" */
+ }
+ /* No leading IACs: found != buf.
+ * IOW: there is a "prefix" of non-IAC chars.
+ * Write only them, and return.
+ */
+ if (found)
+ wr = found - buf;
+
+ /* We map \r\n ==> \r for pragmatic reasons:
+ * many client implementations send \r\n when
+ * the user hits the CarriageReturn key.
+ * See RFC 1123 3.3.1 Telnet End-of-Line Convention.
+ */
+ found = memchr(buf, '\r', wr);
+ if (found)
+ wr = found - buf + 1; /* write up to and including \r */
+ rc = safe_write(ts->write_fd, buf, wr);
+ if (rc <= 0)
+ return 0; /* "didn't write anything" */
+ //dbg_iac("wrote: %s", bin_to_hex(buf, rc));
+ /* check is needed: imagine that write was _short_ */
+ if (buf[rc - 1] == '\r') /* last written byte was CR? */
+ ts->skip_LF_or_NUL = 1;
+
+ BUF2PTY_INC(ts->wridx, rc);
+ ts->size -= rc;
+ if (ts->size == 0) { /* very typical */
+ /* Avoid buffer wrapping most of the time (-> have bigger writes) */
+ //dbg_buffer("zero size");
+ ts->rdidx = 0;
+ ts->wridx = 0;
}
return rc;
}
-/*
- * Converting single IAC into double on output
- */
-static size_t safe_write_double_iac(int fd, const char *buf, size_t count)
+/* Check if buffer has space to read into */
+static int net_to_pty__have_buffer_to_read_into(void *this)
{
- const char *IACptr;
- size_t wr, rc, total;
+ //connection_t *conn = this;
+ net_to_pty_t *ts = this;
+
+ /* the sibling may have closed our ptyfd after getting 10 EIO's */
+ if (ts->write_fd < 0
+ || ts->shell_pid < 0 /* -K option, and our pty's pid has exited */
+ ) {
+ if (ts->sibling) {
+ /* do not close our netfd, it's in use by sibling */
+ ts->read_fd = -1;
+ }
+ dbg_close("%s:%d: remove_and_free_to_pty:%p wr:%d pid:%d",
+ __func__, __LINE__, ts, ts->write_fd, ts->shell_pid);
+ remove_and_free_to_pty(ts);
+ return -1; /* "don't use me anymore, I'm gone" */
+ }
- total = 0;
- while (1) {
- if (count == 0)
- return total;
- if (*buf == (char)IAC) {
- static const char IACIAC[] ALIGN1 = { IAC, IAC };
- rc = safe_write(fd, IACIAC, 2);
-/* BUG: if partial write was only 1 byte long, we end up emitting just one IAC */
- if (rc != 2)
- break;
- buf++;
- total++;
- count--;
- continue;
+ if (ts->read_fd < 0) { /* we've seen EOF/error on netfd */
+ dbg_close("EOF on netfd was seen, ts->size:%d", ts->size);
+ if (ts->size == 0) { /* we wrote everything */
+ if (!ts->sibling) { /* if not in use by sibling... */
+ /* close ptyfd, but after a pause */
+ unsigned rem;
+
+ /* pty has a design problem: close(master_ptyfd)
+ * is defined as causing hangup on slave pty.
+ * Which discards all currently buffered unread data
+ * (in our case, all data we just wrote).
+ * usleep(1000) is a crude solution.
+ * A better one (does not block other conns):
+ */
+ dbg_close("going to close ptyfd:%d deadline_us:%u",
+ ts->write_fd, ts->deadline_us);
+ if (!ts->deadline_us) {
+ ts->deadline_us = (monotonic_us() + 1000) | 1;
+ rem = 1000;
+ } else {
+ rem = ts->deadline_us - monotonic_us();
+ }
+ dbg_close("until ptyfd_close:%d", rem);
+ if (rem <= 1000) {
+ ioloop_decrease_current_timeout(ts->io, rem);
+ return 0; /* "do not read" */
+ }
+ /* rem undeflowed (is "negative"): we waited at least 1000us */
+ dbg_close("EOF on netfd, closing ptyfd:%d", ts->write_fd);
+ close(ts->write_fd);
+ }
+ ts->write_fd = -1;
+ remove_and_free_to_pty(ts);
+ return -1; /* "don't use me anymore, I'm gone" */
}
- /* count != 0, *buf != IAC */
- IACptr = memchr(buf, IAC, count);
- wr = count;
- if (IACptr)
- wr = IACptr - buf;
- rc = safe_write(fd, buf, wr);
- if (rc != wr)
- break;
- buf += rc;
- total += rc;
- count -= rc;
+ return 0; /* "don't want to read" */
}
- /* here: rc - result of last short write */
- if ((ssize_t)rc < 0) { /* error? */
- if (total == 0)
- return rc;
- rc = 0;
+
+ return ts->size < TO_PTY_BUFSIZE;
+}
+
+/* Read from socket to buffer, stripping trailing NUL if present */
+static int net_to_pty__read(void *this)
+{
+ net_to_pty_t *ts = this;
+ ssize_t count;
+ int avail;
+
+ /* Calculate available space and contiguous segment */
+ avail = TO_PTY_BUFSIZE - ts->size;
+ //if (avail <= 0)
+ // return 0; /* buffer full */
+ // ioloop logic ensures this is false
+
+ /* Read into contiguous segment from rdidx */
+ count = MIN(TO_PTY_BUFSIZE - ts->rdidx, avail);
+ count = safe_read(ts->read_fd, BUF2PTY(ts) + ts->rdidx, count);
+ if (count <= 0) {
+ dbg_close("EOF/error on netfd:%d sibling:%p", ts->read_fd, ts->sibling);
+ /* There is no way to signal to pty that input is in "error state",
+ * therefore treat both EOF and error the same.
+ */
+ if (!ts->sibling)
+ close(ts->read_fd); /* close netfd if not in use by sibling */
+ ts->read_fd = -1;
+ return 0; /* "didn't read anything */
}
- return total + rc;
+
+ ts->size += count;
+ BUF2PTY_INC(ts->rdidx, count);
+
+ return count;
}
-/* Must match getopt32 string */
-enum {
- OPT_WATCHCHILD = (1 << 2), /* -K */
- OPT_INETD = (1 << 3) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -i */
- OPT_PORT = (1 << 4) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -p PORT */
- OPT_FOREGROUND = (1 << 6) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -F */
- OPT_SYSLOG = (1 << 7) * ENABLE_FEATURE_TELNETD_INETD_WAIT, /* -S */
- OPT_WAIT = (1 << 8) * ENABLE_FEATURE_TELNETD_INETD_WAIT, /* -w SEC */
-};
+static net_to_pty_t *new_net_to_pty(int from, int to)
+{
+ net_to_pty_t *this = xzalloc(sizeof(*this) + TO_PTY_BUFSIZE);
+ this->have_buffer_to_read_into = net_to_pty__have_buffer_to_read_into;
+ this->have_data_to_write = net_to_pty__have_data_to_write;
+ this->read = net_to_pty__read;
+ this->write = net_to_pty__write;
+ this->read_fd = from;
+ this->write_fd = to;
+ /* indexes and size are all 0 */
+ return this;
+}
+
+static int pty_to_net__have_buffer_to_read_into(void *this)
+{
+ //connection_t *conn = this;
+ pty_to_net_t *ts = this;
+
+ if (ts->read_fd < 0 /* we saw fatal read error before */
+ || ts->shell_pid < 0 /* -K option, and our pty's pid has exited */
+ ) {
+ if (ts->size == 0) { /* output flushed? */
+ remove_and_free_to_net(ts);
+ return -1; /* "don't use me anymore, I'm gone" */
+ }
+ return 0; /* "don't read" */
+ }
+ if (ts->eio_count != 0) {
+ unsigned rem;
+
+ //if (ts->eio_count >= 10)
+ // return 0; /* "do not read" */
+ //^^^ can't reach, caught by "ts->read_fd < 0" above.
+
+ /* last read was EIO. Still waiting before retry? */
+ rem = ts->deadline_us - monotonic_us();
+ if (rem <= 1000) {
+ /* tell io loop that poll timeout should now be "rem" us, not infinity */
+ ioloop_decrease_current_timeout(ts->io, rem);
+ return 0; /* "do not read me (yet)" */
+ }
+ /* else: rem underflowed: 1000us passed, can try reading again */
+ }
+ return ts->size < TO_NET_BUFSIZE;
+}
+
+/* Read from pty to buffer, handling vhangup() EIO retries */
+static int pty_to_net__read(void *this)
+{
+ //connection_t *conn = this;
+ pty_to_net_t *ts = this;
+ ssize_t count;
+ int avail;
+
+ /* Calculate available space and contiguous segment */
+ avail = TO_NET_BUFSIZE - ts->size;
+ //if (avail <= 0)
+ // return 0; /* buffer full */
+ // io loop logic ensures this is false
+
+ /* Read into contiguous segment from rdidx */
+ count = MIN(TO_NET_BUFSIZE - ts->rdidx, avail);
+ count = safe_read(ts->read_fd, BUF2NET(ts) + ts->rdidx, count);
+ if (count <= 0) {
+ if (count < 0) {
+ if (errno == EAGAIN)
+ return 0;
+ /* login process might call vhangup(),
+ * which causes intermittent EIOs on read above
+ * (observed on kernel 4.12.0, not seen on 5.18.0).
+ * Try up to 10*1000 us.
+ */
+ if (errno == EIO) {
+ ts->eio_count++;
+ dbg_close("EIO ptyfd:%d ts->eio_count:%d %06u",
+ ts->read_fd, ts->eio_count,
+ (unsigned)(monotonic_us() % 1000000)
+ );
+ if (ts->eio_count >= 10)
+ goto close_ptyfd;
+ ts->deadline_us = monotonic_us() + 1000;
+ return 0; /* "read nothing" */
+ }
+ dbg_close("error on ptyfd:%d %m", ts->read_fd);
+ /* (what other read errors from pty are possible, apart from EIO?) */
+ ts->eio_count = 10; /* "fatally bad error" */
+ goto close_ptyfd;
+ }
+ /* EOF. (Doesn't happen with master ptys on Linux when slave closes - always EIO instead?) */
+ dbg_close("EOF on ptyfd:%d", ts->read_fd);
+ ts->eio_count = 0;
+ close_ptyfd:
+ dbg_close("closing ptyfd:%d", ts->read_fd);
+ close(ts->read_fd);
+ if (ts->sibling)
+ /* net2pty pipe needs to close (nowhere to write its data) */
+ ts->sibling->write_fd = -1;
+ ts->read_fd = -1;
+ /* Returning -1 here would mean "do not try writing to net", which is wrong */
+ return 0; /* EOF or error, but we do not prevent draining to net */
+ }
+ ts->eio_count = 0;
+
+ ts->size += count;
+ BUF2NET_INC(ts->rdidx, count);
+
+ return count;
+}
+
+static int pty_to_net__have_data_to_write(void *this)
+{
+ pty_to_net_t *ts = this;
+ if (ts->size == 0) {
+ /* Wrote everything and pty is dead? */
+ if (ts->read_fd < 0) {
+ dbg_close("%s:%d: remove_and_free_to_net:%p rd:%d eio_count:%d",
+ __func__, __LINE__, ts, ts->read_fd, ts->eio_count);
+ if (ts->sibling)
+ ts->write_fd = -1;
+ remove_and_free_to_net(ts);
+ return -1; /* "I'm gone" */
+ }
+ }
+ return ts->size > 0 || ts->respond_to_AYT || ts->extra_byte_to_write;
+}
+
+/* Write to socket from buffer, escaping IAC bytes */
+static int pty_to_net__write(void *this)
+{
+ pty_to_net_t *ts = this;
+ ssize_t count;
+ unsigned char *buf;
+ unsigned char *iac_ptr;
+ size_t wr;
+
+ /* Did we fail to send a byte last time? */
+ if (ts->extra_byte_to_write) {
+ count = safe_write(ts->write_fd, &ts->extra_byte_to_write, 1);
+ if (count == 1)
+ ts->extra_byte_to_write = 0;
+ goto ret;
+ }
+
+ if (ts->size == 0) {
+ if (ts->respond_to_AYT) { /* other side pinged us? */
+ /* Send back evidence that AYT was seen: IAC NOP */
+ static const char ayt_response[2] = { IAC, NOP };
+ count = safe_write(ts->write_fd, ayt_response, 2);
+ if (count >= 1) {
+ if (count == 1) /* goddamit */
+ ts->extra_byte_to_write = NOP;
+ ts->respond_to_AYT = 0;
+ }
+ goto ret;
+ }
+ return 0;
+ }
+
+ buf = BUF2NET(ts) + ts->wridx;
+ wr = MIN(TO_NET_BUFSIZE - ts->wridx, ts->size);
+ dbg("rem:%d sz:%d ch:0x%02x", TO_NET_BUFSIZE - ts->wridx, ts->size, *buf);
+
+ if (*buf == IAC) {
+ static const char IACIAC[2] ALIGN1 = { IAC, IAC };
+ count = safe_write(ts->write_fd, IACIAC, 2);
+ if (count <= 1) {
+ if (count < 1) /* wrote nothing? */
+ goto ret;
+ /* Wrote only one byte of two. Amazing... */
+ ts->extra_byte_to_write = IAC;
+ }
+ /* Consume one byte from buffer */
+ count = 1;
+ } else {
+ /* Find next IAC or write up to end of segment */
+ iac_ptr = memchr(buf, IAC, wr);
+ if (iac_ptr)
+ wr = iac_ptr - buf;
+ count = safe_write(ts->write_fd, buf, wr);
+ if (count <= 0)
+ goto ret;
+ }
+
+ BUF2NET_INC(ts->wridx, count);
+ ts->size -= count;
+ if (ts->size == 0) { /* very typical */
+ /* Avoid buffer wrapping most of the time (-> have bigger writes) */
+ //dbg_buffer("zero size");
+ ts->rdidx = 0;
+ ts->wridx = 0;
+ }
+ ret:
+ if (count >= 0)
+ return count; /* "not error" */
+ if (errno == EAGAIN)
+ return 0; /* "not error" */
+
+ /* Assuming fatal write error */
+ /* If peer closes its read side with
+ * shutdown(SHUT_RD), we'll see EPIPE
+ * or ECONNRESET.
+ * (both were seen to occur in practice)
+ */
+ if (ts->sibling) {
+ /* Sibling exists, don't close yet */
+ ts->read_fd = -1;
+ ts->write_fd = -1;
+ }
+ dbg_close("%s:%d: remove_and_free_to_net:%p", __func__, __LINE__, ts);
+ remove_and_free_to_net(ts);
+ return -1; /* "I'm gone" */
+}
+
+static pty_to_net_t *new_pty_to_net(int from, int to)
+{
+ pty_to_net_t *this = xzalloc(sizeof(*this) + TO_NET_BUFSIZE);
+ this->have_buffer_to_read_into = pty_to_net__have_buffer_to_read_into;
+ this->have_data_to_write = pty_to_net__have_data_to_write;
+ this->read = pty_to_net__read;
+ this->write = pty_to_net__write;
+ this->read_fd = from;
+ this->write_fd = to;
+ /* indexes and size are all 0 */
+ return this;
+}
-static struct tsession *
-make_new_session(
- IF_FEATURE_TELNETD_STANDALONE(int sock)
- IF_NOT_FEATURE_TELNETD_STANDALONE(void)
-) {
#if !ENABLE_FEATURE_TELNETD_STANDALONE
- enum { sock = 0 };
+#define make_new_session(io, sockrd) \
+ make_new_session(io)
#endif
+static void make_new_session(ioloop_state_t *io, int sockrd)
+{
+ IF_NOT_FEATURE_TELNETD_STANDALONE(enum { sockrd = 0 };)
+ const int sockwr = sockrd != 0 ? sockrd : 1;
const char *login_argv[2];
struct termios termbuf;
int fd, pid;
char tty_name[GETPTY_BUFSIZE];
- struct tsession *ts = xzalloc(sizeof(struct tsession) + BUFSIZE * 2);
-
- /*ts->buf1 = (char *)(ts + 1);*/
- /*ts->buf2 = ts->buf1 + BUFSIZE;*/
/* Got a new connection, set up a tty */
fd = xgetpty(tty_name);
- if (fd > G.maxfd)
- G.maxfd = fd;
- ts->ptyfd = fd;
ndelay_on(fd);
close_on_exec_on(fd);
/* SO_KEEPALIVE by popular demand */
- setsockopt_keepalive(sock);
-#if ENABLE_FEATURE_TELNETD_STANDALONE
- ts->sockfd_read = sock;
- ndelay_on(sock);
- if (sock == 0) { /* We are called with fd 0 - we are in inetd mode */
- sock++; /* so use fd 1 for output */
- ndelay_on(sock);
- }
- ts->sockfd_write = sock;
- if (sock > G.maxfd)
- G.maxfd = sock;
-#else
- /* ts->sockfd_read = 0; - done by xzalloc */
- ts->sockfd_write = 1;
- ndelay_on(0);
- ndelay_on(1);
-#endif
+ setsockopt_keepalive(sockrd);
+ ndelay_on(sockrd);
+ if (sockrd == 0) /* called with fd 0 (inetd mode?) */
+ ndelay_on(sockwr);
/* Make the telnet client understand we will echo characters so it
* should not do it locally. We don't tell the client to run linemode,
@@ -481,34 +815,33 @@ make_new_session(
IAC, WILL, TELOPT_ECHO,
IAC, WILL, TELOPT_SGA
};
- /* This confuses safe_write_double_iac(), it will try to duplicate
- * each IAC... */
- //memcpy(TS_BUF2(ts), iacs_to_send, sizeof(iacs_to_send));
- //ts->rdidx2 = sizeof(iacs_to_send);
- //ts->size2 = sizeof(iacs_to_send);
- /* So just stuff it into TCP stream! (no error check...) */
-#if ENABLE_FEATURE_TELNETD_STANDALONE
- safe_write(sock, iacs_to_send, sizeof(iacs_to_send));
-#else
- safe_write(1, iacs_to_send, sizeof(iacs_to_send));
-#endif
- /*ts->rdidx2 = 0; - xzalloc did it */
- /*ts->size2 = 0;*/
+ /* Just stuff it into TCP stream! (no error check...) */
+ safe_write(sockwr, iacs_to_send, sizeof(iacs_to_send));
}
fflush_all();
pid = vfork(); /* NOMMU-friendly */
if (pid < 0) {
- free(ts);
close(fd);
- /* sock will be closed by caller */
+ /* socket will be closed by caller */
bb_simple_perror_msg("vfork");
- return NULL;
+ return;
}
if (pid > 0) {
/* Parent */
- ts->shell_pid = pid;
- return ts;
+ pty_to_net_t *to_net;
+ net_to_pty_t *to_pty;
+
+ to_net = new_pty_to_net(fd, sockwr);
+ to_pty = new_net_to_pty(sockrd, fd);
+ dbg("to_net:%p to_pty:%p", to_net, to_pty);
+ to_pty->sibling = to_net;
+ to_net->sibling = to_pty;
+ to_net->shell_pid = pid;
+ to_pty->shell_pid = pid;
+ ioloop_insert_conn(io, (connection_t *)to_net);
+ ioloop_insert_conn(io, (connection_t *)to_pty);
+ return;
}
/* Child */
@@ -517,10 +850,21 @@ make_new_session(
/* Restore default signal handling ASAP */
bb_signals((1 << SIGCHLD) + (1 << SIGPIPE), SIG_DFL);
- pid = getpid();
+ /* Make new session and process group */
+ //pid = getpid(); // redundant, setsid gives us our pid
+ pid = setsid();
+
+ /* Open the child's side of the tty */
+ /* NB: setsid() disconnects from any previous ctty's. Therefore
+ * we must open child's side of the tty AFTER setsid! */
+ close(0);
+ xopen(tty_name, O_RDWR); /* fd 0: becomes our ctty */
+ xdup2(0, 1);
+ xdup2(0, 2);
+ tcsetpgrp(0, pid); /* switch this tty's process group to us */
if (ENABLE_FEATURE_UTMP) {
- len_and_sockaddr *lsa = get_peer_lsa(sock);
+ len_and_sockaddr *lsa = get_peer_lsa(sockrd);
char *hostname = NULL;
if (lsa) {
hostname = xmalloc_sockaddr2dotted(&lsa->u.sa);
@@ -530,18 +874,6 @@ make_new_session(
free(hostname);
}
- /* Make new session and process group */
- setsid();
-
- /* Open the child's side of the tty */
- /* NB: setsid() disconnects from any previous ctty's. Therefore
- * we must open child's side of the tty AFTER setsid! */
- close(0);
- xopen(tty_name, O_RDWR); /* becomes our ctty */
- xdup2(0, 1);
- xdup2(0, 2);
- tcsetpgrp(0, pid); /* switch this tty's process group to us */
-
/* The pseudo-terminal allocated to the client is configured to operate
* in cooked mode, and with XTABS CRMOD enabled (see tty(4)) */
tcgetattr(0, &termbuf);
@@ -574,113 +906,689 @@ make_new_session(
_exit_FAILURE(); /*bb_perror_msg_and_die("execv %s", G.loginpath);*/
}
+
#if ENABLE_FEATURE_TELNETD_STANDALONE
+struct accept_conn {
+ TELNET_CONNECTION
+};
-static void
-free_session(struct tsession *ts)
+static int accept_conn__can_accept(void *this UNUSED_PARAM)
{
- struct tsession *t;
-
- if (option_mask32 & OPT_INETD)
- exit_SUCCESS();
+ return 1;
+}
- /* Unlink this telnet session from the session list */
- t = G.sessions;
- if (t == ts)
- G.sessions = ts->next;
- else {
- while (t->next != ts)
- t = t->next;
- t->next = ts->next;
- }
+static int accept_conn__accept(void *this)
+{
+ struct accept_conn *ts = this;
+ int fd;
-#if 0
- /* It was said that "normal" telnetd just closes ptyfd,
- * doesn't send SIGKILL. When we close ptyfd,
- * kernel sends SIGHUP to processes having slave side opened. */
- kill(ts->shell_pid, SIGKILL);
- waitpid(ts->shell_pid, NULL, 0);
-#endif
- close(ts->ptyfd);
- close(ts->sockfd_read);
- /* We do not need to close(ts->sockfd_write), it's the same
- * as sockfd_read unless we are in inetd mode. But in inetd mode
- * we do not reach this */
- free(ts);
-
- /* Scan all sessions and find new maxfd */
- G.maxfd = 0;
- ts = G.sessions;
- while (ts) {
- if (G.maxfd < ts->ptyfd)
- G.maxfd = ts->ptyfd;
- if (G.maxfd < ts->sockfd_read)
- G.maxfd = ts->sockfd_read;
-#if 0
- /* Again, sockfd_write == sockfd_read here */
- if (G.maxfd < ts->sockfd_write)
- G.maxfd = ts->sockfd_write;
-#endif
- ts = ts->next;
+ dbg("%s:%d", __func__, __LINE__);
+ fd = accept(ts->read_fd, NULL, NULL);
+//TODO: accept4(SOCK_NONBLOCK) - can remove ndelay_on() in make_new_session
+ if (fd >= 0) {
+ close_on_exec_on(fd);
+ /* Create two new pipes and insert them into ioloop */
+ make_new_session(ts->io, fd);
}
+ return 0;
}
-#else /* !FEATURE_TELNETD_STANDALONE */
-
-/* Used in main() only, thus "return 0" actually is exit(EXIT_SUCCESS). */
-#define free_session(ts) return 0
+static int accept_conn__return_zero(void *this UNUSED_PARAM)
+{
+ return 0;
+}
+static struct accept_conn *new_accept_conn(int fd)
+{
+ struct accept_conn *this = xzalloc(sizeof(*this));
+ this->have_buffer_to_read_into = accept_conn__can_accept;
+ this->have_data_to_write = accept_conn__return_zero;
+ this->read = accept_conn__accept;
+ //this->write = accept_conn__return_zero; //never called
+ this->read_fd = fd;
+ this->write_fd = -1;
+ return this;
+}
#endif
static void handle_sigchld(int sig UNUSED_PARAM)
{
- pid_t pid;
- struct tsession *ts;
int save_errno = errno;
/* Looping: more than one child may have exited */
while (1) {
+ pid_t pid;
+ struct telnet_conn *conn;
+
pid = wait_any_nohang(NULL);
if (pid <= 0)
break;
- ts = G.sessions;
- while (ts) {
- if (ts->shell_pid == pid) {
- ts->shell_pid = -1;
- update_utmp_DEAD_PROCESS(pid);
- break;
+ update_utmp_DEAD_PROCESS(pid);
+ conn = (void*)G.io.conns;
+ while (conn) {
+ if (conn->shell_pid == pid) {
+ /* mark the conn to close soon */
+ conn->shell_pid = -1;
}
- ts = ts->next;
+ conn = (void*)conn->next;
}
}
errno = save_errno;
}
+#if ENABLE_FEATURE_TELNETD_SELFTEST_DEBUG
+
+static char *bin_to_hex(const void *hash_value, unsigned hash_length)
+{
+ /* xzalloc zero-terminates */
+ char *hex_value = xzalloc((hash_length * 2) + 1);
+ bin2hex(hex_value, (char*)hash_value, hash_length);
+ return auto_string(hex_value);
+}
+
+static int test_vhangup(void)
+{
+ char tty_name[GETPTY_BUFSIZE];
+ int master_fd;
+ int last_rc = 999;
+ int last_errno = 0;
+ unsigned long long last_change_ns, base_ns;
+
+ bb_simple_info_msg("selftest: pty vhangup() behavior");
+
+ master_fd = xgetpty(tty_name);
+ ndelay_on(master_fd);
+
+ fflush_all();
+ if (xfork() == 0) {
+ /* Child */
+ //int slave_fd;
+ int fd;
+
+ /* vhangup() sends SIGHUP to the session, ignore it */
+ signal(SIGHUP, SIG_IGN);
+
+ applet_name = "vhangup child";
+
+ close(master_fd);
+ setsid();
+ /*slave_fd =*/ xopen(tty_name, O_RDWR);
+
+ fd = open("/dev/tty", O_RDWR);
+ if (fd >= 0) {
+ bb_simple_info_msg("/dev/tty opened OK - we have a ctty");
+ close(fd);
+ } else {
+ bb_simple_perror_msg("/dev/tty");
+ }
+
+ bb_simple_info_msg("sleeping 0.2 sec before vhangup()");
+ usleep(200*1000);
+
+ bb_simple_info_msg("calling vhangup()");
+ vhangup();
+
+ fd = open("/dev/tty", O_RDWR);
+ if (fd >= 0) {
+ bb_simple_info_msg("/dev/tty opened OK - we still have a ctty");
+ close(fd);
+ } else {
+ bb_simple_perror_msg("/dev/tty");
+ }
+
+ bb_simple_info_msg("sleeping 0.2 sec after vhangup()");
+ usleep(200*1000);
+
+ bb_simple_error_msg_and_die("exiting");
+ }
+
+ /* Parent */
+ bb_simple_info_msg("nonblocking read() from pty master in a loop...");
+ last_change_ns = monotonic_ns();
+
+ base_ns = monotonic_ns();
+ for (;;) {
+ int rc;
+ char buf[1];
+ unsigned long long now_ns;
+
+ errno = 0;
+ rc = read(master_fd, buf, 1);
+ now_ns = monotonic_ns() - base_ns;
+
+ if (rc != last_rc || errno != last_errno) {
+ bb_error_msg("t=%lluns: read()=%d errno=%d(%s)",
+ now_ns,
+ rc, errno, strerror(errno));
+ last_rc = rc;
+ last_errno = errno;
+ last_change_ns = now_ns;
+ }
+
+ /* Exit if no change for 2 seconds */
+ if ((now_ns - last_change_ns) > 1000*1000*1000) {
+ bb_simple_info_msg("no change for 1 second, stopping read() loop");
+ break;
+ }
+
+ usleep(100); /* 100us polling - fast enough to catch transitions */
+ }
+
+ close(master_fd);
+ waitpid(-1, NULL, 0);
+
+ bb_simple_error_msg("vhangup test completed");
+ return 0;
+}
+static int test_net_to_pty_data_integrity(void)
+{
+ enum { TESTDATA_SIZE = 16 * 1024 * 1024 };
+ unsigned char *input_data;
+ unsigned char *expected_data;
+ int input_size;
+ int expected_size;
+ uint32_t randbits;
+ uint8_t expected_hash32[32];
+ uint8_t hash_in_pty32[32];
+ uint8_t dummy_byte;
+ int sock_pair[2];
+ char tty_name[GETPTY_BUFSIZE];
+ int ptyfd;
+ int i;
+ int waitstatus, exitcode;
+
+ bb_simple_info_msg("selftest: net_to_pty");
+
+ /* Allocate buffer with paranoia space */
+ input_data = xzalloc(TESTDATA_SIZE + 64);
+
+ /* Generate nightmare input data:
+ * - Mostly random bytes
+ * - IAC bytes sprinkled in (1 in 16 probability)
+ * - IAC IAC sequences (1 in 32)
+ * - IAC SB TELOPT_NAWS sequences(1 in 32)
+ * - All sorts of IAC commands with random parameters
+ * - Malformed sequences
+ */
+ randbits = 0;
+ for (i = 0; i < TESTDATA_SIZE;) {
+ unsigned _32possibilities;
+ if (randbits == 0)
+ randbits = random();
+ _32possibilities = (randbits & 0x1f);
+ randbits >>= 5;
+ if (_32possibilities == 3 && (randbits & 0x7f) == 0) { /* 1 in 32*127=4*1k prob: long run of printables */
+ if (i < TESTDATA_SIZE - 4*1024) {
+ int end = i + ((4*1024 - 1) & random());
+ while (i < end)
+ input_data[i++] = (random() & 0x3f) + 0x20;
+ continue;
+ }
+ }
+ if (_32possibilities == 3) { /* CR,LF/NUL: 1 in 32 prob */
+ input_data[i++] = '\r';
+ input_data[i++] = randbits & 1 ? '\n' : '\0';
+ } else
+ if (_32possibilities == 2) { /* NAWS: 1 in 32 prob */
+ input_data[i++] = IAC;
+ input_data[i++] = SB;
+ input_data[i++] = TELOPT_NAWS;
+ } else
+ if (_32possibilities == 1) { /* IAC: 1 in 32 prob */
+ input_data[i++] = IAC;
+ if (random() & 1) /* IAC IAC: 1 in 64 prob */
+ input_data[i++] = IAC;
+ } else {
+ input_data[i++] = random();
+ }
+ }
+ input_size = TESTDATA_SIZE;
+ bb_error_msg("generated %d bytes of test data", input_size);
+
+ /* Allocate expected output buffer */
+ expected_data = xmalloc(TESTDATA_SIZE);
+
+ /* Process input_data simulating net_to_pty transformations:
+ * - IAC IAC → single IAC
+ * - IAC SB TELOPT_NAWS [4 bytes] IAC SE → stripped
+ * - Other IAC sequences → stripped
+ * - \r\n → \r (skip the \n)
+ * - \r\0 → \r (skip the \0)
+ */
+ memset(input_data + input_size, 0, 64);
+ expected_size = 0;
+ i = 0;
+ while (i < input_size) {
+ /* Tail of 64 extra zero bytes allows to safely read beyond the end */
+ if (input_data[i] == IAC) {
+ if (input_data[i + 1] == IAC) {
+ /* IAC IAC → single IAC */
+ expected_data[expected_size++] = IAC;
+ i += 2;
+ continue;
+ }
+ if (input_data[i + 1] >= 240 && input_data[i + 1] <= 249) {
+ /* 2-byte IAC command (NOP, etc.) - skip */
+ i += 2;
+ continue;
+ }
+ if (input_data[i + 1] == SB) {
+ /* IAC SB ... skip subnegotiation */
+ if (input_data[i + 2] == TELOPT_NAWS) {
+ /* IAC SB TELOPT_NAWS [4 bytes] */
+ i += 3; /* skip IAC SB TELOPT_NAWS */
+ /* Skip 4 bytes of window size */
+ i += 4;
+ continue;
+// } else {
+//should do: /* Other subneg, skip to IAC SE */ but that's not what telnetd code is doing yet...
+ }
+ }
+ /* Assumed 3-byte IAC WILL/WONT/DO/DONT - skip */
+ i += 3;
+ continue;
+ }
+ if (input_data[i] == '\r') {
+ /* Write \r, check if followed by \n or \0 */
+ expected_data[expected_size++] = '\r';
+ i++;
+ if (input_data[i] == '\n' || input_data[i] == '\0')
+ i++; /* skip \n or \0 after \r */
+ continue;
+ }
+ /* Regular byte */
+ expected_data[expected_size++] = input_data[i++];
+ }
+ sha256_block(expected_data, expected_size, expected_hash32);
+ bb_error_msg("expected %d bytes after net_to_pty processing. hash:%s", expected_size, bin_to_hex(expected_hash32, 32));
+#if SAVE_NET2PTY_EXPECTED
+ {
+ int fd = xopen("expected.bin", O_WRONLY | O_CREAT | O_TRUNC);
+ xwrite(fd, expected_data, expected_size);
+ close(fd);
+ bb_error_msg("wrote expected.bin");
+ }
+#endif
+ free(expected_data);
+ expected_data = NULL; /* to catch use-after-free */
+
+ /* Create socketpair for network side */
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock_pair) < 0)
+ bb_simple_perror_msg_and_die("socketpair");
+
+ fflush_all();
+ if (xfork() == 0) {
+ /* Fork writer child: writes telnet data to socket in random chunks */
+ int written = 0;
+ close(sock_pair[0]); /* close read end */
+
+ applet_name = "tty input";
+
+ while (written < input_size) {
+ int chunk = (random() & 4095) + 1; /* 1 to 4096 bytes */
+ if (written + chunk > input_size)
+ chunk = input_size - written;
+
+ chunk = safe_write(sock_pair[1], input_data + written, chunk);
+ if (chunk > 0)
+ written += chunk;
+
+ /* Small random delay to vary timing */
+ if ((random() & 0xf) == 0)
+ usleep(random() & 0x3ff);
+ }
+ _exit(0);
+ }
+ close(sock_pair[1]); /* close write end */
+
+ free(input_data);
+ input_data = NULL; /* to catch use-after-free */
+
+ /* Create pty pair */
+ ptyfd = xgetpty(tty_name);
+
+ if (xfork() == 0) {
+ /* Fork reader child: reads from PTY slave */
+ int slave_fd;
+ int retry;
+ int n;
+ unsigned char read_buf[8192];
+ int total_read = 0;
+ struct termios termbuf;
+ sha256_ctx_t ctx;
+#if SAVE_NET2PTY_ACTUAL
+ int out_fd = xopen("actual.bin", O_WRONLY | O_CREAT | O_TRUNC);
+#endif
+ applet_name = "pty reader";
+
+ close(sock_pair[0]);
+ close(ptyfd);
+
+ slave_fd = xopen(tty_name, O_RDWR);
+ setsid(); /* this loses ctty (no tty signals ^C, ^Z, ^\; no SIGHUP on master close (probably?) */
+
+ tcgetattr(slave_fd, &termbuf);
+ cfmakeraw(&termbuf);
+ tcsetattr(slave_fd, TCSANOW, &termbuf);
+ //bb_error_msg("tty_name:'%s'", tty_name);
+
+ /* Synchronize with master: "raw set, you can start writing" */
+ dummy_byte = '!';
+ safe_write(slave_fd, &dummy_byte, 1);
+
+ sha256_begin(&ctx);
+ /* Retry logic shows how master pty close looks on slave side:
+ * on kernel 5.18.0, I see EIO once, then (if I retry reads),
+ * I see EOFs (zero reads).
+ * What is not visible (but does happen) is that any
+ * unread-by-me buffered data is lost - something that
+ * wouldn't happen on pipes/socketpairs!
+ */
+ retry = 0;
+ for (;;) {
+ n = safe_read(slave_fd, read_buf, sizeof(read_buf));
+ if (n == 0) {
+ retry++;
+ bb_error_msg("EOF on read(%d)", retry);
+ if (retry < 3) continue;
+ break; /* EOF (parent closed master) - expected */
+ }
+ if (n < 0) {
+ retry++;
+ bb_perror_msg("read(%d)", retry);
+ if (retry < 3) continue;
+ break; /* error (parent closed master) - expected */
+ }
+ /* If happens, probably kenel bug! */
+ if (retry != 0) bb_error_msg_and_die("READ DATA AFTER EOF/ERROR RETRY!!!");
+#if SAVE_NET2PTY_ACTUAL
+ xwrite(out_fd, read_buf, n);
+#endif
+ sha256_hash(&ctx, read_buf, n);
+ total_read += n;
+ // Test premature close on pty read side:
+ //if (total_read >= 128*1024) _exit(0);
+ }
+
+ sha256_end(&ctx, hash_in_pty32);
+ n = memcmp(hash_in_pty32, expected_hash32, 32);
+ bb_error_msg("received %d bytes, hash:%s%s", total_read, bin_to_hex(hash_in_pty32, 32), n ? " MISMATCH" : "");
+ _exit(!!n);
+ }
+
+ /* Parent: run the net_to_pty logic */
+ {
+ net_to_pty_t *conn;
+ ioloop_state_t *io;
+ int rc;
+
+ io = new_ioloop_state();
+ conn = new_net_to_pty(sock_pair[0], ptyfd);
+ ioloop_insert_conn(io, (void*)conn);
+
+ /* We must allow the pty child to change its tty to RAW
+ * before we start writing. Otherwise (it was observed),
+ * it can see a few \r -> \n remapped.
+ */
+ safe_read(ptyfd, &dummy_byte, 1);
+
+ rc = ioloop_run(io);
+ bb_error_msg("ioloop_run:%d", rc);
+
+ /* This should be done inside ioloop. Check where possible: */
+ //free_connection(conn);
+ if (close(sock_pair[0]) == 0) /* should be EBADF */
+ bb_error_msg_and_die("BUG: net read side wasn't closed");
+ if (close(ptyfd) == 0)
+ bb_error_msg_and_die("BUG: pty write side wasn't closed");
+
+ free_ioloop_state(io);
+ }
+
+ /* Wait for all children */
+ exitcode = 0;
+ while (safe_waitpid(-1, &waitstatus, 0) > 0)
+ exitcode |= (waitstatus != 0);
+
+ bb_error_msg("pty_to_net test completed: %d", exitcode);
+ return exitcode;
+}
+
+static int test_pty_to_net_data_integrity(void)
+{
+ enum { TESTDATA_SIZE = 16 * 1024 * 1024 };
+ unsigned char *input_data;
+ unsigned char *expected_data;
+ int input_size;
+ int expected_size;
+ uint32_t randbits;
+ uint8_t expected_hash32[32];
+ uint8_t hash_from_net32[32];
+ int sock_pair[2];
+ char tty_name[GETPTY_BUFSIZE];
+ int ptyfd;
+ int i;
+ int waitstatus, exitcode;
+
+ bb_simple_info_msg("selftest: pty_to_net");
+
+ /* Allocate buffer */
+ input_data = xzalloc(TESTDATA_SIZE);
+
+ /* Generate random data - this time we want lots of 0xFF (IAC) bytes
+ * to test IAC escaping (IAC → IAC IAC)
+ */
+ randbits = 0;
+ for (i = 0; i < TESTDATA_SIZE;) {
+ unsigned _32possibilities;
+ if (randbits == 0)
+ randbits = random();
+ _32possibilities = (randbits & 0x1f);
+ randbits >>= 5;
+
+ if (_32possibilities == 1 && (randbits & 0x7f) == 0) {
+ /* Long run of printables (1 in ~4k prob) */
+ if (i < TESTDATA_SIZE - 4*1024) {
+ int end = i + ((4*1024 - 1) & random());
+ while (i < end)
+ input_data[i++] = (random() & 0x3f) + 0x20;
+ continue;
+ }
+ }
+ if (_32possibilities == 1) {
+ /* IAC byte: 1 in 32 prob */
+ input_data[i++] = IAC;
+ } else {
+ input_data[i++] = random();
+ }
+ }
+ input_size = TESTDATA_SIZE;
+ bb_error_msg("generated %d bytes of test data", input_size);
+
+ /* Allocate expected output buffer (worst case: all IACs doubled) */
+ expected_data = xmalloc(TESTDATA_SIZE * 2);
+
+ /* Process input_data simulating pty_to_net transformations:
+ * - IAC (0xFF) → IAC IAC (doubled)
+ * - All other bytes pass through unchanged
+ */
+ expected_size = 0;
+ for (i = 0; i < input_size; i++) {
+ if (input_data[i] == IAC) {
+ expected_data[expected_size++] = IAC;
+ expected_data[expected_size++] = IAC;
+ } else {
+ expected_data[expected_size++] = input_data[i];
+ }
+ }
+
+ sha256_block(expected_data, expected_size, expected_hash32);
+ bb_error_msg("expected %d bytes after pty_to_net processing. hash:%s",
+ expected_size, bin_to_hex(expected_hash32, 32));
+
+#if SAVE_PTY2NET_EXPECTED
+ {
+ int fd = xopen("expected.bin", O_WRONLY | O_CREAT | O_TRUNC);
+ xwrite(fd, expected_data, expected_size);
+ close(fd);
+ bb_error_msg("wrote expected.bin");
+ }
+#endif
+
+ free(expected_data);
+ expected_data = NULL;
+
+ /* Create socketpair for network side */
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock_pair) < 0)
+ bb_simple_perror_msg_and_die("socketpair");
+
+ /* Create pty pair */
+ ptyfd = xgetpty(tty_name);
+
+ fflush_all();
+ if (xfork() == 0) {
+ /* Fork writer child: writes data to PTY slave */
+ int slave_fd;
+ int written = 0;
+ struct termios termbuf;
+
+ applet_name = "pty writer";
+
+ close(sock_pair[0]);
+ close(sock_pair[1]);
+ close(ptyfd);
+
+ slave_fd = xopen(tty_name, O_RDWR);
+ //setsid(); /* lose ctty */
+
+ /* Put PTY in raw mode */
+ tcgetattr(slave_fd, &termbuf);
+ cfmakeraw(&termbuf);
+ tcsetattr(slave_fd, TCSANOW, &termbuf);
+
+ while (written < input_size) {
+ int n = (random() & 4095) + 1; /* 1 to 4096 bytes */
+ if (n > input_size - written)
+ n = input_size - written;
+ n = safe_write(slave_fd, input_data + written, n);
+ if (n < 0)
+ bb_perror_msg_and_die("%s:%d: write error", __func__, __LINE__);
+ written += n;
+ /* Small random delay */
+ if ((random() & 0xf) == 0)
+ usleep(random() & 0x3ff);
+ }
+ _exit(0);
+ }
+
+ free(input_data);
+ input_data = NULL;
+
+ if (xfork() == 0) {
+ /* Fork reader child: reads from network socket */
+ int n;
+ unsigned char read_buf[8192];
+ int total_read = 0;
+ sha256_ctx_t ctx;
+#if SAVE_PTY2NET_ACTUAL
+ int out_fd = xopen("actual.bin", O_WRONLY | O_CREAT | O_TRUNC);
+#endif
+ applet_name = "net reader";
+
+ close(sock_pair[1]); /* close write end */
+ close(ptyfd);
+
+ sha256_begin(&ctx);
+ for (;;) {
+ n = safe_read(sock_pair[0], read_buf, sizeof(read_buf));
+ if (n <= 0) {
+ if (n < 0)
+ bb_simple_perror_msg("read");
+ break; /* EOF or error */
+ }
+ // To test what writer sees if we close read end:
+ //shutdown(sock_pair[0], SHUT_RD);
+ //sleep(2);
+ //_exit(1);
+#if SAVE_PTY2NET_ACTUAL
+ xwrite(out_fd, read_buf, n);
+#endif
+ sha256_hash(&ctx, read_buf, n);
+ total_read += n;
+ }
+
+ sha256_end(&ctx, hash_from_net32);
+ n = memcmp(hash_from_net32, expected_hash32, 32);
+ bb_error_msg("received %d bytes, hash:%s%s",
+ total_read, bin_to_hex(hash_from_net32, 32), n ? " MISMATCH" : "");
+ _exit(!!n);
+ }
+
+ close(sock_pair[0]); /* close read end */
+
+ /* Parent: run the pty_to_net logic */
+ {
+ pty_to_net_t *conn;
+ ioloop_state_t *io;
+ int rc;
+
+ io = new_ioloop_state();
+
+ conn = new_pty_to_net(ptyfd, sock_pair[1]);
+ ioloop_insert_conn(io, (void*)conn);
+
+ rc = ioloop_run(io);
+ bb_error_msg("ioloop_run:%d", rc);
+
+ /* This should be done inside ioloop. Check where possible: */
+ //free_connection(conn);
+ if (close(sock_pair[1]) == 0) /* should be EBADF */
+ bb_error_msg_and_die("BUG: net write side wasn't closed");
+ if (close(ptyfd) == 0)
+ bb_error_msg_and_die("BUG: pty read side wasn't closed");
+
+ free_ioloop_state(io);
+ }
+
+ /* Wait for all children */
+ exitcode = 0;
+ while (safe_waitpid(-1, &waitstatus, 0) > 0)
+ exitcode |= (waitstatus != 0);
+
+ bb_error_msg("pty_to_net test completed: %d", exitcode);
+ return exitcode;
+}
+
+static void selftest(void)
+{
+ int exitcode = 0;
+ srandom(12345);
+ if (SELFTEST_VHANGUP) exitcode |= test_vhangup();
+ if (SELFTEST_NET2PTY) exitcode |= test_net_to_pty_data_integrity();
+ if (SELFTEST_PTY2NET) exitcode |= test_pty_to_net_data_integrity();
+ bb_error_msg("selftest completed: %d", exitcode);
+ _exit(exitcode);
+}
+#endif
+
int telnetd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int telnetd_main(int argc UNUSED_PARAM, char **argv)
{
- fd_set rdfdset, wrfdset;
unsigned opt;
- int count;
- struct tsession *ts;
#if ENABLE_FEATURE_TELNETD_STANDALONE
#define IS_INETD (opt & OPT_INETD)
- int master_fd = master_fd; /* for compiler */
- int sec_linger = sec_linger;
char *opt_bindaddr = NULL;
char *opt_portnbr;
#else
- enum {
- IS_INETD = 1,
- master_fd = -1,
- };
+ enum { IS_INETD = 1 };
#endif
INIT_G();
/* Even if !STANDALONE, we accept (and ignore) -i, thus people
* don't need to guess whether it's ok to pass -i to us */
opt = getopt32(argv, "^"
+ IF_FEATURE_TELNETD_SELFTEST_DEBUG("@")
"f:l:Ki"
IF_FEATURE_TELNETD_STANDALONE("p:b:F")
IF_FEATURE_TELNETD_INETD_WAIT("Sw:+") /* -w NUM */
@@ -689,8 +1597,15 @@ int telnetd_main(int argc UNUSED_PARAM, char **argv)
IF_FEATURE_TELNETD_INETD_WAIT("wF:i--w:w--i"),
&G.issuefile, &G.loginpath
IF_FEATURE_TELNETD_STANDALONE(, &opt_portnbr, &opt_bindaddr)
- IF_FEATURE_TELNETD_INETD_WAIT(, &sec_linger)
+ IF_FEATURE_TELNETD_INETD_WAIT(, &G.io.max_timeout)
);
+
+#if ENABLE_FEATURE_TELNETD_SELFTEST_DEBUG
+ if (opt & 1) { /* -@ is first option */
+ selftest(); /* does not return */
+ }
+ opt >>= 1; /* Shift away the selftest bit */
+#endif
if (!IS_INETD /*&& !re_execed*/) {
/* inform that we start in standalone mode?
* May be useful when people forget to give -i */
@@ -706,13 +1621,13 @@ int telnetd_main(int argc UNUSED_PARAM, char **argv)
openlog(applet_name, LOG_PID, LOG_DAEMON);
logmode = LOGMODE_SYSLOG;
}
-#if ENABLE_FEATURE_TELNETD_STANDALONE
+
if (IS_INETD) {
- G.sessions = make_new_session(0);
- if (!G.sessions) /* pty opening or vfork problem, exit */
- return 1; /* make_new_session printed error message */
- } else {
- master_fd = 0;
+ make_new_session(&G.io, 0);
+ }
+#if ENABLE_FEATURE_TELNETD_STANDALONE
+ else {
+ int master_fd = 0;
if (!(opt & OPT_WAIT)) {
unsigned portnbr = CONFIG_FEATURE_TELNETD_PORT_DEFAULT;
if (opt & OPT_PORT)
@@ -721,11 +1636,8 @@ int telnetd_main(int argc UNUSED_PARAM, char **argv)
xlisten(master_fd, 1);
}
close_on_exec_on(master_fd);
+ ioloop_insert_conn(&G.io, (void *)new_accept_conn(master_fd));
}
-#else
- G.sessions = make_new_session();
- if (!G.sessions) /* pty opening or vfork problem, exit */
- return 1; /* make_new_session printed error message */
#endif
/* We don't want to die if just one session is broken */
@@ -736,189 +1648,30 @@ int telnetd_main(int argc UNUSED_PARAM, char **argv)
else /* prevent dead children from becoming zombies */
signal(SIGCHLD, SIG_IGN);
-/*
- This is how the buffers are used. The arrows indicate data flow.
-
- +-------+ wridx1++ +------+ rdidx1++ +----------+
- | | <-------------- | buf1 | <-------------- | |
- | | size1-- +------+ size1++ | |
- | pty | | socket |
- | | rdidx2++ +------+ wridx2++ | |
- | | --------------> | buf2 | --------------> | |
- +-------+ size2++ +------+ size2-- +----------+
-
- size1: "how many bytes are buffered for pty between rdidx1 and wridx1?"
- size2: "how many bytes are buffered for socket between rdidx2 and wridx2?"
-
- Each session has got two buffers. Buffers are circular. If sizeN == 0,
- buffer is empty. If sizeN == BUFSIZE, buffer is full. In both these cases
- rdidxN == wridxN.
-*/
- again:
- FD_ZERO(&rdfdset);
- FD_ZERO(&wrfdset);
-
- /* Select on the master socket, all telnet sockets and their
- * ptys if there is room in their session buffers.
- * NB: scalability problem: we recalculate entire bitmap
- * before each select. Can be a problem with 500+ connections. */
- ts = G.sessions;
- while (ts) {
- struct tsession *next = ts->next; /* in case we free ts */
- if (ts->shell_pid == -1) {
- /* Child died and we detected that */
- free_session(ts);
- } else {
- if (ts->size1 > 0) /* can write to pty */
- FD_SET(ts->ptyfd, &wrfdset);
- if (ts->size1 < BUFSIZE) /* can read from socket */
- FD_SET(ts->sockfd_read, &rdfdset);
- if (ts->size2 > 0) /* can write to socket */
- FD_SET(ts->sockfd_write, &wrfdset);
- if (ts->size2 < BUFSIZE) /* can read from pty */
- FD_SET(ts->ptyfd, &rdfdset);
- }
- ts = next;
- }
- if (!IS_INETD) {
- FD_SET(master_fd, &rdfdset);
- /* This is needed because free_session() does not
- * take master_fd into account when it finds new
- * maxfd among remaining fd's */
- if (master_fd > G.maxfd)
- G.maxfd = master_fd;
- }
-
- {
- struct timeval *tv_ptr = NULL;
#if ENABLE_FEATURE_TELNETD_INETD_WAIT
- struct timeval tv;
- if ((opt & OPT_WAIT) && !G.sessions) {
- tv.tv_sec = sec_linger;
- tv.tv_usec = 0;
- tv_ptr = &tv;
- }
-#endif
- count = select(G.maxfd + 1, &rdfdset, &wrfdset, NULL, tv_ptr);
- }
- if (count == 0) /* "telnetd -w SEC" timed out */
- return 0;
- if (count < 0)
- goto again; /* EINTR or ENOMEM */
-
-#if ENABLE_FEATURE_TELNETD_STANDALONE
- /* Check for and accept new sessions */
- if (!IS_INETD && FD_ISSET(master_fd, &rdfdset)) {
- int fd;
- struct tsession *new_ts;
-
- fd = accept(master_fd, NULL, NULL);
- if (fd < 0)
- goto again;
- close_on_exec_on(fd);
-
- /* Create a new session and link it into active list */
- new_ts = make_new_session(fd);
- if (new_ts) {
- new_ts->next = G.sessions;
- G.sessions = new_ts;
- } else {
- close(fd);
- }
- }
+ if (G.io.max_timeout > UINT_MAX / 1000000) /* -w TMOUT capped at 4294 sec */
+ G.io.max_timeout = (UINT_MAX / 1000000);
+ G.io.max_timeout *= 1000000;
#endif
-
- /* Then check for data tunneling */
- ts = G.sessions;
- while (ts) { /* For all sessions... */
- struct tsession *next = ts->next; /* in case we free ts */
-
- if (/*ts->size1 &&*/ FD_ISSET(ts->ptyfd, &wrfdset)) {
- /* Write to pty from buffer 1 */
- count = safe_write_to_pty_decode_iac(ts);
- if (count < 0) {
- if (errno == EAGAIN)
- goto skip1;
- goto kill_session;
- }
+ G.io.flags |= IOLOOP_FLAG_EXIT_IF_TIMEOUT;
+ for (;;) {
+ int rc = ioloop_run(&G.io);
+ if (rc == IOLOOP_NO_CONNS) {
+ /* inetd mode (not -w), and it's closed now */
+ dbg_close("connection is closed");
+ break;
}
- skip1:
- if (/*ts->size2 &&*/ FD_ISSET(ts->sockfd_write, &wrfdset)) {
- /* Write to socket from buffer 2 */
- count = MIN(BUFSIZE - ts->wridx2, ts->size2);
- count = safe_write_double_iac(ts->sockfd_write, (void*)(TS_BUF2(ts) + ts->wridx2), count);
- if (count < 0) {
- if (errno == EAGAIN)
- goto skip2;
- goto kill_session;
- }
- ts->wridx2 += count;
- if (ts->wridx2 >= BUFSIZE) /* actually == BUFSIZE */
- ts->wridx2 = 0;
- ts->size2 -= count;
- if (ts->size2 == 0) {
- ts->rdidx2 = 0;
- ts->wridx2 = 0;
- }
+ /* else: timeout */
+#if ENABLE_FEATURE_TELNETD_INETD_WAIT
+ if (1 /* rc == IOLOOP_TIMEOUT */
+ && (!G.io.conns || !G.io.conns->next) /* only accept conn exists */
+ ) {
+ dbg_close("-w TIMEOUT:%dus", G.io.max_timeout);
+ break;
}
- skip2:
-
- if (/*ts->size1 < BUFSIZE &&*/ FD_ISSET(ts->sockfd_read, &rdfdset)) {
- /* Read from socket to buffer 1 */
- count = MIN(BUFSIZE - ts->rdidx1, BUFSIZE - ts->size1);
- count = safe_read(ts->sockfd_read, TS_BUF1(ts) + ts->rdidx1, count);
- if (count <= 0) {
- if (count < 0 && errno == EAGAIN)
- goto skip3;
- goto kill_session;
- }
- /* Ignore trailing NUL if it is there */
- if (!TS_BUF1(ts)[ts->rdidx1 + count - 1]) {
- --count;
- }
- ts->size1 += count;
- ts->rdidx1 += count;
- if (ts->rdidx1 >= BUFSIZE) /* actually == BUFSIZE */
- ts->rdidx1 = 0;
- }
- skip3:
- if (/*ts->size2 < BUFSIZE &&*/ FD_ISSET(ts->ptyfd, &rdfdset)) {
- /* Read from pty to buffer 2 */
- int eio = 0;
- read_pty:
- count = MIN(BUFSIZE - ts->rdidx2, BUFSIZE - ts->size2);
- count = safe_read(ts->ptyfd, TS_BUF2(ts) + ts->rdidx2, count);
- if (count <= 0) {
- if (count < 0) {
- if (errno == EAGAIN)
- goto skip4;
- /* login process might call vhangup(),
- * which causes intermittent EIOs on read above
- * (observed on kernel 4.12.0). Try up to 10 ms.
- */
- if (errno == EIO && eio < 10) {
- eio++;
- //bb_error_msg("EIO pty %u", eio);
- usleep(1000);
- goto read_pty;
- }
- }
- goto kill_session;
- }
- ts->size2 += count;
- ts->rdidx2 += count;
- if (ts->rdidx2 >= BUFSIZE) /* actually == BUFSIZE */
- ts->rdidx2 = 0;
- }
- skip4:
- ts = next;
- continue;
- kill_session:
- if (ts->shell_pid > 0)
- update_utmp_DEAD_PROCESS(ts->shell_pid);
- free_session(ts);
- ts = next;
- }
-
- goto again;
+#endif
+ dbg("timeout was due to temporary EIO: continue looping");
+ }
+ dbg_close("terminating");
+ return EXIT_SUCCESS;
}
networking/telnetd.c.OLD+924 -0
@@ -0,0 +1,924 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * Simple telnet server
+ * Bjorn Wesen, Axis Communications AB ([email protected])
+ *
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
+ *
+ * ---------------------------------------------------------------------------
+ * (C) Copyright 2000, Axis Communications AB, LUND, SWEDEN
+ ****************************************************************************
+ *
+ * The telnetd manpage says it all:
+ *
+ * Telnetd operates by allocating a pseudo-terminal device (see pty(4)) for
+ * a client, then creating a login process which has the slave side of the
+ * pseudo-terminal as stdin, stdout, and stderr. Telnetd manipulates the
+ * master side of the pseudo-terminal, implementing the telnet protocol and
+ * passing characters between the remote client and the login process.
+ *
+ * Vladimir Oleynik <[email protected]> 2001
+ * Set process group corrections, initial busybox port
+ */
+//config:config TELNETD
+//config: bool "telnetd (13 kb)"
+//config: default y
+//config: select FEATURE_SYSLOG
+//config: help
+//config: A daemon for the TELNET protocol, allowing you to log onto the host
+//config: running the daemon. Please keep in mind that the TELNET protocol
+//config: sends passwords in plain text. If you can't afford the space for an
+//config: SSH daemon and you trust your network, you may say 'y' here. As a
+//config: more secure alternative, you should seriously consider installing the
+//config: very small Dropbear SSH daemon instead:
+//config: http://matt.ucc.asn.au/dropbear/dropbear.html
+//config:
+//config: Note that for busybox telnetd to work you need several things:
+//config: First of all, your kernel needs:
+//config: CONFIG_UNIX98_PTYS=y
+//config:
+//config: Next, you need a /dev/pts directory on your root filesystem:
+//config:
+//config: $ ls -ld /dev/pts
+//config: drwxr-xr-x 2 root root 0 Sep 23 13:21 /dev/pts/
+//config:
+//config: Next you need the pseudo terminal master multiplexer /dev/ptmx:
+//config:
+//config: $ ls -la /dev/ptmx
+//config: crw-rw-rw- 1 root tty 5, 2 Sep 23 13:55 /dev/ptmx
+//config:
+//config: Any /dev/ttyp[0-9]* files you may have can be removed.
+//config: Next, you need to mount the devpts filesystem on /dev/pts using:
+//config:
+//config: mount -t devpts devpts /dev/pts
+//config:
+//config: You need to be sure that busybox has LOGIN and
+//config: FEATURE_SUID enabled. And finally, you should make
+//config: certain that busybox has been installed setuid root:
+//config:
+//config: chown root.root /bin/busybox
+//config: chmod 4755 /bin/busybox
+//config:
+//config: with all that done, telnetd _should_ work....
+//config:
+//config:config FEATURE_TELNETD_STANDALONE
+//config: bool "Support standalone telnetd (not inetd only)"
+//config: default y
+//config: depends on TELNETD
+//config: help
+//config: Selecting this will make telnetd able to run standalone.
+//config:
+//config:config FEATURE_TELNETD_PORT_DEFAULT
+//config: int "Default port"
+//config: default 23
+//config: range 1 65535
+//config: depends on FEATURE_TELNETD_STANDALONE
+//config:
+//config:config FEATURE_TELNETD_INETD_WAIT
+//config: bool "Support -w SEC option (inetd wait mode)"
+//config: default y
+//config: depends on FEATURE_TELNETD_STANDALONE
+//config: help
+//config: This option allows you to run telnetd in "inet wait" mode.
+//config: Example inetd.conf line (note "wait", not usual "nowait"):
+//config:
+//config: telnet stream tcp wait root /bin/telnetd telnetd -w10
+//config:
+//config: In this example, inetd passes _listening_ socket_ as fd 0
+//config: to telnetd when connection appears.
+//config: telnetd will wait for connections until all existing
+//config: connections are closed, and no new connections
+//config: appear during 10 seconds. Then it exits, and inetd continues
+//config: to listen for new connections.
+//config:
+//config: This option is rarely used. "tcp nowait" is much more usual
+//config: way of running tcp services, including telnetd.
+//config: You most probably want to say N here.
+
+//applet:IF_TELNETD(APPLET(telnetd, BB_DIR_USR_SBIN, BB_SUID_DROP))
+
+//kbuild:lib-$(CONFIG_TELNETD) += telnetd.o
+
+//usage:#define telnetd_trivial_usage
+//usage: "[OPTIONS]"
+//usage:#define telnetd_full_usage "\n\n"
+//usage: "Handle incoming telnet connections"
+//usage: IF_NOT_FEATURE_TELNETD_STANDALONE(" via inetd") "\n"
+//usage: "\n -l LOGIN Exec LOGIN on connect (default /bin/login)"
+//usage: "\n -f ISSUE_FILE Display ISSUE_FILE instead of /etc/issue.net"
+//usage: "\n -K Close connection as soon as login exits"
+//usage: "\n (normally wait until all programs close slave pty)"
+//usage: IF_FEATURE_TELNETD_STANDALONE(
+//usage: "\n -p PORT Port to listen on. Default "STR(CONFIG_FEATURE_TELNETD_PORT_DEFAULT)
+//usage: "\n -b ADDR[:PORT] Address to bind to"
+//usage: "\n -F Run in foreground"
+//usage: "\n -i Inetd mode"
+//usage: IF_FEATURE_TELNETD_INETD_WAIT(
+//usage: "\n -w SEC Inetd 'wait' mode, linger time SEC"
+//usage: "\n inetd.conf line: 23 stream tcp wait root telnetd telnetd -w10"
+//usage: "\n -S Log to syslog (implied by -i or without -F and -w)"
+//usage: )
+//usage: )
+
+#define DEBUG 0
+
+#include "libbb.h"
+#include "common_bufsiz.h"
+#include <syslog.h>
+
+#if DEBUG
+# define TELCMDS
+# define TELOPTS
+#endif
+#include <arpa/telnet.h>
+
+
+struct tsession {
+ struct tsession *next;
+ pid_t shell_pid;
+ int sockfd_read;
+ int sockfd_write;
+ int ptyfd;
+ smallint buffered_IAC_for_pty;
+
+ /* two circular buffers */
+ /*char *buf1, *buf2;*/
+/*#define TS_BUF1(ts) ts->buf1*/
+/*#define TS_BUF2(ts) TS_BUF2(ts)*/
+#define TS_BUF1(ts) ((unsigned char*)(ts + 1))
+#define TS_BUF2(ts) (((unsigned char*)(ts + 1)) + BUFSIZE)
+ int rdidx1, wridx1, size1;
+ int rdidx2, wridx2, size2;
+};
+
+/* Two buffers are directly after tsession in malloced memory.
+ * Make whole thing fit in 4k */
+enum { BUFSIZE = (4 * 1024 - sizeof(struct tsession)) / 2 };
+
+
+/* Globals */
+struct globals {
+ struct tsession *sessions;
+ const char *loginpath;
+ const char *issuefile;
+ int maxfd;
+} FIX_ALIASING;
+#define G (*(struct globals*)bb_common_bufsiz1)
+#define INIT_G() do { \
+ setup_common_bufsiz(); \
+ G.loginpath = "/bin/login"; \
+ G.issuefile = "/etc/issue.net"; \
+} while (0)
+
+
+/* Write some buf1 data to pty, processing IACs.
+ * Update wridx1 and size1. Return < 0 on error.
+ * Buggy if IAC is present but incomplete: skips them.
+ */
+static ssize_t
+safe_write_to_pty_decode_iac(struct tsession *ts)
+{
+ unsigned wr;
+ ssize_t rc;
+ unsigned char *buf;
+ unsigned char *found;
+
+ buf = TS_BUF1(ts) + ts->wridx1;
+ wr = MIN(BUFSIZE - ts->wridx1, ts->size1);
+ /* wr is at least 1 here */
+
+ if (ts->buffered_IAC_for_pty) {
+ /* Last time we stopped on a "dangling" IAC byte.
+ * We removed it from the buffer back then.
+ * Now pretend it's still there, and jump to IAC processing.
+ */
+ ts->buffered_IAC_for_pty = 0;
+ wr++;
+ ts->size1++;
+ buf--; /* Yes, this can point before the buffer. It's ok */
+ ts->wridx1--;
+ goto handle_iac;
+ }
+
+ found = memchr(buf, IAC, wr);
+ if (found != buf) {
+ /* There is a "prefix" of non-IAC chars.
+ * Write only them, and return.
+ */
+ if (found)
+ wr = found - buf;
+
+ /* We map \r\n ==> \r for pragmatic reasons:
+ * many client implementations send \r\n when
+ * the user hits the CarriageReturn key.
+ * See RFC 1123 3.3.1 Telnet End-of-Line Convention.
+ */
+ rc = wr;
+ found = memchr(buf, '\r', wr);
+ if (found)
+ rc = found - buf + 1;
+ rc = safe_write(ts->ptyfd, buf, rc);
+ if (rc <= 0)
+ return rc;
+ if (rc < wr /* don't look past available data */
+ && buf[rc-1] == '\r' /* need this: imagine that write was _short_ */
+ && (buf[rc] == '\n' || buf[rc] == '\0')
+ ) {
+ rc++;
+ }
+ goto update_and_return;
+ }
+
+ /* buf starts with IAC char. Process that sequence.
+ * Example: we get this from our own (bbox) telnet client:
+ * read(5, "\377\374\1""\377\373\37""\377\372\37\0\262\0@\377\360""\377\375\1""\377\375\3"):
+ * IAC WONT ECHO, IAC WILL NAWS, IAC SB NAWS <cols> <rows> IAC SE, IAC DO SGA
+ * Another example (telnet-0.17 from old-netkit):
+ * read(4, "\377\375\3""\377\373\30""\377\373\37""\377\373 ""\377\373!""\377\373\"""\377\373'"
+ * "\377\375\5""\377\373#""\377\374\1""\377\372\37\0\257\0I\377\360""\377\375\1"):
+ * IAC DO SGA, IAC WILL TTYPE, IAC WILL NAWS, IAC WILL TSPEED, IAC WILL LFLOW, IAC WILL LINEMODE, IAC WILL NEW_ENVIRON,
+ * IAC DO STATUS, IAC WILL XDISPLOC, IAC WONT ECHO, IAC SB NAWS <cols> <rows> IAC SE, IAC DO ECHO
+ */
+ if (wr <= 1) {
+ /* Only the single IAC byte is in the buffer, eat it
+ * and set a flag "process the rest of the sequence
+ * next time we are here".
+ */
+ //bb_error_msg("dangling IAC!");
+ ts->buffered_IAC_for_pty = 1;
+ rc = 1;
+ goto update_and_return;
+ }
+
+ handle_iac:
+ /* 2-byte commands (240..250 and 255):
+ * IAC IAC (255) Literal 255. Supported.
+ * IAC SE (240) End of subnegotiation. Treated as NOP.
+ * IAC NOP (241) NOP. Supported.
+ * IAC BRK (243) Break. Like serial line break. TODO via tcsendbreak()?
+ * IAC AYT (246) Are you there.
+ * These don't look useful:
+ * IAC DM (242) Data mark. What is this?
+ * IAC IP (244) Suspend, interrupt or abort the process. (Ancient cousin of ^C).
+ * IAC AO (245) Abort output. "You can continue running, but do not send me the output".
+ * IAC EC (247) Erase character. The receiver should delete the last received char.
+ * IAC EL (248) Erase line. The receiver should delete everything up tp last newline.
+ * IAC GA (249) Go ahead. For half-duplex lines: "now you talk".
+ * Implemented only as part of NAWS:
+ * IAC SB (250) Subnegotiation of an option follows.
+ */
+ if (buf[1] == IAC) {
+ /* Literal 255 (emacs M-DEL) */
+ //bb_error_msg("255!");
+ rc = safe_write(ts->ptyfd, &buf[1], 1);
+ /*
+ * If we went through buffered_IAC_for_pty==1 path,
+ * bailing out on error like below messes up the buffer.
+ * EAGAIN is highly unlikely here, other errors will be
+ * repeated on next write, let's just skip error check.
+ */
+#if 0
+ if (rc <= 0)
+ return rc;
+#endif
+ rc = 2;
+ goto update_and_return;
+ }
+ if (buf[1] == AYT) {
+ if (ts->size2 == 0) { /* if nothing buffered yet... */
+ /* Send back evidence that AYT was seen */
+ unsigned char *buf2 = TS_BUF2(ts);
+ buf2[0] = IAC;
+ buf2[1] = NOP;
+ ts->wridx2 = 0;
+ ts->rdidx2 = ts->size2 = 2;
+ }
+ rc = 2;
+ goto update_and_return;
+ }
+ if (buf[1] >= 240 && buf[1] <= 249) {
+ /* NOP (241). Ignore (putty keepalive, etc) */
+ /* All other 2-byte commands also treated as NOPs here */
+ rc = 2;
+ goto update_and_return;
+ }
+
+ if (wr <= 2) {
+/* BUG: only 2 bytes of the IAC is in the buffer, we just eat them.
+ * This is not a practical problem since >2 byte IACs are seen only
+ * in initial negotiation, when buffer is empty
+ */
+ rc = 2;
+ goto update_and_return;
+ }
+
+ if (buf[1] == SB) {
+ if (buf[2] == TELOPT_NAWS) {
+ /* IAC SB, TELOPT_NAWS, 4-byte, IAC SE */
+ struct winsize ws;
+ if (wr <= 6) {
+/* BUG: incomplete, can't process */
+ rc = wr;
+ goto update_and_return;
+ }
+ memset(&ws, 0, sizeof(ws)); /* pixel sizes are set to 0 */
+ ws.ws_col = (buf[3] << 8) | buf[4];
+ ws.ws_row = (buf[5] << 8) | buf[6];
+ ioctl(ts->ptyfd, TIOCSWINSZ, (char *)&ws);
+ rc = 7;
+ /* trailing IAC SE will be eaten separately, as 2-byte NOP */
+ goto update_and_return;
+ }
+ /* else: other subnegs not supported yet */
+ }
+
+ /* Assume it is a 3-byte WILL/WONT/DO/DONT 251..254 command and skip it */
+#if DEBUG
+ fprintf(stderr, "Ignoring IAC %s,%s\n",
+ TELCMD(buf[1]), TELOPT(buf[2]));
+#endif
+ rc = 3;
+
+ update_and_return:
+ ts->wridx1 += rc;
+ if (ts->wridx1 >= BUFSIZE) /* actually == BUFSIZE */
+ ts->wridx1 = 0;
+ ts->size1 -= rc;
+ /*
+ * Hack. We cannot process IACs which wrap around buffer's end.
+ * Since properly fixing it requires writing bigger code,
+ * we rely instead on this code making it virtually impossible
+ * to have wrapped IAC (people don't type at 2k/second).
+ * It also allows for bigger reads in common case.
+ */
+ if (ts->size1 == 0) { /* very typical */
+ //bb_error_msg("zero size1");
+ ts->rdidx1 = 0;
+ ts->wridx1 = 0;
+ return rc;
+ }
+ wr = ts->wridx1;
+ if (wr != 0 && wr < ts->rdidx1) {
+ /* Buffer is not wrapped yet.
+ * We can easily move it to the beginning.
+ */
+ //bb_error_msg("moved %d", wr);
+ memmove(TS_BUF1(ts), TS_BUF1(ts) + wr, ts->size1);
+ ts->rdidx1 -= wr;
+ ts->wridx1 = 0;
+ }
+ return rc;
+}
+
+/*
+ * Converting single IAC into double on output
+ */
+static size_t safe_write_double_iac(int fd, const char *buf, size_t count)
+{
+ const char *IACptr;
+ size_t wr, rc, total;
+
+ total = 0;
+ while (1) {
+ if (count == 0)
+ return total;
+ if (*buf == (char)IAC) {
+ static const char IACIAC[] ALIGN1 = { IAC, IAC };
+ rc = safe_write(fd, IACIAC, 2);
+/* BUG: if partial write was only 1 byte long, we end up emitting just one IAC */
+ if (rc != 2)
+ break;
+ buf++;
+ total++;
+ count--;
+ continue;
+ }
+ /* count != 0, *buf != IAC */
+ IACptr = memchr(buf, IAC, count);
+ wr = count;
+ if (IACptr)
+ wr = IACptr - buf;
+ rc = safe_write(fd, buf, wr);
+ if (rc != wr)
+ break;
+ buf += rc;
+ total += rc;
+ count -= rc;
+ }
+ /* here: rc - result of last short write */
+ if ((ssize_t)rc < 0) { /* error? */
+ if (total == 0)
+ return rc;
+ rc = 0;
+ }
+ return total + rc;
+}
+
+/* Must match getopt32 string */
+enum {
+ OPT_WATCHCHILD = (1 << 2), /* -K */
+ OPT_INETD = (1 << 3) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -i */
+ OPT_PORT = (1 << 4) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -p PORT */
+ OPT_FOREGROUND = (1 << 6) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -F */
+ OPT_SYSLOG = (1 << 7) * ENABLE_FEATURE_TELNETD_INETD_WAIT, /* -S */
+ OPT_WAIT = (1 << 8) * ENABLE_FEATURE_TELNETD_INETD_WAIT, /* -w SEC */
+};
+
+static struct tsession *
+make_new_session(
+ IF_FEATURE_TELNETD_STANDALONE(int sock)
+ IF_NOT_FEATURE_TELNETD_STANDALONE(void)
+) {
+#if !ENABLE_FEATURE_TELNETD_STANDALONE
+ enum { sock = 0 };
+#endif
+ const char *login_argv[2];
+ struct termios termbuf;
+ int fd, pid;
+ char tty_name[GETPTY_BUFSIZE];
+ struct tsession *ts = xzalloc(sizeof(struct tsession) + BUFSIZE * 2);
+
+ /*ts->buf1 = (char *)(ts + 1);*/
+ /*ts->buf2 = ts->buf1 + BUFSIZE;*/
+
+ /* Got a new connection, set up a tty */
+ fd = xgetpty(tty_name);
+ if (fd > G.maxfd)
+ G.maxfd = fd;
+ ts->ptyfd = fd;
+ ndelay_on(fd);
+ close_on_exec_on(fd);
+
+ /* SO_KEEPALIVE by popular demand */
+ setsockopt_keepalive(sock);
+#if ENABLE_FEATURE_TELNETD_STANDALONE
+ ts->sockfd_read = sock;
+ ndelay_on(sock);
+ if (sock == 0) { /* We are called with fd 0 - we are in inetd mode */
+ sock++; /* so use fd 1 for output */
+ ndelay_on(sock);
+ }
+ ts->sockfd_write = sock;
+ if (sock > G.maxfd)
+ G.maxfd = sock;
+#else
+ /* ts->sockfd_read = 0; - done by xzalloc */
+ ts->sockfd_write = 1;
+ ndelay_on(0);
+ ndelay_on(1);
+#endif
+
+ /* Make the telnet client understand we will echo characters so it
+ * should not do it locally. We don't tell the client to run linemode,
+ * because we want to handle line editing and tab completion and other
+ * stuff that requires char-by-char support. */
+ {
+ static const char iacs_to_send[] ALIGN1 = {
+ IAC, DO, TELOPT_ECHO,
+ IAC, DO, TELOPT_NAWS,
+ /* This requires telnetd.ctrlSQ.patch (incomplete) */
+ /*IAC, DO, TELOPT_LFLOW,*/
+ IAC, WILL, TELOPT_ECHO,
+ IAC, WILL, TELOPT_SGA
+ };
+ /* This confuses safe_write_double_iac(), it will try to duplicate
+ * each IAC... */
+ //memcpy(TS_BUF2(ts), iacs_to_send, sizeof(iacs_to_send));
+ //ts->rdidx2 = sizeof(iacs_to_send);
+ //ts->size2 = sizeof(iacs_to_send);
+ /* So just stuff it into TCP stream! (no error check...) */
+#if ENABLE_FEATURE_TELNETD_STANDALONE
+ safe_write(sock, iacs_to_send, sizeof(iacs_to_send));
+#else
+ safe_write(1, iacs_to_send, sizeof(iacs_to_send));
+#endif
+ /*ts->rdidx2 = 0; - xzalloc did it */
+ /*ts->size2 = 0;*/
+ }
+
+ fflush_all();
+ pid = vfork(); /* NOMMU-friendly */
+ if (pid < 0) {
+ free(ts);
+ close(fd);
+ /* sock will be closed by caller */
+ bb_simple_perror_msg("vfork");
+ return NULL;
+ }
+ if (pid > 0) {
+ /* Parent */
+ ts->shell_pid = pid;
+ return ts;
+ }
+
+ /* Child */
+ /* Careful - we are after vfork! */
+
+ /* Restore default signal handling ASAP */
+ bb_signals((1 << SIGCHLD) + (1 << SIGPIPE), SIG_DFL);
+
+ pid = getpid();
+
+ if (ENABLE_FEATURE_UTMP) {
+ len_and_sockaddr *lsa = get_peer_lsa(sock);
+ char *hostname = NULL;
+ if (lsa) {
+ hostname = xmalloc_sockaddr2dotted(&lsa->u.sa);
+ free(lsa);
+ }
+ write_new_utmp(pid, LOGIN_PROCESS, tty_name, /*username:*/ "LOGIN", hostname);
+ free(hostname);
+ }
+
+ /* Make new session and process group */
+ setsid();
+
+ /* Open the child's side of the tty */
+ /* NB: setsid() disconnects from any previous ctty's. Therefore
+ * we must open child's side of the tty AFTER setsid! */
+ close(0);
+ xopen(tty_name, O_RDWR); /* becomes our ctty */
+ xdup2(0, 1);
+ xdup2(0, 2);
+ tcsetpgrp(0, pid); /* switch this tty's process group to us */
+
+ /* The pseudo-terminal allocated to the client is configured to operate
+ * in cooked mode, and with XTABS CRMOD enabled (see tty(4)) */
+ tcgetattr(0, &termbuf);
+ termbuf.c_lflag |= ECHO; /* if we use readline we dont want this */
+ termbuf.c_oflag |= ONLCR | XTABS;
+ termbuf.c_iflag |= ICRNL;
+ termbuf.c_iflag &= ~IXOFF;
+ /*termbuf.c_lflag &= ~ICANON;*/
+ tcsetattr_stdin_TCSANOW(&termbuf);
+
+ /* Uses FILE-based I/O to stdout, but does fflush_all(),
+ * so should be safe with vfork.
+ * I fear, though, that some users will have ridiculously big
+ * issue files, and they may block writing to fd 1,
+ * (parent is supposed to read it, but parent waits
+ * for vforked child to exec!) */
+ print_login_issue(G.issuefile, tty_name);
+
+ /* Exec shell / login / whatever */
+ login_argv[0] = G.loginpath;
+ login_argv[1] = NULL;
+ /* exec busybox applet (if PREFER_APPLETS=y), if that fails,
+ * exec external program.
+ * NB: sock is either 0 or has CLOEXEC set on it.
+ * fd has CLOEXEC set on it too. These two fds will be closed here.
+ */
+ BB_EXECVP(G.loginpath, (char **)login_argv);
+ /* _exit is safer with vfork, and we shouldn't send message
+ * to remote clients anyway */
+ _exit_FAILURE(); /*bb_perror_msg_and_die("execv %s", G.loginpath);*/
+}
+
+#if ENABLE_FEATURE_TELNETD_STANDALONE
+
+static void
+free_session(struct tsession *ts)
+{
+ struct tsession *t;
+
+ if (option_mask32 & OPT_INETD)
+ exit_SUCCESS();
+
+ /* Unlink this telnet session from the session list */
+ t = G.sessions;
+ if (t == ts)
+ G.sessions = ts->next;
+ else {
+ while (t->next != ts)
+ t = t->next;
+ t->next = ts->next;
+ }
+
+#if 0
+ /* It was said that "normal" telnetd just closes ptyfd,
+ * doesn't send SIGKILL. When we close ptyfd,
+ * kernel sends SIGHUP to processes having slave side opened. */
+ kill(ts->shell_pid, SIGKILL);
+ waitpid(ts->shell_pid, NULL, 0);
+#endif
+ close(ts->ptyfd);
+ close(ts->sockfd_read);
+ /* We do not need to close(ts->sockfd_write), it's the same
+ * as sockfd_read unless we are in inetd mode. But in inetd mode
+ * we do not reach this */
+ free(ts);
+
+ /* Scan all sessions and find new maxfd */
+ G.maxfd = 0;
+ ts = G.sessions;
+ while (ts) {
+ if (G.maxfd < ts->ptyfd)
+ G.maxfd = ts->ptyfd;
+ if (G.maxfd < ts->sockfd_read)
+ G.maxfd = ts->sockfd_read;
+#if 0
+ /* Again, sockfd_write == sockfd_read here */
+ if (G.maxfd < ts->sockfd_write)
+ G.maxfd = ts->sockfd_write;
+#endif
+ ts = ts->next;
+ }
+}
+
+#else /* !FEATURE_TELNETD_STANDALONE */
+
+/* Used in main() only, thus "return 0" actually is exit(EXIT_SUCCESS). */
+#define free_session(ts) return 0
+
+#endif
+
+static void handle_sigchld(int sig UNUSED_PARAM)
+{
+ pid_t pid;
+ struct tsession *ts;
+ int save_errno = errno;
+
+ /* Looping: more than one child may have exited */
+ while (1) {
+ pid = wait_any_nohang(NULL);
+ if (pid <= 0)
+ break;
+ ts = G.sessions;
+ while (ts) {
+ if (ts->shell_pid == pid) {
+ ts->shell_pid = -1;
+ update_utmp_DEAD_PROCESS(pid);
+ break;
+ }
+ ts = ts->next;
+ }
+ }
+
+ errno = save_errno;
+}
+
+int telnetd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
+int telnetd_main(int argc UNUSED_PARAM, char **argv)
+{
+ fd_set rdfdset, wrfdset;
+ unsigned opt;
+ int count;
+ struct tsession *ts;
+#if ENABLE_FEATURE_TELNETD_STANDALONE
+#define IS_INETD (opt & OPT_INETD)
+ int master_fd = master_fd; /* for compiler */
+ int sec_linger = sec_linger;
+ char *opt_bindaddr = NULL;
+ char *opt_portnbr;
+#else
+ enum {
+ IS_INETD = 1,
+ master_fd = -1,
+ };
+#endif
+ INIT_G();
+
+ /* Even if !STANDALONE, we accept (and ignore) -i, thus people
+ * don't need to guess whether it's ok to pass -i to us */
+ opt = getopt32(argv, "^"
+ "f:l:Ki"
+ IF_FEATURE_TELNETD_STANDALONE("p:b:F")
+ IF_FEATURE_TELNETD_INETD_WAIT("Sw:+") /* -w NUM */
+ "\0"
+ /* -w implies -F. -w and -i don't mix */
+ IF_FEATURE_TELNETD_INETD_WAIT("wF:i--w:w--i"),
+ &G.issuefile, &G.loginpath
+ IF_FEATURE_TELNETD_STANDALONE(, &opt_portnbr, &opt_bindaddr)
+ IF_FEATURE_TELNETD_INETD_WAIT(, &sec_linger)
+ );
+ if (!IS_INETD /*&& !re_execed*/) {
+ /* inform that we start in standalone mode?
+ * May be useful when people forget to give -i */
+ /*bb_error_msg("listening for connections");*/
+ if (!(opt & OPT_FOREGROUND)) {
+ /* DAEMON_CHDIR_ROOT was giving inconsistent
+ * behavior with/without -F, -i */
+ bb_daemonize_or_rexec(0 /*was DAEMON_CHDIR_ROOT*/, argv);
+ }
+ }
+ /* Redirect log to syslog early, if needed */
+ if (IS_INETD || (opt & OPT_SYSLOG) || !(opt & OPT_FOREGROUND)) {
+ openlog(applet_name, LOG_PID, LOG_DAEMON);
+ logmode = LOGMODE_SYSLOG;
+ }
+#if ENABLE_FEATURE_TELNETD_STANDALONE
+ if (IS_INETD) {
+ G.sessions = make_new_session(0);
+ if (!G.sessions) /* pty opening or vfork problem, exit */
+ return 1; /* make_new_session printed error message */
+ } else {
+ master_fd = 0;
+ if (!(opt & OPT_WAIT)) {
+ unsigned portnbr = CONFIG_FEATURE_TELNETD_PORT_DEFAULT;
+ if (opt & OPT_PORT)
+ portnbr = xatou16(opt_portnbr);
+ master_fd = create_and_bind_stream_or_die(opt_bindaddr, portnbr);
+ xlisten(master_fd, 1);
+ }
+ close_on_exec_on(master_fd);
+ }
+#else
+ G.sessions = make_new_session();
+ if (!G.sessions) /* pty opening or vfork problem, exit */
+ return 1; /* make_new_session printed error message */
+#endif
+
+ /* We don't want to die if just one session is broken */
+ signal(SIGPIPE, SIG_IGN);
+
+ if (opt & OPT_WATCHCHILD)
+ signal(SIGCHLD, handle_sigchld);
+ else /* prevent dead children from becoming zombies */
+ signal(SIGCHLD, SIG_IGN);
+
+/*
+ This is how the buffers are used. The arrows indicate data flow.
+
+ +-------+ wridx1++ +------+ rdidx1++ +----------+
+ | | <-------------- | buf1 | <-------------- | |
+ | | size1-- +------+ size1++ | |
+ | pty | | socket |
+ | | rdidx2++ +------+ wridx2++ | |
+ | | --------------> | buf2 | --------------> | |
+ +-------+ size2++ +------+ size2-- +----------+
+
+ size1: "how many bytes are buffered for pty between rdidx1 and wridx1?"
+ size2: "how many bytes are buffered for socket between rdidx2 and wridx2?"
+
+ Each session has got two buffers. Buffers are circular. If sizeN == 0,
+ buffer is empty. If sizeN == BUFSIZE, buffer is full. In both these cases
+ rdidxN == wridxN.
+*/
+ again:
+ FD_ZERO(&rdfdset);
+ FD_ZERO(&wrfdset);
+
+ /* Select on the master socket, all telnet sockets and their
+ * ptys if there is room in their session buffers.
+ * NB: scalability problem: we recalculate entire bitmap
+ * before each select. Can be a problem with 500+ connections. */
+ ts = G.sessions;
+ while (ts) {
+ struct tsession *next = ts->next; /* in case we free ts */
+ if (ts->shell_pid == -1) {
+ /* Child died and we detected that */
+ free_session(ts);
+ } else {
+ if (ts->size1 > 0) /* can write to pty */
+ FD_SET(ts->ptyfd, &wrfdset);
+ if (ts->size1 < BUFSIZE) /* can read from socket */
+ FD_SET(ts->sockfd_read, &rdfdset);
+ if (ts->size2 > 0) /* can write to socket */
+ FD_SET(ts->sockfd_write, &wrfdset);
+ if (ts->size2 < BUFSIZE) /* can read from pty */
+ FD_SET(ts->ptyfd, &rdfdset);
+ }
+ ts = next;
+ }
+ if (!IS_INETD) {
+ FD_SET(master_fd, &rdfdset);
+ /* This is needed because free_session() does not
+ * take master_fd into account when it finds new
+ * maxfd among remaining fd's */
+ if (master_fd > G.maxfd)
+ G.maxfd = master_fd;
+ }
+
+ {
+ struct timeval *tv_ptr = NULL;
+#if ENABLE_FEATURE_TELNETD_INETD_WAIT
+ struct timeval tv;
+ if ((opt & OPT_WAIT) && !G.sessions) {
+ tv.tv_sec = sec_linger;
+ tv.tv_usec = 0;
+ tv_ptr = &tv;
+ }
+#endif
+ count = select(G.maxfd + 1, &rdfdset, &wrfdset, NULL, tv_ptr);
+ }
+ if (count == 0) /* "telnetd -w SEC" timed out */
+ return 0;
+ if (count < 0)
+ goto again; /* EINTR or ENOMEM */
+
+#if ENABLE_FEATURE_TELNETD_STANDALONE
+ /* Check for and accept new sessions */
+ if (!IS_INETD && FD_ISSET(master_fd, &rdfdset)) {
+ int fd;
+ struct tsession *new_ts;
+
+ fd = accept(master_fd, NULL, NULL);
+ if (fd < 0)
+ goto again;
+ close_on_exec_on(fd);
+
+ /* Create a new session and link it into active list */
+ new_ts = make_new_session(fd);
+ if (new_ts) {
+ new_ts->next = G.sessions;
+ G.sessions = new_ts;
+ } else {
+ close(fd);
+ }
+ }
+#endif
+
+ /* Then check for data tunneling */
+ ts = G.sessions;
+ while (ts) { /* For all sessions... */
+ struct tsession *next = ts->next; /* in case we free ts */
+
+ if (/*ts->size1 &&*/ FD_ISSET(ts->ptyfd, &wrfdset)) {
+ /* Write to pty from buffer 1 */
+ count = safe_write_to_pty_decode_iac(ts);
+ if (count < 0) {
+ if (errno == EAGAIN)
+ goto skip1;
+ goto kill_session;
+ }
+ }
+ skip1:
+ if (/*ts->size2 &&*/ FD_ISSET(ts->sockfd_write, &wrfdset)) {
+ /* Write to socket from buffer 2 */
+ count = MIN(BUFSIZE - ts->wridx2, ts->size2);
+ count = safe_write_double_iac(ts->sockfd_write, (void*)(TS_BUF2(ts) + ts->wridx2), count);
+ if (count < 0) {
+ if (errno == EAGAIN)
+ goto skip2;
+ goto kill_session;
+ }
+ ts->wridx2 += count;
+ if (ts->wridx2 >= BUFSIZE) /* actually == BUFSIZE */
+ ts->wridx2 = 0;
+ ts->size2 -= count;
+ if (ts->size2 == 0) {
+ ts->rdidx2 = 0;
+ ts->wridx2 = 0;
+ }
+ }
+ skip2:
+
+ if (/*ts->size1 < BUFSIZE &&*/ FD_ISSET(ts->sockfd_read, &rdfdset)) {
+ /* Read from socket to buffer 1 */
+ count = MIN(BUFSIZE - ts->rdidx1, BUFSIZE - ts->size1);
+ count = safe_read(ts->sockfd_read, TS_BUF1(ts) + ts->rdidx1, count);
+ if (count <= 0) {
+ if (count < 0 && errno == EAGAIN)
+ goto skip3;
+ goto kill_session;
+ }
+ /* Ignore trailing NUL if it is there */
+ if (!TS_BUF1(ts)[ts->rdidx1 + count - 1]) {
+ --count;
+ }
+ ts->size1 += count;
+ ts->rdidx1 += count;
+ if (ts->rdidx1 >= BUFSIZE) /* actually == BUFSIZE */
+ ts->rdidx1 = 0;
+ }
+ skip3:
+ if (/*ts->size2 < BUFSIZE &&*/ FD_ISSET(ts->ptyfd, &rdfdset)) {
+ /* Read from pty to buffer 2 */
+ int eio = 0;
+ read_pty:
+ count = MIN(BUFSIZE - ts->rdidx2, BUFSIZE - ts->size2);
+ count = safe_read(ts->ptyfd, TS_BUF2(ts) + ts->rdidx2, count);
+ if (count <= 0) {
+ if (count < 0) {
+ if (errno == EAGAIN)
+ goto skip4;
+ /* login process might call vhangup(),
+ * which causes intermittent EIOs on read above
+ * (observed on kernel 4.12.0). Try up to 10 ms.
+ */
+ if (errno == EIO && eio < 10) {
+ eio++;
+ //bb_error_msg("EIO pty %u", eio);
+ usleep(1000);
+ goto read_pty;
+ }
+ }
+ goto kill_session;
+ }
+ ts->size2 += count;
+ ts->rdidx2 += count;
+ if (ts->rdidx2 >= BUFSIZE) /* actually == BUFSIZE */
+ ts->rdidx2 = 0;
+ }
+ skip4:
+ ts = next;
+ continue;
+ kill_session:
+ if (ts->shell_pid > 0)
+ update_utmp_DEAD_PROCESS(ts->shell_pid);
+ free_session(ts);
+ ts = next;
+ }
+
+ goto again;
+}