| @@ -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; |
| } |