diff --git a/include/bootstrap.h b/include/bootstrap.h new file mode 100644 index 0000000..a64221e --- /dev/null +++ b/include/bootstrap.h @@ -0,0 +1,39 @@ +#ifndef BOOTSTRAP_H +#define BOOTSTRAP_H + +#include "ssh_server.h" /* for client_t and the libssh / arpa includes */ + +/* Hand-off envelope between the accept loop and the bootstrap thread. + * The accept loop allocates one of these per accepted session, fills it, + * and pthread_create()s a detached bootstrap_run() with this pointer. + * bootstrap_run() owns the struct and the embedded ssh_session, and frees + * both before returning. */ +typedef struct { + ssh_session session; + char client_ip[INET6_ADDRSTRLEN]; +} accepted_session_t; + +/* Read TNT_ACCESS_TOKEN from the environment. Idempotent. Call once + * during startup, before bootstrap_run() can fire on any accepted + * session. */ +void bootstrap_init(void); + +/* Read the peer IP off an accepted ssh_session into ip_buf. Sets ip_buf + * to "unknown" when the address family is unrecognised or getpeername() + * fails. ip_buf must be at least INET6_ADDRSTRLEN bytes. */ +void bootstrap_peer_ip(ssh_session session, char *ip_buf, size_t buf_size); + +/* pthread entry point for the per-connection bootstrap thread. + * + * Steps performed before handing control to client_handle_session(): + * 1. SSH key exchange + * 2. auth (password / none / pubkey, with rate-limit feedback) + * 3. channel open + PTY/shell-or-exec request + * 4. construct a client_t and install its lifetime channel callbacks + * + * On any failure path the connection is torn down and ratelimit / + * connection counters are released; client_handle_session() is never + * invoked. Always returns NULL. */ +void *bootstrap_run(void *arg); + +#endif /* BOOTSTRAP_H */ diff --git a/include/common.h b/include/common.h index 7d5b6b7..becd137 100644 --- a/include/common.h +++ b/include/common.h @@ -72,4 +72,9 @@ int env_int(const char *name, int fallback, int min_val, int max_val); * author), and the :nick command. */ bool is_valid_username(const char *username); +/* Clamp a terminal size to sensible bounds (1..500 cols, 1..200 rows). + * Replaces zero/negative/oversize values with 80x24. Used by the PTY + * request callback and the window-change callback. */ +void sanitize_terminal_size(int *width, int *height); + #endif /* COMMON_H */ diff --git a/include/ssh_server.h b/include/ssh_server.h index e658f5c..2aa0db9 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -58,6 +58,14 @@ int client_printf(client_t *client, const char *fmt, ...); void client_addref(client_t *client); void client_release(client_t *client); +/* Install the post-bootstrap channel callbacks (window-change, eof, close) + * that target this client_t. Caller MUST have already added one + * client_addref() to keep the client alive across in-flight callback + * invocations; the matching client_release() happens during cleanup in + * client_handle_session(). Returns 0 on success, -1 on failure (in which + * case the caller still owns both refs and must release them). */ +int client_install_channel_callbacks(client_t *client); + /* Bell-notify any clients whose @username appears in the broadcast content, * skipping the sender. Defined in ssh_server.c (will move to a dedicated * client.c during PR2-M6). */ diff --git a/src/bootstrap.c b/src/bootstrap.c new file mode 100644 index 0000000..2754c89 --- /dev/null +++ b/src/bootstrap.c @@ -0,0 +1,492 @@ +#include "bootstrap.h" +#include "common.h" +#include "ratelimit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Per-connection bootstrap state. Kept private to this translation unit: + * its lifetime ends inside bootstrap_run() once a client_t takes over. */ +typedef struct { + char client_ip[INET6_ADDRSTRLEN]; + char requested_user[MAX_USERNAME_LEN]; + int pty_width; + int pty_height; + char exec_command[MAX_EXEC_COMMAND_LEN]; + bool auth_success; + int auth_attempts; + bool channel_ready; /* Set when shell/exec request received */ + ssh_channel channel; /* Channel created in callback */ + struct ssh_channel_callbacks_struct *channel_cb; /* Channel callbacks */ +} session_context_t; + +/* Configured access token; empty string means "no auth required". */ +static char g_access_token[256] = ""; + +void bootstrap_init(void) { + const char *token_env = getenv("TNT_ACCESS_TOKEN"); + if (token_env != NULL) { + strncpy(g_access_token, token_env, sizeof(g_access_token) - 1); + g_access_token[sizeof(g_access_token) - 1] = '\0'; + } else { + g_access_token[0] = '\0'; + } +} + +void bootstrap_peer_ip(ssh_session session, char *ip_buf, size_t buf_size) { + int fd = ssh_get_fd(session); + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + + if (getpeername(fd, (struct sockaddr *)&addr, &addr_len) == 0) { + if (addr.ss_family == AF_INET) { + struct sockaddr_in *s = (struct sockaddr_in *)&addr; + inet_ntop(AF_INET, &s->sin_addr, ip_buf, buf_size); + } else if (addr.ss_family == AF_INET6) { + struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr; + inet_ntop(AF_INET6, &s->sin6_addr, ip_buf, buf_size); + } else { + strncpy(ip_buf, "unknown", buf_size - 1); + } + } else { + strncpy(ip_buf, "unknown", buf_size - 1); + } + ip_buf[buf_size - 1] = '\0'; +} + +/* Constant-time string comparison to prevent timing side-channel attacks. + * Always iterates over the full length of the secret (b) to avoid leaking + * its length. When the input (a) is shorter, compares against zero bytes; + * the length mismatch is folded into the result separately. + * + * Note: the length-diff is accumulated in size_t to avoid the bug where a + * narrower type (e.g. unsigned char) would collapse pairs like (300, 44) to + * 0 because 300 ^ 44 == 256 ^ (44 ^ 44) == 256 which truncates to 0. */ +static bool constant_time_strcmp(const char *a, const char *b) { + size_t len_a = strlen(a); + size_t len_b = strlen(b); + volatile size_t length_diff = len_a ^ len_b; + volatile unsigned char byte_diff = 0; + for (size_t i = 0; i < len_b; i++) { + unsigned char ca = (i < len_a) ? (unsigned char)a[i] : 0; + byte_diff |= ca ^ (unsigned char)b[i]; + } + return length_diff == 0 && byte_diff == 0; +} + +/* Password authentication callback */ +static int auth_password(ssh_session session, const char *user, + const char *password, void *userdata) { + session_context_t *ctx = (session_context_t *)userdata; + + if (user && user[0] != '\0') { + strncpy(ctx->requested_user, user, sizeof(ctx->requested_user) - 1); + ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0'; + } + + ctx->auth_attempts++; + + /* Limit auth attempts */ + if (ctx->auth_attempts > 3) { + ratelimit_record_auth_failure(ctx->client_ip); + fprintf(stderr, "Too many auth attempts from %s\n", ctx->client_ip); + ssh_disconnect(session); + return SSH_AUTH_DENIED; + } + + /* If access token is configured, require it */ + if (g_access_token[0] != '\0') { + if (password && constant_time_strcmp(password, g_access_token)) { + /* Token matches */ + ctx->auth_success = true; + return SSH_AUTH_SUCCESS; + } else { + /* Wrong token — IP blocking handles brute force, no sleep needed here + * (sleeping in a libssh callback blocks the entire accept loop). */ + ratelimit_record_auth_failure(ctx->client_ip); + return SSH_AUTH_DENIED; + } + } else { + /* No token configured, accept any password */ + ctx->auth_success = true; + return SSH_AUTH_SUCCESS; + } +} + +/* Passwordless (none) authentication callback */ +static int auth_none(ssh_session session, const char *user, void *userdata) { + (void)session; /* Unused */ + session_context_t *ctx = (session_context_t *)userdata; + + if (user && user[0] != '\0') { + strncpy(ctx->requested_user, user, sizeof(ctx->requested_user) - 1); + ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0'; + } + + /* If access token is configured, reject passwordless */ + if (g_access_token[0] != '\0') { + return SSH_AUTH_DENIED; + } else { + /* No token configured, allow passwordless */ + ctx->auth_success = true; + return SSH_AUTH_SUCCESS; + } +} + +/* Public key authentication callback */ +static int auth_pubkey(ssh_session session, const char *user, + struct ssh_key_struct *pubkey, char signature_state, + void *userdata) { + (void)session; + (void)pubkey; + session_context_t *ctx = (session_context_t *)userdata; + + if (user && user[0] != '\0') { + strncpy(ctx->requested_user, user, sizeof(ctx->requested_user) - 1); + ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0'; + } + + /* Reject if access token is required (pubkey auth not supported with tokens) */ + if (g_access_token[0] != '\0') { + return SSH_AUTH_DENIED; + } + + /* SSH_PUBLICKEY_STATE_NONE = key offer (no signature yet). + * Return SUCCESS to tell libssh "I accept this key, verify the signature." + * SSH_PUBLICKEY_STATE_VALID = signature verified by libssh. */ + if (signature_state != SSH_PUBLICKEY_STATE_VALID) { + return SSH_AUTH_SUCCESS; + } + + ctx->auth_success = true; + return SSH_AUTH_SUCCESS; +} + +static void destroy_session_context(session_context_t *ctx) { + if (!ctx) { + return; + } + + if (ctx->channel_cb) { + free(ctx->channel_cb); + } + + free(ctx); +} + +static void cleanup_failed_session(ssh_session session, session_context_t *ctx) { + if (ctx && ctx->channel) { + if (ctx->channel_cb) { + ssh_remove_channel_callbacks(ctx->channel, ctx->channel_cb); + } + ssh_channel_close(ctx->channel); + ssh_channel_free(ctx->channel); + ctx->channel = NULL; + } + + if (session) { + ssh_disconnect(session); + ssh_free(session); + } + + if (ctx) { + ratelimit_release_ip(ctx->client_ip); + } + destroy_session_context(ctx); + ratelimit_decrement_total(); +} + +static void setup_session_channel_callbacks(ssh_channel channel, + session_context_t *ctx); + +/* Channel open callback */ +static ssh_channel channel_open_request_session(ssh_session session, void *userdata) { + session_context_t *ctx = (session_context_t *)userdata; + ssh_channel channel; + + channel = ssh_channel_new(session); + if (channel == NULL) { + return NULL; + } + + /* Store channel in context for main loop */ + ctx->channel = channel; + + /* Set up channel-specific callbacks (PTY, shell, exec) */ + setup_session_channel_callbacks(channel, ctx); + + return channel; +} + +/* PTY request callback */ +static int channel_pty_request(ssh_session session, ssh_channel channel, + const char *term, int width, int height, + int pxwidth, int pxheight, void *userdata) { + (void)session; /* Unused */ + (void)channel; /* Unused */ + (void)term; /* Unused */ + (void)pxwidth; /* Unused */ + (void)pxheight; /* Unused */ + + session_context_t *ctx = (session_context_t *)userdata; + + /* Store terminal dimensions */ + ctx->pty_width = width; + ctx->pty_height = height; + + sanitize_terminal_size(&ctx->pty_width, &ctx->pty_height); + + return SSH_OK; +} + +static int channel_pty_window_change(ssh_session session, ssh_channel channel, + int width, int height, + int pxwidth, int pxheight, + void *userdata) { + (void)session; + (void)channel; + (void)pxwidth; + (void)pxheight; + + session_context_t *ctx = (session_context_t *)userdata; + + ctx->pty_width = width; + ctx->pty_height = height; + sanitize_terminal_size(&ctx->pty_width, &ctx->pty_height); + + return SSH_OK; +} + +/* Shell request callback */ +static int channel_shell_request(ssh_session session, ssh_channel channel, + void *userdata) { + (void)session; /* Unused */ + (void)channel; /* Unused */ + + session_context_t *ctx = (session_context_t *)userdata; + + /* Mark channel as ready */ + ctx->channel_ready = true; + + /* Accept shell request */ + return SSH_OK; +} + +/* Exec request callback */ +static int channel_exec_request(ssh_session session, ssh_channel channel, + const char *command, void *userdata) { + (void)session; /* Unused */ + (void)channel; /* Unused */ + + session_context_t *ctx = (session_context_t *)userdata; + + /* Store exec command */ + if (command) { + strncpy(ctx->exec_command, command, sizeof(ctx->exec_command) - 1); + ctx->exec_command[sizeof(ctx->exec_command) - 1] = '\0'; + } + + /* Mark channel as ready */ + ctx->channel_ready = true; + + return SSH_OK; +} + +/* Set up channel callbacks */ +static void setup_session_channel_callbacks(ssh_channel channel, + session_context_t *ctx) { + /* Allocate channel callbacks on heap to persist */ + ctx->channel_cb = calloc(1, sizeof(struct ssh_channel_callbacks_struct)); + if (!ctx->channel_cb) { + return; + } + + ssh_callbacks_init(ctx->channel_cb); + + ctx->channel_cb->userdata = ctx; + ctx->channel_cb->channel_pty_request_function = channel_pty_request; + ctx->channel_cb->channel_shell_request_function = channel_shell_request; + ctx->channel_cb->channel_pty_window_change_function = channel_pty_window_change; + ctx->channel_cb->channel_exec_request_function = channel_exec_request; + + ssh_set_channel_callbacks(channel, ctx->channel_cb); +} + +void *bootstrap_run(void *arg) { + accepted_session_t *accepted = (accepted_session_t *)arg; + ssh_session session; + session_context_t *ctx = NULL; + ssh_event event = NULL; + struct ssh_server_callbacks_struct server_cb; + ssh_channel channel; + client_t *client = NULL; + bool timed_out = false; + time_t start_time; + char accepted_ip[INET6_ADDRSTRLEN] = ""; + + if (!accepted) { + return NULL; + } + + session = accepted->session; + if (accepted->client_ip[0] != '\0') { + snprintf(accepted_ip, sizeof(accepted_ip), "%s", accepted->client_ip); + } + free(accepted); + + ctx = calloc(1, sizeof(session_context_t)); + if (!ctx) { + ratelimit_release_ip(accepted_ip); + ssh_disconnect(session); + ssh_free(session); + ratelimit_decrement_total(); + return NULL; + } + + if (accepted_ip[0] != '\0') { + snprintf(ctx->client_ip, sizeof(ctx->client_ip), "%s", accepted_ip); + } else { + bootstrap_peer_ip(session, ctx->client_ip, sizeof(ctx->client_ip)); + } + ctx->pty_width = 80; + ctx->pty_height = 24; + ctx->exec_command[0] = '\0'; + ctx->requested_user[0] = '\0'; + ctx->auth_success = false; + ctx->auth_attempts = 0; + ctx->channel_ready = false; + ctx->channel = NULL; + ctx->channel_cb = NULL; + + memset(&server_cb, 0, sizeof(server_cb)); + ssh_callbacks_init(&server_cb); + server_cb.userdata = ctx; + server_cb.auth_password_function = auth_password; + server_cb.auth_none_function = auth_none; + server_cb.auth_pubkey_function = auth_pubkey; + server_cb.channel_open_request_session_function = channel_open_request_session; + ssh_set_server_callbacks(session, &server_cb); + + if (ssh_handle_key_exchange(session) != SSH_OK) { + fprintf(stderr, "Key exchange failed from %s: %s\n", + ctx->client_ip, ssh_get_error(session)); + cleanup_failed_session(session, ctx); + return NULL; + } + + event = ssh_event_new(); + if (!event) { + fprintf(stderr, "Failed to create SSH event for %s\n", ctx->client_ip); + cleanup_failed_session(session, ctx); + return NULL; + } + + if (ssh_event_add_session(event, session) != SSH_OK) { + fprintf(stderr, "Failed to add session to event loop for %s\n", + ctx->client_ip); + ssh_event_free(event); + cleanup_failed_session(session, ctx); + return NULL; + } + + start_time = time(NULL); + while ((!ctx->auth_success || ctx->channel == NULL || !ctx->channel_ready) && + !timed_out) { + int rc = ssh_event_dopoll(event, 1000); + + if (rc == SSH_ERROR) { + fprintf(stderr, "Event poll error from %s: %s\n", + ctx->client_ip, ssh_get_error(session)); + break; + } + + if (time(NULL) - start_time > 10) { + timed_out = true; + } + } + + ssh_event_free(event); + event = NULL; + + if (!ctx->auth_success) { + fprintf(stderr, "Authentication failed or timed out from %s\n", + ctx->client_ip); + cleanup_failed_session(session, ctx); + return NULL; + } + + channel = ctx->channel; + if (!channel || !ctx->channel_ready || timed_out) { + fprintf(stderr, "Failed to open/setup channel from %s\n", + ctx->client_ip); + cleanup_failed_session(session, ctx); + return NULL; + } + + client = calloc(1, sizeof(client_t)); + if (!client) { + cleanup_failed_session(session, ctx); + return NULL; + } + + client->session = session; + client->channel = channel; + int init_w = ctx->pty_width; + int init_h = ctx->pty_height; + sanitize_terminal_size(&init_w, &init_h); + client->width = init_w; + client->height = init_h; + client->ref_count = 1; + pthread_mutex_init(&client->ref_lock, NULL); + pthread_mutex_init(&client->io_lock, NULL); + + if (ctx->requested_user[0] != '\0') { + strncpy(client->ssh_login, ctx->requested_user, + sizeof(client->ssh_login) - 1); + client->ssh_login[sizeof(client->ssh_login) - 1] = '\0'; + } + if (ctx->client_ip[0] != '\0') { + snprintf(client->client_ip, sizeof(client->client_ip), "%s", + ctx->client_ip); + } + if (ctx->exec_command[0] != '\0') { + strncpy(client->exec_command, ctx->exec_command, + sizeof(client->exec_command) - 1); + client->exec_command[sizeof(client->exec_command) - 1] = '\0'; + } + + /* Add a ref for the channel callbacks (eof/close/window_change) so the + * client_t outlives any in-flight callback invocation. */ + client_addref(client); + + if (client_install_channel_callbacks(client) < 0) { + /* Nullify session/channel ownership so client_release won't + * double-free what cleanup_failed_session is about to free. */ + client->session = NULL; + client->channel = NULL; + client_release(client); /* drop the callback ref (2 → 1) */ + client_release(client); /* drop the main ref (1 → 0, frees client) */ + cleanup_failed_session(session, ctx); + return NULL; + } + + if (ctx->channel_cb) { + ssh_remove_channel_callbacks(channel, ctx->channel_cb); + free(ctx->channel_cb); + ctx->channel_cb = NULL; + } + destroy_session_context(ctx); + + client_handle_session(client); + return NULL; +} diff --git a/src/common.c b/src/common.c index c7be7e2..a1890d1 100644 --- a/src/common.c +++ b/src/common.c @@ -161,3 +161,16 @@ bool is_valid_username(const char *username) { return true; } + +void sanitize_terminal_size(int *width, int *height) { + if (!width || !height) { + return; + } + + if (*width <= 0 || *width > 500) { + *width = 80; + } + if (*height <= 0 || *height > 200) { + *height = 24; + } +} diff --git a/src/ssh_server.c b/src/ssh_server.c index 9d1ebe9..de2c782 100644 --- a/src/ssh_server.c +++ b/src/ssh_server.c @@ -1,4 +1,5 @@ #include "ssh_server.h" +#include "bootstrap.h" #include "commands.h" #include "exec.h" #include "ratelimit.h" @@ -22,92 +23,17 @@ static ssh_bind g_sshbind = NULL; static int g_listen_port = DEFAULT_PORT; -/* Session context for callback-based API */ -typedef struct { - char client_ip[INET6_ADDRSTRLEN]; - char requested_user[MAX_USERNAME_LEN]; - int pty_width; - int pty_height; - char exec_command[MAX_EXEC_COMMAND_LEN]; - bool auth_success; - int auth_attempts; - bool channel_ready; /* Set when shell/exec request received */ - ssh_channel channel; /* Channel created in callback */ - struct ssh_channel_callbacks_struct *channel_cb; /* Channel callbacks */ -} session_context_t; - -typedef struct { - ssh_session session; - char client_ip[INET6_ADDRSTRLEN]; -} accepted_session_t; - static time_t g_server_start_time = 0; time_t ssh_server_start_time(void) { return g_server_start_time; } -/* Configuration from environment variables. Rate-limiting / connection-count - * config has moved to ratelimit.{c,h}; the two below stay here until the auth - * and input modules are extracted in later PR2 steps. */ -static char g_access_token[256] = ""; +/* Configuration from environment variables. Rate-limiting moved to ratelimit.{c,h} + * and the access token now lives in bootstrap.{c,h}; the idle timeout stays here + * until input.c is extracted in PR2-M5. */ static int g_idle_timeout = DEFAULT_IDLE_TIMEOUT; -/* Constant-time string comparison to prevent timing side-channel attacks. - * Always iterates over the full length of the secret (b) to avoid leaking - * its length. When the input (a) is shorter, compares against zero bytes; - * the length mismatch is folded into the result separately. - * - * Note: the length-diff is accumulated in size_t to avoid the bug where a - * narrower type (e.g. unsigned char) would collapse pairs like (300, 44) to - * 0 because 300 ^ 44 == 256 ^ (44 ^ 44) == 256 which truncates to 0. */ -static bool constant_time_strcmp(const char *a, const char *b) { - size_t len_a = strlen(a); - size_t len_b = strlen(b); - volatile size_t length_diff = len_a ^ len_b; - volatile unsigned char byte_diff = 0; - for (size_t i = 0; i < len_b; i++) { - unsigned char ca = (i < len_a) ? (unsigned char)a[i] : 0; - byte_diff |= ca ^ (unsigned char)b[i]; - } - return length_diff == 0 && byte_diff == 0; -} - -/* Get client IP address */ -static void get_client_ip(ssh_session session, char *ip_buf, size_t buf_size) { - int fd = ssh_get_fd(session); - struct sockaddr_storage addr; - socklen_t addr_len = sizeof(addr); - - if (getpeername(fd, (struct sockaddr *)&addr, &addr_len) == 0) { - if (addr.ss_family == AF_INET) { - struct sockaddr_in *s = (struct sockaddr_in *)&addr; - inet_ntop(AF_INET, &s->sin_addr, ip_buf, buf_size); - } else if (addr.ss_family == AF_INET6) { - struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr; - inet_ntop(AF_INET6, &s->sin6_addr, ip_buf, buf_size); - } else { - strncpy(ip_buf, "unknown", buf_size - 1); - } - } else { - strncpy(ip_buf, "unknown", buf_size - 1); - } - ip_buf[buf_size - 1] = '\0'; -} - -static void sanitize_terminal_size(int *width, int *height) { - if (!width || !height) { - return; - } - - if (*width <= 0 || *width > 500) { - *width = 80; - } - if (*height <= 0 || *height > 200) { - *height = 24; - } -} - /* Generate or load SSH host key */ static int setup_host_key(ssh_bind sshbind) { struct stat st; @@ -890,7 +816,7 @@ cleanup: ssh_remove_channel_callbacks(client->channel, client->channel_cb); } - /* Release the callback reference (paired with addref before install_client_channel_callbacks) */ + /* Release the callback reference (paired with addref before client_install_channel_callbacks) */ client_release(client); /* Release the main reference - client will be freed when all refs are gone */ @@ -902,249 +828,6 @@ cleanup: return NULL; } -/* Authentication callbacks for callback-based API */ - -/* Password authentication callback */ -static int auth_password(ssh_session session, const char *user, - const char *password, void *userdata) { - session_context_t *ctx = (session_context_t *)userdata; - - if (user && user[0] != '\0') { - strncpy(ctx->requested_user, user, sizeof(ctx->requested_user) - 1); - ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0'; - } - - ctx->auth_attempts++; - - /* Limit auth attempts */ - if (ctx->auth_attempts > 3) { - ratelimit_record_auth_failure(ctx->client_ip); - fprintf(stderr, "Too many auth attempts from %s\n", ctx->client_ip); - ssh_disconnect(session); - return SSH_AUTH_DENIED; - } - - /* If access token is configured, require it */ - if (g_access_token[0] != '\0') { - if (password && constant_time_strcmp(password, g_access_token)) { - /* Token matches */ - ctx->auth_success = true; - return SSH_AUTH_SUCCESS; - } else { - /* Wrong token — IP blocking handles brute force, no sleep needed here - * (sleeping in a libssh callback blocks the entire accept loop). */ - ratelimit_record_auth_failure(ctx->client_ip); - return SSH_AUTH_DENIED; - } - } else { - /* No token configured, accept any password */ - ctx->auth_success = true; - return SSH_AUTH_SUCCESS; - } -} - -/* Passwordless (none) authentication callback */ -static int auth_none(ssh_session session, const char *user, void *userdata) { - (void)session; /* Unused */ - session_context_t *ctx = (session_context_t *)userdata; - - if (user && user[0] != '\0') { - strncpy(ctx->requested_user, user, sizeof(ctx->requested_user) - 1); - ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0'; - } - - /* If access token is configured, reject passwordless */ - if (g_access_token[0] != '\0') { - return SSH_AUTH_DENIED; - } else { - /* No token configured, allow passwordless */ - ctx->auth_success = true; - return SSH_AUTH_SUCCESS; - } -} - -/* Public key authentication callback */ -static int auth_pubkey(ssh_session session, const char *user, - struct ssh_key_struct *pubkey, char signature_state, - void *userdata) { - (void)session; - (void)pubkey; - session_context_t *ctx = (session_context_t *)userdata; - - if (user && user[0] != '\0') { - strncpy(ctx->requested_user, user, sizeof(ctx->requested_user) - 1); - ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0'; - } - - /* Reject if access token is required (pubkey auth not supported with tokens) */ - if (g_access_token[0] != '\0') { - return SSH_AUTH_DENIED; - } - - /* SSH_PUBLICKEY_STATE_NONE = key offer (no signature yet). - * Return SUCCESS to tell libssh "I accept this key, verify the signature." - * SSH_PUBLICKEY_STATE_VALID = signature verified by libssh. */ - if (signature_state != SSH_PUBLICKEY_STATE_VALID) { - return SSH_AUTH_SUCCESS; - } - - ctx->auth_success = true; - return SSH_AUTH_SUCCESS; -} - -static void destroy_session_context(session_context_t *ctx) { - if (!ctx) { - return; - } - - if (ctx->channel_cb) { - free(ctx->channel_cb); - } - - free(ctx); -} - -static void cleanup_failed_session(ssh_session session, session_context_t *ctx) { - if (ctx && ctx->channel) { - if (ctx->channel_cb) { - ssh_remove_channel_callbacks(ctx->channel, ctx->channel_cb); - } - ssh_channel_close(ctx->channel); - ssh_channel_free(ctx->channel); - ctx->channel = NULL; - } - - if (session) { - ssh_disconnect(session); - ssh_free(session); - } - - if (ctx) { - ratelimit_release_ip(ctx->client_ip); - } - destroy_session_context(ctx); - ratelimit_decrement_total(); -} - -static void setup_session_channel_callbacks(ssh_channel channel, - session_context_t *ctx); -static int install_client_channel_callbacks(client_t *client); - -/* Channel open callback */ -static ssh_channel channel_open_request_session(ssh_session session, void *userdata) { - session_context_t *ctx = (session_context_t *)userdata; - ssh_channel channel; - - channel = ssh_channel_new(session); - if (channel == NULL) { - return NULL; - } - - /* Store channel in context for main loop */ - ctx->channel = channel; - - /* Set up channel-specific callbacks (PTY, shell, exec) */ - setup_session_channel_callbacks(channel, ctx); - - return channel; -} - -/* Channel callback functions */ - -/* PTY request callback */ -static int channel_pty_request(ssh_session session, ssh_channel channel, - const char *term, int width, int height, - int pxwidth, int pxheight, void *userdata) { - (void)session; /* Unused */ - (void)channel; /* Unused */ - (void)term; /* Unused */ - (void)pxwidth; /* Unused */ - (void)pxheight; /* Unused */ - - session_context_t *ctx = (session_context_t *)userdata; - - /* Store terminal dimensions */ - ctx->pty_width = width; - ctx->pty_height = height; - - sanitize_terminal_size(&ctx->pty_width, &ctx->pty_height); - - return SSH_OK; -} - -static int channel_pty_window_change(ssh_session session, ssh_channel channel, - int width, int height, - int pxwidth, int pxheight, - void *userdata) { - (void)session; - (void)channel; - (void)pxwidth; - (void)pxheight; - - session_context_t *ctx = (session_context_t *)userdata; - - ctx->pty_width = width; - ctx->pty_height = height; - sanitize_terminal_size(&ctx->pty_width, &ctx->pty_height); - - return SSH_OK; -} - -/* Shell request callback */ -static int channel_shell_request(ssh_session session, ssh_channel channel, - void *userdata) { - (void)session; /* Unused */ - (void)channel; /* Unused */ - - session_context_t *ctx = (session_context_t *)userdata; - - /* Mark channel as ready */ - ctx->channel_ready = true; - - /* Accept shell request */ - return SSH_OK; -} - -/* Exec request callback */ -static int channel_exec_request(ssh_session session, ssh_channel channel, - const char *command, void *userdata) { - (void)session; /* Unused */ - (void)channel; /* Unused */ - - session_context_t *ctx = (session_context_t *)userdata; - - /* Store exec command */ - if (command) { - strncpy(ctx->exec_command, command, sizeof(ctx->exec_command) - 1); - ctx->exec_command[sizeof(ctx->exec_command) - 1] = '\0'; - } - - /* Mark channel as ready */ - ctx->channel_ready = true; - - return SSH_OK; -} - -/* Set up channel callbacks */ -static void setup_session_channel_callbacks(ssh_channel channel, - session_context_t *ctx) { - /* Allocate channel callbacks on heap to persist */ - ctx->channel_cb = calloc(1, sizeof(struct ssh_channel_callbacks_struct)); - if (!ctx->channel_cb) { - return; - } - - ssh_callbacks_init(ctx->channel_cb); - - ctx->channel_cb->userdata = ctx; - ctx->channel_cb->channel_pty_request_function = channel_pty_request; - ctx->channel_cb->channel_shell_request_function = channel_shell_request; - ctx->channel_cb->channel_pty_window_change_function = channel_pty_window_change; - ctx->channel_cb->channel_exec_request_function = channel_exec_request; - - ssh_set_channel_callbacks(channel, ctx->channel_cb); -} - static int client_channel_window_change(ssh_session session, ssh_channel channel, int width, int height, int pxwidth, int pxheight, @@ -1190,7 +873,7 @@ static void client_channel_close(ssh_session session, ssh_channel channel, } } -static int install_client_channel_callbacks(client_t *client) { +int client_install_channel_callbacks(client_t *client) { if (!client || !client->channel) { return -1; } @@ -1216,187 +899,17 @@ static int install_client_channel_callbacks(client_t *client) { return 0; } -static void *bootstrap_client_session(void *arg) { - accepted_session_t *accepted = (accepted_session_t *)arg; - ssh_session session; - session_context_t *ctx = NULL; - ssh_event event = NULL; - struct ssh_server_callbacks_struct server_cb; - ssh_channel channel; - client_t *client = NULL; - bool timed_out = false; - time_t start_time; - char accepted_ip[INET6_ADDRSTRLEN] = ""; - - if (!accepted) { - return NULL; - } - - session = accepted->session; - if (accepted->client_ip[0] != '\0') { - snprintf(accepted_ip, sizeof(accepted_ip), "%s", accepted->client_ip); - } - free(accepted); - - ctx = calloc(1, sizeof(session_context_t)); - if (!ctx) { - ratelimit_release_ip(accepted_ip); - ssh_disconnect(session); - ssh_free(session); - ratelimit_decrement_total(); - return NULL; - } - - if (accepted_ip[0] != '\0') { - snprintf(ctx->client_ip, sizeof(ctx->client_ip), "%s", accepted_ip); - } else { - get_client_ip(session, ctx->client_ip, sizeof(ctx->client_ip)); - } - ctx->pty_width = 80; - ctx->pty_height = 24; - ctx->exec_command[0] = '\0'; - ctx->requested_user[0] = '\0'; - ctx->auth_success = false; - ctx->auth_attempts = 0; - ctx->channel_ready = false; - ctx->channel = NULL; - ctx->channel_cb = NULL; - - memset(&server_cb, 0, sizeof(server_cb)); - ssh_callbacks_init(&server_cb); - server_cb.userdata = ctx; - server_cb.auth_password_function = auth_password; - server_cb.auth_none_function = auth_none; - server_cb.auth_pubkey_function = auth_pubkey; - server_cb.channel_open_request_session_function = channel_open_request_session; - ssh_set_server_callbacks(session, &server_cb); - - if (ssh_handle_key_exchange(session) != SSH_OK) { - fprintf(stderr, "Key exchange failed from %s: %s\n", - ctx->client_ip, ssh_get_error(session)); - cleanup_failed_session(session, ctx); - return NULL; - } - - event = ssh_event_new(); - if (!event) { - fprintf(stderr, "Failed to create SSH event for %s\n", ctx->client_ip); - cleanup_failed_session(session, ctx); - return NULL; - } - - if (ssh_event_add_session(event, session) != SSH_OK) { - fprintf(stderr, "Failed to add session to event loop for %s\n", - ctx->client_ip); - ssh_event_free(event); - cleanup_failed_session(session, ctx); - return NULL; - } - - start_time = time(NULL); - while ((!ctx->auth_success || ctx->channel == NULL || !ctx->channel_ready) && - !timed_out) { - int rc = ssh_event_dopoll(event, 1000); - - if (rc == SSH_ERROR) { - fprintf(stderr, "Event poll error from %s: %s\n", - ctx->client_ip, ssh_get_error(session)); - break; - } - - if (time(NULL) - start_time > 10) { - timed_out = true; - } - } - - ssh_event_free(event); - event = NULL; - - if (!ctx->auth_success) { - fprintf(stderr, "Authentication failed or timed out from %s\n", - ctx->client_ip); - cleanup_failed_session(session, ctx); - return NULL; - } - - channel = ctx->channel; - if (!channel || !ctx->channel_ready || timed_out) { - fprintf(stderr, "Failed to open/setup channel from %s\n", - ctx->client_ip); - cleanup_failed_session(session, ctx); - return NULL; - } - - client = calloc(1, sizeof(client_t)); - if (!client) { - cleanup_failed_session(session, ctx); - return NULL; - } - - client->session = session; - client->channel = channel; - int init_w = ctx->pty_width; - int init_h = ctx->pty_height; - sanitize_terminal_size(&init_w, &init_h); - client->width = init_w; - client->height = init_h; - client->ref_count = 1; - pthread_mutex_init(&client->ref_lock, NULL); - pthread_mutex_init(&client->io_lock, NULL); - - if (ctx->requested_user[0] != '\0') { - strncpy(client->ssh_login, ctx->requested_user, - sizeof(client->ssh_login) - 1); - client->ssh_login[sizeof(client->ssh_login) - 1] = '\0'; - } - if (ctx->client_ip[0] != '\0') { - snprintf(client->client_ip, sizeof(client->client_ip), "%s", - ctx->client_ip); - } - if (ctx->exec_command[0] != '\0') { - strncpy(client->exec_command, ctx->exec_command, - sizeof(client->exec_command) - 1); - client->exec_command[sizeof(client->exec_command) - 1] = '\0'; - } - - /* Add a ref for the channel callbacks (eof/close/window_change) so the - * client_t outlives any in-flight callback invocation. */ - client_addref(client); - - if (install_client_channel_callbacks(client) < 0) { - /* Nullify session/channel ownership so client_release won't - * double-free what cleanup_failed_session is about to free. */ - client->session = NULL; - client->channel = NULL; - client_release(client); /* drop the callback ref (2 → 1) */ - client_release(client); /* drop the main ref (1 → 0, frees client) */ - cleanup_failed_session(session, ctx); - return NULL; - } - - if (ctx->channel_cb) { - ssh_remove_channel_callbacks(channel, ctx->channel_cb); - free(ctx->channel_cb); - ctx->channel_cb = NULL; - } - destroy_session_context(ctx); - - client_handle_session(client); - return NULL; -} /* Initialize SSH server */ int ssh_server_init(int port) { /* Initialize rate-limit / connection-count subsystem */ ratelimit_init(); - /* Auth / session config (will move into auth.c and input.c in later PR2 steps) */ + /* Initialize bootstrap (reads TNT_ACCESS_TOKEN) */ + bootstrap_init(); + + /* Idle timeout stays here until input.c is extracted in PR2-M5 */ g_idle_timeout = env_int("TNT_IDLE_TIMEOUT", DEFAULT_IDLE_TIMEOUT, 0, 86400); - const char *token_env = getenv("TNT_ACCESS_TOKEN"); - if (token_env != NULL) { - strncpy(g_access_token, token_env, sizeof(g_access_token) - 1); - g_access_token[sizeof(g_access_token) - 1] = '\0'; - } g_listen_port = port; g_server_start_time = time(NULL); @@ -1469,7 +982,7 @@ int ssh_server_start(int unused) { continue; } - get_client_ip(session, client_ip, sizeof(client_ip)); + bootstrap_peer_ip(session, client_ip, sizeof(client_ip)); /* Check total connection limit */ if (!ratelimit_check_and_increment_total()) { @@ -1499,7 +1012,7 @@ int ssh_server_start(int unused) { snprintf(accepted->client_ip, sizeof(accepted->client_ip), "%s", client_ip); - if (pthread_create(&thread, &attr, bootstrap_client_session, accepted) != 0) { + if (pthread_create(&thread, &attr, bootstrap_run, accepted) != 0) { fprintf(stderr, "Thread creation failed: %s\n", strerror(errno)); free(accepted); ratelimit_release_ip(client_ip);