AmendHub

jcs

/

subtext

/

amendments

/

150

telnet: Make listening more resilient, add IP banning

MacTCP doesn't make it easy to detect when a listening socket has a
valid connection, or when it doesn't. Sometimes the connection is
accepted but then closed, sometimes TCPStatus returns a
connectionDoesntExist error, sometimes an open connection lingers in
TCP Wait. All of these were contributing to us no longer opening a
listening socket after some period of time accepting and closing
connections.
When bots try to login with a banned username, add their IP to a
ring buffer of IPs that we won't service. Unfortunately there is no
way to actually block these connections before they are accepted, so
we have to accept them, check the IP, and then close them right away.

jcs made amendment 150 about 1 month ago
--- session.c Thu Jun 16 09:41:33 2022 +++ session.c Thu Jun 16 11:01:19 2022 @@ -276,13 +276,14 @@ session_close(struct session *session) bool found; if (!session->ending) { - /* give 1 second to flush output */ - now = Ticks; - while (session->obuflen && (Ticks - now) < 60) { - session->node_funcs->output(session); - uthread_yield(); + if (!session->ban_node_source) { + /* give 1 second to flush output */ + now = Ticks; + while (session->obuflen && (Ticks - now) < 60) { + session->node_funcs->output(session); + uthread_yield(); + } } - session->ending = 1; } @@ -774,6 +775,7 @@ session_login(struct session *s) if (!user && user_username_is_banned(username)) { session_log(s, "Attempted login as banned username %s", username); + s->ban_node_source = 1; goto login_bail; } } @@ -843,8 +845,10 @@ login_bail: free(username); if (password != NULL) free(password); - session_printf(s, "Thanks for playing\r\n"); - session_flush(s); + if (!s->ban_node_source) { + session_printf(s, "Thanks for playing\r\n"); + session_flush(s); + } return AUTH_USER_FAILED; } --- session.h Sun Jun 12 22:18:30 2022 +++ session.h Thu Jun 16 11:00:57 2022 @@ -77,6 +77,7 @@ struct session { enum session_input_state input_state; unsigned short last_input; unsigned char abort_input; + unsigned char ban_node_source; unsigned long established_at; unsigned long last_input_at; unsigned short terminal_columns; --- telnet.c Wed Jun 15 13:48:57 2022 +++ telnet.c Thu Jun 16 12:54:06 2022 @@ -88,7 +88,9 @@ enum { struct telnet_node { short id; + char name[8]; short state; + ip_addr ip; char ip_s[20]; TCPiopb rcv_pb, send_pb, listen_pb; StreamPtr stream; @@ -106,15 +108,18 @@ struct telnet_node { unsigned char tcp_buf[(4 * 1500) + 1024]; }; -#define MAX_TELNET_NODES 5 -struct telnet_node *telnet_nodes[MAX_TELNET_NODES] = { NULL }; -struct telnet_node *telnet_listener_node = NULL; -short telnet_local_port; -TCPiopb telnet_exit_pb; -TCPStatusPB telnet_status_pb; -bool did_initial_log = false; +#define MAX_TELNET_NODES 5 +static struct telnet_node *telnet_nodes[MAX_TELNET_NODES] = { NULL }; +static struct telnet_node *telnet_listener_node = NULL; +static TCPiopb telnet_exit_pb; +static TCPStatusPB telnet_status_pb; +static bool did_initial_log = false; +#define MAX_BANNED_IPS 25 +static ip_addr banned_ips[MAX_BANNED_IPS] = { 0 }; + void telnet_setup(struct session *session); +void telnet_create_listener_node(short node_idx); void telnet_output_iac(struct session *session, char *iacs, size_t len); struct node_funcs telnet_node_funcs = { @@ -147,12 +152,8 @@ telnet_atexit(void) if (!node) continue; - if (node->state > TELNET_PB_STATE_UNUSED) { - error = _TCPAbort(&telnet_exit_pb, node->stream, nil, nil, - false); - error = _TCPRelease(&telnet_exit_pb, node->stream, nil, - nil, false); - } + if (node->state > TELNET_PB_STATE_UNUSED) + _TCPRelease(&telnet_exit_pb, node->stream, nil, nil, false); free(node); telnet_nodes[n] = NULL; @@ -162,84 +163,135 @@ telnet_atexit(void) } void -telnet_idle(void) +telnet_create_listener_node(short node_idx) { char ip_s[20]; struct telnet_node *node; - short n, j, error; + short error; ip_addr ip; tcp_port port; long mask; + + node = xmalloczero(sizeof(struct telnet_node)); + node->id = node_idx; + snprintf(node->name, sizeof(node->name), "ttyt%d", node_idx); + + error = _TCPCreate(&node->listen_pb, &node->stream, + (Ptr)&node->tcp_buf, sizeof(node->tcp_buf), nil, nil, nil, false); + if (error) + panic("TCPCreate[%d] failed: %d", node->id, error); + error = _TCPGetOurIP(&ip, &mask); + if (error) + panic("TCPGetOurIP failed: %d", error); + + port = db->config.telnet_port; + + if (!did_initial_log) { + long2ip(ip, ip_s); + logger_printf(logger, "[telnet] Listening on %s:%d", ip_s, port); + did_initial_log = true; + } + + error = _TCPPassiveOpen(&node->listen_pb, node->stream, nil, nil, &ip, + &port, nil, nil, true); + if (error) + panic("TCPPassiveOpen[%d] on port %d failed: %d", + node->id, db->config.telnet_port, error); + + node->state = TELNET_PB_STATE_LISTENING; + telnet_nodes[node->id] = node; + telnet_listener_node = node; +} + +void +telnet_idle(void) +{ + struct telnet_node *node; + short n, j, error; + if (!db->config.telnet_port) return; for (n = 0; n < MAX_TELNET_NODES; n++) { node = telnet_nodes[n]; + if (!node) { if (telnet_listener_node != NULL) continue; - - node = xmalloczero(sizeof(struct telnet_node)); - node->id = n; - - error = _TCPCreate(&node->send_pb, &node->stream, - (Ptr)&node->tcp_buf, sizeof(node->tcp_buf), nil, nil, nil, - false); - if (error) - panic("TCPCreate[%d] failed: %d", node->id, error); - - error = _TCPGetOurIP(&ip, &mask); - if (error) - panic("TCPGetOurIP failed: %d", error); - - port = db->config.telnet_port; - - if (!did_initial_log) { - long2ip(ip, ip_s); - logger_printf(logger, "[telnet] Listening on %s:%d", ip_s, - port); - did_initial_log = true; - } - - error = _TCPPassiveOpen(&node->listen_pb, node->stream, nil, - nil, &ip, &port, nil, nil, true); - if (error) - panic("TCPPassiveOpen[%d] on port %d failed: %d", - node->id, db->config.telnet_port, error); - - node->state = TELNET_PB_STATE_LISTENING; - telnet_nodes[node->id] = node; - telnet_listener_node = node; + telnet_create_listener_node(n); + continue; } - + switch (node->state) { case TELNET_PB_STATE_LISTENING: error = _TCPStatus(&node->send_pb, node->stream, &telnet_status_pb, nil, nil, false); - if (error == connectionDoesntExist) - continue; - if (error != noErr) - panic("TCPStatus[%d] %d", n, error); - if (telnet_status_pb.connectionState == - ConnectionStateEstablished) { - char tty[7]; - - sprintf(tty, "ttyt%d", n); + if (error == connectionDoesntExist) { + /* + * The connection closed before it could be handled, but + * telnet_status_pb.connectionState will still be + * listening, so force it into a bogus state to recycle it. + */ + telnet_status_pb.connectionState = -1; + } + switch (telnet_status_pb.connectionState) { + case ConnectionStateListening: + goto next_node; + case ConnectionStateSYNReceived: + case ConnectionStateSYNSent: + /* TODO: enforce our own timeout on these? */ + break; + case ConnectionStateEstablished: + /* start up a new socket for listening */ + telnet_listener_node = NULL; + + node->ip = telnet_status_pb.remoteHost; long2ip(telnet_status_pb.remoteHost, node->ip_s); - logger_printf(logger, "[telnet] [%s] New connection from %s", - tty, node->ip_s); + + for (j = 0; j < nitems(banned_ips); j++) { + if (banned_ips[j] != telnet_status_pb.remoteHost) + continue; + + logger_printf(logger, "[%s] Refusing telnet " + "connection from banned IP %s", node->name, + node->ip_s); + _TCPRelease(&node->listen_pb, node->stream, nil, nil, + false); + + telnet_nodes[n] = NULL; + free(node); + goto next_node; + } + + logger_printf(logger, "[%s] New telnet connection " + "from %s", node->name, node->ip_s); node->state = TELNET_PB_STATE_CONNECTED; - node->session = session_create(tty, "telnet", + node->session = session_create(node->name, "telnet", &telnet_node_funcs); node->session->cookie = (void *)node; node->session->tspeed = 19200; node->session->log.ip_address = telnet_status_pb.remoteHost; - - /* start up a new socket for listening */ - telnet_listener_node = NULL; + break; + default: + if (telnet_status_pb.remoteHost == 0) + long2ip(node->listen_pb.csParam.open.remoteHost, + node->ip_s); + else + long2ip(telnet_status_pb.remoteHost, node->ip_s); + + logger_printf(logger, "[%s] Telnet connection from " + "%s in state %d, aborting", node->name, node->ip_s, + telnet_status_pb.connectionState); + + _TCPRelease(&node->listen_pb, node->stream, nil, nil, + false); + + telnet_nodes[n] = telnet_listener_node = NULL; + free(node); + goto next_node; } break; case TELNET_PB_STATE_CONNECTED: @@ -252,6 +304,9 @@ telnet_idle(void) telnet_output(node->session); break; } + +next_node: + continue; } } @@ -298,7 +353,8 @@ telnet_input(struct session *session) error = _TCPNoCopyRcv(&node->rcv_pb, node->stream, (Ptr)&node->tcp_rds[0], 1, nil, nil, false); if (error) { - warn("TCPNoCopyRcv[%d] failed: %d", node->id, error); + session_log(session, "TCP read failed (%d), closing connection", + error); session->ending = 1; return 0; } @@ -392,9 +448,8 @@ telnet_input(struct session *session) if (node->iac_sb[sboff] == 255) sboff++; session->terminal_lines |= node->iac_sb[sboff++]; - logger_printf(logger, "[telnet] [%s] IAC NAWS %d, %d", - session->node, session->terminal_columns, - session->terminal_lines); + session_log(session, "IAC NAWS %d, %d", + session->terminal_columns, session->terminal_lines); break; case IAC_TTYPE: /* IAC SB TTYPE IS XTERM IAC SE */ @@ -405,8 +460,8 @@ telnet_input(struct session *session) /* TODO: compare terminal type to list */ session->vt100 = 1; - logger_printf(logger, "[telnet] [%s] IAC TTYPE IS %s", - session->node, session->terminal_type); + session_log(session, "IAC TTYPE IS %s", + session->terminal_type); break; } node->iac_state = TELNET_IAC_STATE_IDLE; @@ -497,7 +552,8 @@ telnet_input(struct session *session) error = _TCPBfrReturn(&node->rcv_pb, node->stream, (Ptr)&node->tcp_rds[0], nil, nil, false); if (error) { - warn("TCPNoCopyRcv[%d] failed: %d", node->id, error); + session_log(session, "TCP read return failed (%d), closing " + "connection", error); session->ending = 1; return 0; } @@ -592,15 +648,28 @@ void telnet_close(struct session *session) { struct telnet_node *node = (struct telnet_node *)session->cookie; - short error; + short error, n; - logger_printf(logger, "[%s] Closing telnet connection from %s", - session->node, node->ip_s); + session_log(session, "%s telnet connection from %s", + (session->ban_node_source ? "Banning" : "Closing"), node->ip_s); + + if (session->ban_node_source) { + for (n = 0; n <= nitems(banned_ips); n++) { + if (n == nitems(banned_ips)) { + memmove(banned_ips + sizeof(banned_ips[0]), banned_ips, + sizeof(banned_ips) - sizeof(banned_ips[0])); + banned_ips[0] = node->ip; + } else if (banned_ips[n] == 0) { + banned_ips[n] = node->ip; + break; + } + } + } else { + error = _TCPClose(&telnet_exit_pb, node->stream, nil, nil, false); + /* TODO: allow some time to fully close? */ + } - error = _TCPClose(&telnet_exit_pb, node->stream, nil, nil, false); - /* TODO: allow some time to fully close? */ error = _TCPRelease(&telnet_exit_pb, node->stream, nil, nil, false); - if (error) { warn("error shutting down telnet session: %d", error); return; @@ -608,9 +677,5 @@ telnet_close(struct session *session) session->cookie = NULL; telnet_nodes[node->id] = NULL; - - if (node == telnet_listener_node) - telnet_listener_node = NULL; - free(node); }