#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util.h" #define RESOURCE_ERROR_SLEEP_MS 10000 static const bool debug_enabled = false; __attribute__((format (printf, 1, 2))) static void debug(const char *restrict format, ...) { if (debug_enabled) { va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); } } static void xxd(FILE *stream, const void *buf_, size_t length) { unsigned char *buf = (unsigned char*)buf_; for (size_t cursor = 0; cursor < length;) { fprintf(stream, "%08zx:", cursor); for (int i = 0; i < 16; i++) { if (i % 2 == 0) fprintf(stream, " "); if (i % 8 == 0) fprintf(stream, " "); if (cursor + i < length) fprintf(stream, "%02x", (unsigned)buf[cursor + i]); else fprintf(stream, " "); } fprintf(stream, " |"); for (int i = 0; i < 16 && cursor + i < length; i++) { if (isprint(buf[cursor + i])) fprintf(stream, "%c", buf[cursor + i]); else fprintf(stream, "."); } fprintf(stream, "|\n"); cursor += 16; } } static atomic_int g_thread_count; struct thread_data { struct addrinfo backend_addr; int backend_fd; int thread_id; ssh_session session; ssh_channel channel; // NULL before channel has been opened bool should_close; struct ssh_server_callbacks_struct server_cb; struct ssh_channel_callbacks_struct chan_cb; }; ///////// CHANNEL CALLBACKS ////////// static int channel_subsystem_request_cb(ssh_session session, ssh_channel channel, const char *subsystem, void *tdata_) { (void)session; (void)channel; struct thread_data *tdata = (struct thread_data*)tdata_; if (strcmp(subsystem, "tomsg") == 0) { debug("[%d] subsystem request: <%s>, allowing\n", tdata->thread_id, subsystem); return 0; } else { debug("[%d] subsystem request: <%s>, denying!\n", tdata->thread_id, subsystem); return 1; } } static void channel_close_cb(ssh_session session, ssh_channel channel, void *tdata_) { (void)session; (void)channel; struct thread_data *tdata = (struct thread_data*)tdata_; debug("[%d] channel close!\n", tdata->thread_id); } static void channel_eof_cb(ssh_session session, ssh_channel channel, void *tdata_) { (void)session; (void)channel; struct thread_data *tdata = (struct thread_data*)tdata_; debug("[%d] eof on channel, setting close flag\n", tdata->thread_id); tdata->should_close = true; } static int channel_data_cb(ssh_session session, ssh_channel channel, void *data, uint32_t len, int is_stderr, void *tdata_) { (void)is_stderr; (void)data; (void)channel; (void)session; struct thread_data *tdata = (struct thread_data*)tdata_; debug("[%d] data on channel (length %u):\n", tdata->thread_id, len); if (debug_enabled) xxd(stdout, data, len); // debug("[%d] echoing back!\n", tdata->thread_id); // if (ssh_channel_write(channel, data, len) == SSH_ERROR) { // debug("[%d] write to channel failed! Setting close flag\n", tdata->thread_id); // tdata->should_close = true; // } const char *start = (const char*)data; const char *cursor = start; const char *end = start + len; while (cursor < end) { ssize_t nw = write(tdata->backend_fd, cursor, end - cursor); if (nw < 0) { if (errno == EINTR) continue; debug("[%d] error writing to backend socket: %s\n", tdata->thread_id, strerror(errno)); tdata->should_close = true; return cursor - start; } if (nw == 0) { // should not happen? debug("[%d] write(2) returned 0?\n", tdata->thread_id); tdata->should_close = true; return cursor - start; } cursor += nw; } return len; } ////////// SERVER CALLBACKS ////////// static int auth_none_cb(ssh_session session, const char *user, void *tdata_) { (void)session; struct thread_data *tdata = (struct thread_data*)tdata_; debug("[%d] auth none (user <%s>), accepting\n", tdata->thread_id, user); return SSH_AUTH_SUCCESS; } static int service_request_cb(ssh_session session, const char *service, void *tdata_) { (void)session; struct thread_data *tdata = (struct thread_data*)tdata_; if (strcmp(service, "ssh-userauth") == 0) { debug("[%d] ssh-userauth service request, allowing through\n", tdata->thread_id); return 0; } else { debug("[%d] service request <%s>, not allowing\n", tdata->thread_id, service); return -1; } } static ssh_channel chan_open_request_cb(ssh_session session, void *tdata_) { struct thread_data *tdata = (struct thread_data*)tdata_; if (tdata->channel == NULL) { ssh_channel chan = ssh_channel_new(session); if (chan != NULL) { if (ssh_set_channel_callbacks(chan, &tdata->chan_cb) == SSH_OK) { debug("[%d] channel open request, allowing\n", tdata->thread_id); tdata->channel = chan; return chan; } ssh_channel_close(chan); ssh_channel_free(chan); } } debug("[%d] channel open request, denying!\n", tdata->thread_id); return NULL; } static int backend_data_cb(int fd, int revents, void *tdata_) { struct thread_data *tdata = (struct thread_data*)tdata_; if (revents & (POLLERR|POLLHUP|POLLNVAL)) { // Print this always, because backend issues are interesting char descr[64] = ""; if (revents & POLLERR) strcat(descr, "|POLLERR"); if (revents & POLLHUP) strcat(descr, "|POLLHUP"); if (revents & POLLNVAL) strcat(descr, "|POLLNVAL"); printf("[%d] %s on backend\n", tdata->thread_id, descr + 1); close(fd); tdata->should_close = true; } if (revents & POLLIN) { char buffer[1024]; ssize_t nr = read(fd, buffer, sizeof buffer); if (nr < 0) { if (errno == EINTR) return 0; // Backend issues are interesting, not noise printf("[%d] Error reading from backend socket: %s\n", tdata->thread_id, strerror(errno)); tdata->should_close = true; return 0; } if (nr == 0) { // eof tdata->should_close = true; return 0; } debug("[%d] data from backend (length %zu):\n", tdata->thread_id, nr); if (debug_enabled) xxd(stdout, buffer, nr); ssize_t cursor = 0; while (cursor < nr) { int nw = ssh_channel_write(tdata->channel, buffer + cursor, nr - cursor); if (nw == SSH_ERROR) { debug("[%d] Error writing to ssh channel: %s\n", tdata->thread_id, ssh_get_error(tdata->channel)); tdata->should_close = true; return 0; } cursor += nw; debug("[%d] forwarded %d bytes, total %zd/%zd\n", tdata->thread_id, nw, cursor, nr); } } return 0; } static void print_addrinfo(FILE *stream, const struct addrinfo *info) { if (info->ai_family == AF_INET) fprintf(stream, "inet "); else if (info->ai_family == AF_INET6) fprintf(stream, "inet6 "); else fprintf(stream, "(family=%d) ", info->ai_family); if (info->ai_socktype == SOCK_STREAM) fprintf(stream, "stream "); else if (info->ai_socktype == SOCK_DGRAM) fprintf(stream, "datagram "); else fprintf(stream, "(socktype=%d) ", info->ai_socktype); if (info->ai_protocol == IPPROTO_TCP) fprintf(stream, "TCP "); else if (info->ai_protocol == IPPROTO_UDP) fprintf(stream, "UDP "); else fprintf(stream, "(protocol=%d) ", info->ai_protocol); bool success = false; if (info->ai_family == AF_INET) { char addrbuf[INET_ADDRSTRLEN]; struct sockaddr_in *sin = (struct sockaddr_in*)info->ai_addr; if (inet_ntop(AF_INET, &sin->sin_addr, addrbuf, INET_ADDRSTRLEN)) { fprintf(stream, "%s\n", addrbuf); success = true; } } else if (info->ai_family == AF_INET6) { char addrbuf[INET6_ADDRSTRLEN]; struct sockaddr_in6 *sin = (struct sockaddr_in6*)info->ai_addr; if (inet_ntop(AF_INET6, &sin->sin6_addr, addrbuf, INET6_ADDRSTRLEN)) { fprintf(stream, "%s\n", addrbuf); success = true; } } if (!success) { fprintf(stream, "(unknown address format: %s)\n", strerror(errno)); } } // Returns whether successful. static bool lookup_backend(const char *host, int port, struct addrinfo *dst) { char port_string[16]; sprintf(port_string, "%d", port); struct addrinfo hints; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_ADDRCONFIG; struct addrinfo *result; int ret = getaddrinfo(host, port_string, &hints, &result); if (ret < 0) { fprintf(stderr, "Could not resolve backend: %s\n", gai_strerror(ret)); return false; } int last_failure = 0; bool success = false; for (struct addrinfo *item = result; item; ) { debug("lookup_backend: option "); if (debug_enabled) print_addrinfo(stdout, item); int sock = socket(item->ai_family, item->ai_socktype, item->ai_protocol); if (sock == -1) { last_failure = errno; debug(" socket() failure: %s\n", strerror(last_failure)); continue; } int ret = connect(sock, item->ai_addr, item->ai_addrlen); last_failure = errno; close(sock); if (ret == 0) { debug(" success!\n"); success = true; // Free the rest of the linked list, keeping this item intact. freeaddrinfo(item->ai_next); *dst = *item; dst->ai_next = NULL; break; } else { debug(" connect() failure: %s\n", strerror(last_failure)); } debug(" next=%p\n", item->ai_next); // Free this element in the linked list, but preserve (and switch to) the tail. struct addrinfo *next = item->ai_next; item->ai_next = NULL; freeaddrinfo(item); item = next; } if (success) { return true; } else { fprintf(stderr, "Could not connect to backend: %s\n", strerror(last_failure)); return false; } } static int connect_backend(const struct thread_data *tdata) { const struct addrinfo *item = &tdata->backend_addr; int sock = socket(item->ai_family, item->ai_socktype, item->ai_protocol); if (sock == -1) return -1; if (connect(sock, item->ai_addr, item->ai_addrlen) == 0) { debug("connect_backend: sock=%d\n", sock); return sock; } close(sock); return -1; } static void* thread_entry(void *tdata_) { struct thread_data *tdata = (struct thread_data*)tdata_; const int tid = tdata->thread_id; const ssh_session session = tdata->session; ssh_event event = NULL; debug("[%d] Thread started\n", tid); memset(&tdata->server_cb, 0, sizeof tdata->server_cb); ssh_callbacks_init(&tdata->server_cb); tdata->server_cb.userdata = tdata; tdata->server_cb.auth_none_function = auth_none_cb; tdata->server_cb.channel_open_request_session_function = chan_open_request_cb; tdata->server_cb.service_request_function = service_request_cb; memset(&tdata->chan_cb, 0, sizeof tdata->chan_cb); ssh_callbacks_init(&tdata->chan_cb); tdata->chan_cb.userdata = tdata; tdata->chan_cb.channel_subsystem_request_function = channel_subsystem_request_cb; tdata->chan_cb.channel_close_function = channel_close_cb; tdata->chan_cb.channel_eof_function = channel_eof_cb; tdata->chan_cb.channel_data_function = channel_data_cb; if (ssh_set_server_callbacks(session, &tdata->server_cb) != SSH_OK) { debug("[%d] Failed setting server callbacks: %s\n", tid, ssh_get_error(session)); goto cleanup; } ssh_set_auth_methods(session, SSH_AUTH_METHOD_NONE); if (ssh_handle_key_exchange(session) != SSH_OK) { debug("[%d] Key exchange failed: %s\n", tid, ssh_get_error(session)); goto cleanup; } debug("[%d] Handled key exchange\n", tid); tdata->backend_fd = connect_backend(tdata); if (tdata->backend_fd == -1) { debug("[%d] Failed to connect to backend: %s\n", tid, strerror(errno)); goto cleanup; } debug("[%d] Connected to backend (fd=%d)\n", tid, tdata->backend_fd); event = ssh_event_new(); if (!event || ssh_event_add_session(event, session) != SSH_OK || ssh_event_add_fd(event, tdata->backend_fd, POLLIN, backend_data_cb, tdata) != SSH_OK) { debug("[%d] Failed to create ssh event context\n", tid); goto cleanup; } while (!tdata->should_close) { // printf("[%d] poll loop...\n", tid); ssh_event_dopoll(event, -1); int status = ssh_get_status(session); if (status & (SSH_CLOSED | SSH_CLOSED_ERROR)) goto cleanup; if (status & SSH_READ_PENDING) { debug("[%d] read pending?\n", tid); } } cleanup: if (tdata->backend_fd != -1) close(tdata->backend_fd); if (event) ssh_event_free(event); if (tdata->channel) { ssh_channel_close(tdata->channel); ssh_channel_free(tdata->channel); } if (session) { ssh_disconnect(session); ssh_free(session); debug("[%d] Disconnected\n", tid); } free(tdata); int num_threads = atomic_fetch_sub(&g_thread_count, 1); debug("[%d] Exiting! (%d threads remaining)\n", tid, num_threads - 1); return NULL; } static void generate_key(const char *outfile) { ssh_key host_key; int ret = ssh_pki_generate(SSH_KEYTYPE_RSA, 4096, &host_key); if (ret != SSH_OK) { fprintf(stderr, "Key generation failed (RSA4096)!\n"); exit(1); } ret = ssh_pki_export_privkey_file(host_key, NULL, NULL, NULL, outfile); if (ret != SSH_OK) { fprintf(stderr, "Failed to export generated host key to file '%s'; is that location accessible?\n", outfile); exit(1); } if (chmod(outfile, S_IRUSR | S_IWUSR) != 0) { fprintf(stderr, "Failed to set mode 600 on generated host key file '%s'; this is insecure!\n", outfile); exit(1); } printf("RSA4096 host key generated and written to '%s'.\n", outfile); } static void usage(const char *argv0) { fprintf(stderr, "Usage: %s [backendhost:port]\n" " %s --generate \n" "SSH-TCP bridge for tomsg. Accepts SSH connections with a channel for subsystem\n" "'tomsg', and matches each SSH connection with a plain TCP connection to the\n" "backend server (which defaults to localhost:29536). All data is forwarded\n" "transparently.\n" "Use the '--generate' form to generate a host key for use in the main invocation\n" "form.\n", argv0, argv0); } int main(int argc, char **argv) { const char *host_key_fname; int ssh_port = 2222; const char *backend_host = "localhost"; int backend_port = 29536; if (argc == 3 && strcmp(argv[1], "--generate") == 0) { generate_key(argv[2]); return 0; } else if (3 <= argc && argc <= 4) { host_key_fname = argv[1]; char *endp; ssh_port = strtol(argv[2], &endp, 10); if (argv[2][0] == '\0' || *endp != '\0' || ssh_port < 0 || ssh_port > 65535) { fprintf(stderr, "Cannot parse port number from argument '%s'\n", argv[2]); return 1; } if (argc == 4) { if (!parse_host_port(argv[3], &backend_host, &backend_port)) { fprintf(stderr, "Cannot parse host:port from argument '%s'\n", argv[3]); return 1; } } } else { usage(argv[0]); return 1; } // We prefer to detect socket closure through return codes, not signals. signal(SIGPIPE, SIG_IGN); if (ssh_init() != SSH_OK) { fprintf(stderr, "Could not initialise libssh\n"); return 1; } ssh_key host_key; if (ssh_pki_import_privkey_file(host_key_fname, NULL, NULL, NULL, &host_key) != SSH_OK) { fprintf(stderr, "Failed to read host private key file '%s'\n", host_key_fname); return 1; } size_t host_key_hash_length = 0; unsigned char *host_key_hash = NULL; if (ssh_get_publickey_hash(host_key, SSH_PUBLICKEY_HASH_SHA256, &host_key_hash, &host_key_hash_length) != SSH_OK) { fprintf(stderr, "Failed to hash host key!\n"); return 1; } printf("Host key hash: "); fflush(stdout); ssh_print_hash(SSH_PUBLICKEY_HASH_SHA256, host_key_hash, host_key_hash_length); ssh_bind srvbind = ssh_bind_new(); if (!srvbind) { fprintf(stderr, "Failed to create new bind socket\n"); return 1; } bool procconfig = false; const char *ciphers_str = "aes256-gcm@openssh.com,aes256-ctr,aes256-cbc"; bool ok = true; ok &= ssh_bind_options_set(srvbind, SSH_BIND_OPTIONS_PROCESS_CONFIG, &procconfig) == SSH_OK; ok &= ssh_bind_options_set(srvbind, SSH_BIND_OPTIONS_BINDPORT, &ssh_port) == SSH_OK; ok &= ssh_bind_options_set(srvbind, SSH_BIND_OPTIONS_IMPORT_KEY, host_key) == SSH_OK; ok &= ssh_bind_options_set(srvbind, SSH_BIND_OPTIONS_CIPHERS_C_S, ciphers_str) == SSH_OK; ok &= ssh_bind_options_set(srvbind, SSH_BIND_OPTIONS_CIPHERS_S_C, ciphers_str) == SSH_OK; if (!ok) { fprintf(stderr, "Could not set options on SSH bind socket: %s\n", ssh_get_error(srvbind)); return 1; } if (ssh_bind_listen(srvbind) != SSH_OK) { fprintf(stderr, "Could not listen on SSH bind socket: %s\n", ssh_get_error(srvbind)); return 1; } struct addrinfo backend_addr; if (!lookup_backend(backend_host, backend_port, &backend_addr)) { // Error already printed in lookup_backend return 1; } printf("Listening for SSH connections on port %d\n", ssh_port); printf("Forwarding to backend at %s:%d\n", backend_host, backend_port); pthread_attr_t thread_attrs; assert(pthread_attr_init(&thread_attrs) == 0); assert(pthread_attr_setdetachstate(&thread_attrs, PTHREAD_CREATE_DETACHED) == 0); int next_thread_id = 0; atomic_store(&g_thread_count, 0); while (true) { ssh_session session = ssh_new(); if (!session) { fprintf(stderr, "ERROR: Cannot create new SSH session object!\n"); usleep(1000 * RESOURCE_ERROR_SLEEP_MS); continue; } if (ssh_bind_accept(srvbind, session) != SSH_OK) { fprintf(stderr, "ERROR: Cannot accept on bind socket: %s", ssh_get_error(srvbind)); ssh_free(session); usleep(1000 * RESOURCE_ERROR_SLEEP_MS); continue; } int num_existing_threads = atomic_fetch_add(&g_thread_count, 1); debug("Accepted connection, spinning up thread (currently %d threads)\n", num_existing_threads + 1); struct thread_data *tdata = calloc(1, sizeof(struct thread_data)); if (!tdata) { fprintf(stderr, "ERROR: Out of memory, cannot allocate thread_data!\n"); ssh_disconnect(session); ssh_free(session); usleep(1000 * RESOURCE_ERROR_SLEEP_MS); continue; } tdata->backend_addr = backend_addr; tdata->backend_fd = -1; tdata->thread_id = next_thread_id++; tdata->session = session; tdata->channel = NULL; tdata->should_close = false; pthread_t thread; if (pthread_create(&thread, &thread_attrs, thread_entry, tdata) != 0) { fprintf(stderr, "ERROR: Could not spawn thread: %s!\n", strerror(errno)); free(tdata); ssh_disconnect(session); ssh_free(session); usleep(1000 * RESOURCE_ERROR_SLEEP_MS); continue; } } }