diff options
Diffstat (limited to 'proxy/sshproxy.c')
-rw-r--r-- | proxy/sshproxy.c | 746 |
1 files changed, 746 insertions, 0 deletions
diff --git a/proxy/sshproxy.c b/proxy/sshproxy.c new file mode 100644 index 00000000..165c6c0a --- /dev/null +++ b/proxy/sshproxy.c @@ -0,0 +1,746 @@ +/* + * sshproxy.c: implement a Socket type that talks to an entire + * subsidiary SSH connection (sometimes called a 'jump host'). + */ + +#include <stdio.h> +#include <assert.h> + +#include "putty.h" +#include "ssh.h" +#include "network.h" +#include "storage.h" +#include "proxy.h" + +const bool ssh_proxy_supported = true; + +typedef struct SshProxy { + char *errmsg; + Conf *conf; + LogContext *logctx; + Backend *backend; + LogPolicy *clientlp; + Seat *clientseat; + Interactor *clientitr; + + bool got_proxy_password, tried_proxy_password; + char *proxy_password; + + ProxyStderrBuf psb; + Plug *plug; + + bool frozen; + bufchain ssh_to_socket; + bool rcvd_eof_ssh_to_socket, sent_eof_ssh_to_socket; + bool conn_established; + + SockAddr *addr; + int port; + + /* Traits implemented: we're a Socket from the point of view of + * the client connection, and a Seat from the POV of the SSH + * backend we instantiate. */ + Socket sock; + LogPolicy logpolicy; + Seat seat; +} SshProxy; + +static Plug *sshproxy_plug(Socket *s, Plug *p) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + Plug *oldplug = sp->plug; + if (p) + sp->plug = p; + return oldplug; +} + +static void sshproxy_close(Socket *s) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + + sk_addr_free(sp->addr); + sfree(sp->errmsg); + conf_free(sp->conf); + if (sp->backend) + backend_free(sp->backend); + if (sp->logctx) + log_free(sp->logctx); + if (sp->proxy_password) + burnstr(sp->proxy_password); + bufchain_clear(&sp->ssh_to_socket); + + delete_callbacks_for_context(sp); + sfree(sp); +} + +static size_t sshproxy_write(Socket *s, const void *data, size_t len) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + if (!sp->backend) + return 0; + backend_send(sp->backend, data, len); + return backend_sendbuffer(sp->backend); +} + +static size_t sshproxy_write_oob(Socket *s, const void *data, size_t len) +{ + /* + * oob data is treated as inband; nasty, but nothing really + * better we can do + */ + return sshproxy_write(s, data, len); +} + +static void sshproxy_write_eof(Socket *s) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + if (!sp->backend) + return; + backend_special(sp->backend, SS_EOF, 0); +} + +static void try_send_ssh_to_socket(void *ctx); + +static void try_send_ssh_to_socket_cb(void *ctx) +{ + SshProxy *sp = (SshProxy *)ctx; + try_send_ssh_to_socket(sp); + if (sp->backend) + backend_unthrottle(sp->backend, bufchain_size(&sp->ssh_to_socket)); +} + +static void sshproxy_set_frozen(Socket *s, bool is_frozen) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + sp->frozen = is_frozen; + if (!sp->frozen) + queue_toplevel_callback(try_send_ssh_to_socket_cb, sp); +} + +static const char *sshproxy_socket_error(Socket *s) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + return sp->errmsg; +} + +static SocketPeerInfo *sshproxy_peer_info(Socket *s) +{ + return NULL; +} + +static const SocketVtable SshProxy_sock_vt = { + .plug = sshproxy_plug, + .close = sshproxy_close, + .write = sshproxy_write, + .write_oob = sshproxy_write_oob, + .write_eof = sshproxy_write_eof, + .set_frozen = sshproxy_set_frozen, + .socket_error = sshproxy_socket_error, + .peer_info = sshproxy_peer_info, +}; + +static void sshproxy_eventlog(LogPolicy *lp, const char *event) +{ + SshProxy *sp = container_of(lp, SshProxy, logpolicy); + log_proxy_stderr(sp->plug, &sp->psb, event, strlen(event)); + log_proxy_stderr(sp->plug, &sp->psb, "\n", 1); +} + +static int sshproxy_askappend(LogPolicy *lp, Filename *filename, + void (*callback)(void *ctx, int result), + void *ctx) +{ + SshProxy *sp = container_of(lp, SshProxy, logpolicy); + + /* + * If we have access to the outer LogPolicy, pass on this request + * to the end user. + */ + if (sp->clientlp) + return lp_askappend(sp->clientlp, filename, callback, ctx); + + /* + * Otherwise, fall back to the safe noninteractive assumption. + */ + char *msg = dupprintf("Log file \"%s\" already exists; logging cancelled", + filename_to_str(filename)); + sshproxy_eventlog(lp, msg); + sfree(msg); + return 0; +} + +static void sshproxy_logging_error(LogPolicy *lp, const char *event) +{ + SshProxy *sp = container_of(lp, SshProxy, logpolicy); + + /* + * If we have access to the outer LogPolicy, pass on this request + * to it. + */ + if (sp->clientlp) { + lp_logging_error(sp->clientlp, event); + return; + } + + /* + * Otherwise, the best we can do is to put it in the outer SSH + * connection's Event Log. + */ + char *msg = dupprintf("Logging error: %s", event); + sshproxy_eventlog(lp, msg); + sfree(msg); +} + +static const LogPolicyVtable SshProxy_logpolicy_vt = { + .eventlog = sshproxy_eventlog, + .askappend = sshproxy_askappend, + .logging_error = sshproxy_logging_error, + .verbose = null_lp_verbose_no, +}; + +/* + * Function called when we encounter an error during connection setup that's + * likely to be the cause of terminating the proxy SSH connection. Putting it + * in the Event Log is useful on general principles; also putting it in + * sp->errmsg meaks that it will be passed back through plug_closing when the + * proxy SSH connection actually terminates, so that the end user will see + * what went wrong in the proxy connection. + */ +static void sshproxy_error(SshProxy *sp, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + char *msg = dupvprintf(fmt, ap); + va_end(ap); + + if (!sp->errmsg) + sp->errmsg = dupstr(msg); + + sshproxy_eventlog(&sp->logpolicy, msg); + sfree(msg); +} + +static void try_send_ssh_to_socket(void *ctx) +{ + SshProxy *sp = (SshProxy *)ctx; + + if (sp->frozen) + return; + + while (bufchain_size(&sp->ssh_to_socket)) { + ptrlen pl = bufchain_prefix(&sp->ssh_to_socket); + plug_receive(sp->plug, 0, pl.ptr, pl.len); + bufchain_consume(&sp->ssh_to_socket, pl.len); + } + + if (sp->rcvd_eof_ssh_to_socket && + !sp->sent_eof_ssh_to_socket) { + sp->sent_eof_ssh_to_socket = true; + plug_closing_normal(sp->plug); + } +} + +static void sshproxy_notify_session_started(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + if (sp->clientseat) + interactor_return_seat(sp->clientitr); + sp->conn_established = true; + + plug_log(sp->plug, PLUGLOG_CONNECT_SUCCESS, sp->addr, sp->port, NULL, 0); +} + +static size_t sshproxy_output(Seat *seat, SeatOutputType type, + const void *data, size_t len) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + switch (type) { + case SEAT_OUTPUT_STDOUT: + bufchain_add(&sp->ssh_to_socket, data, len); + try_send_ssh_to_socket(sp); + break; + case SEAT_OUTPUT_STDERR: + log_proxy_stderr(sp->plug, &sp->psb, data, len); + break; + } + return bufchain_size(&sp->ssh_to_socket); +} + +static inline InteractionReadySeat wrap(Seat *seat) +{ + /* + * When we receive interaction requests from the proxy and want to + * pass them on to our client Seat, we have to present them to the + * latter in the form of an InteractionReadySeat. This forwarding + * scenario is the one case where we _mustn't_ get an + * InteractionReadySeat by calling interactor_announce(), because + * the point is that we're _not_ the originating Interactor, we're + * just forwarding the request from the real one, which has + * already announced itself. + * + * So, just here in the code, it really is the right thing to make + * an InteractionReadySeat out of a plain Seat * without an + * announcement. + */ + InteractionReadySeat iseat; + iseat.seat = seat; + return iseat; +} + +static size_t sshproxy_banner(Seat *seat, const void *data, size_t len) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + if (sp->clientseat) { + /* + * If we have access to the outer Seat, pass the SSH login + * banner on to it. + */ + return seat_banner(wrap(sp->clientseat), data, len); + } else { + return 0; + } +} + +static bool sshproxy_eof(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + sp->rcvd_eof_ssh_to_socket = true; + try_send_ssh_to_socket(sp); + return false; +} + +static void sshproxy_sent(Seat *seat, size_t new_bufsize) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + plug_sent(sp->plug, new_bufsize); +} + +static void sshproxy_send_close(SshProxy *sp) +{ + if (sp->clientseat) + interactor_return_seat(sp->clientitr); + + if (!sp->conn_established) + plug_log(sp->plug, PLUGLOG_CONNECT_FAILED, sp->addr, sp->port, + sp->errmsg, 0); + + if (sp->errmsg) + plug_closing_error(sp->plug, sp->errmsg); + else if (!sp->conn_established && backend_exitcode(sp->backend) == 0) + plug_closing_user_abort(sp->plug); + else + plug_closing_normal(sp->plug); +} + +static void sshproxy_notify_remote_disconnect_callback(void *vctx) +{ + SshProxy *sp = (SshProxy *)vctx; + + /* notify_remote_disconnect can be called redundantly, so first + * check if the backend really has become disconnected */ + if (backend_connected(sp->backend)) + return; + + sshproxy_send_close(sp); +} + +static void sshproxy_notify_remote_disconnect(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + queue_toplevel_callback(sshproxy_notify_remote_disconnect_callback, sp); +} + +static SeatPromptResult sshproxy_get_userpass_input(Seat *seat, prompts_t *p) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + /* + * If we have a stored proxy_password, use that, via logic similar + * to cmdline_get_passwd_input: we only try it if we're given a + * prompts_t containing exactly one prompt, and that prompt is set + * to non-echoing. + */ + if (sp->got_proxy_password && !sp->tried_proxy_password && + p->n_prompts == 1 && !p->prompts[0]->echo) { + prompt_set_result(p->prompts[0], sp->proxy_password); + burnstr(sp->proxy_password); + sp->proxy_password = NULL; + sp->tried_proxy_password = true; + return SPR_OK; + } + + if (sp->clientseat) { + /* + * If we have access to the outer Seat, pass this prompt + * request on to it. + */ + return seat_get_userpass_input(wrap(sp->clientseat), p); + } + + /* + * Otherwise, behave as if noninteractive (like plink -batch): + * reject all attempts to present a prompt to the user, and log in + * the Event Log to say why not. + */ + sshproxy_error(sp, "Unable to provide interactive authentication " + "requested by proxy SSH connection"); + return SPR_SW_ABORT("Noninteractive SSH proxy cannot perform " + "interactive authentication"); +} + +static void sshproxy_connection_fatal_callback(void *vctx) +{ + SshProxy *sp = (SshProxy *)vctx; + sshproxy_send_close(sp); +} + +static void sshproxy_connection_fatal(Seat *seat, const char *message) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + if (!sp->errmsg) { + sp->errmsg = dupprintf( + "fatal error in proxy SSH connection: %s", message); + queue_toplevel_callback(sshproxy_connection_fatal_callback, sp); + } +} + +static SeatPromptResult sshproxy_confirm_ssh_host_key( + Seat *seat, const char *host, int port, const char *keytype, + char *keystr, SeatDialogText *text, HelpCtx helpctx, + void (*callback)(void *ctx, SeatPromptResult result), void *ctx) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + if (sp->clientseat) { + /* + * If we have access to the outer Seat, pass this prompt + * request on to it. + */ + return seat_confirm_ssh_host_key( + wrap(sp->clientseat), host, port, keytype, keystr, text, + helpctx, callback, ctx); + } + + /* + * Otherwise, behave as if we're in batch mode, i.e. take the safe + * option in the absence of interactive confirmation, i.e. abort + * the connection. + */ + return SPR_SW_ABORT("Noninteractive SSH proxy cannot confirm host key"); +} + +static SeatPromptResult sshproxy_confirm_weak_crypto_primitive( + Seat *seat, const char *algtype, const char *algname, + void (*callback)(void *ctx, SeatPromptResult result), void *ctx) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + if (sp->clientseat) { + /* + * If we have access to the outer Seat, pass this prompt + * request on to it. + */ + return seat_confirm_weak_crypto_primitive( + wrap(sp->clientseat), algtype, algname, callback, ctx); + } + + /* + * Otherwise, behave as if we're in batch mode: take the safest + * option. + */ + sshproxy_error(sp, "First %s supported by server is %s, below warning " + "threshold. Abandoning proxy SSH connection.", + algtype, algname); + return SPR_SW_ABORT("Noninteractive SSH proxy cannot confirm " + "weak crypto primitive"); +} + +static SeatPromptResult sshproxy_confirm_weak_cached_hostkey( + Seat *seat, const char *algname, const char *betteralgs, + void (*callback)(void *ctx, SeatPromptResult result), void *ctx) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + if (sp->clientseat) { + /* + * If we have access to the outer Seat, pass this prompt + * request on to it. + */ + return seat_confirm_weak_cached_hostkey( + wrap(sp->clientseat), algname, betteralgs, callback, ctx); + } + + /* + * Otherwise, behave as if we're in batch mode: take the safest + * option. + */ + sshproxy_error(sp, "First host key type stored for server is %s, below " + "warning threshold. Abandoning proxy SSH connection.", + algname); + return SPR_SW_ABORT("Noninteractive SSH proxy cannot confirm " + "weak cached host key"); +} + +static const SeatDialogPromptDescriptions *sshproxy_prompt_descriptions( + Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + /* If we have a client seat, return their prompt descriptions, so + * that prompts passed on to them will make sense. */ + if (sp->clientseat) + return seat_prompt_descriptions(sp->clientseat); + + /* Otherwise, it doesn't matter what we return, so do the easiest thing. */ + return nullseat_prompt_descriptions(NULL); +} + +static StripCtrlChars *sshproxy_stripctrl_new( + Seat *seat, BinarySink *bs_out, SeatInteractionContext sic) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + if (sp->clientseat) + return seat_stripctrl_new(sp->clientseat, bs_out, sic); + else + return NULL; +} + +static void sshproxy_set_trust_status(Seat *seat, bool trusted) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + if (sp->clientseat) + seat_set_trust_status(sp->clientseat, trusted); +} + +static bool sshproxy_can_set_trust_status(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + return sp->clientseat && seat_can_set_trust_status(sp->clientseat); +} + +static bool sshproxy_verbose(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + return sp->clientseat && seat_verbose(sp->clientseat); +} + +static bool sshproxy_interactive(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + return sp->clientseat && seat_interactive(sp->clientseat); +} + +static const SeatVtable SshProxy_seat_vt = { + .output = sshproxy_output, + .eof = sshproxy_eof, + .sent = sshproxy_sent, + .banner = sshproxy_banner, + .get_userpass_input = sshproxy_get_userpass_input, + .notify_session_started = sshproxy_notify_session_started, + .notify_remote_exit = nullseat_notify_remote_exit, + .notify_remote_disconnect = sshproxy_notify_remote_disconnect, + .connection_fatal = sshproxy_connection_fatal, + .update_specials_menu = nullseat_update_specials_menu, + .get_ttymode = nullseat_get_ttymode, + .set_busy_status = nullseat_set_busy_status, + .confirm_ssh_host_key = sshproxy_confirm_ssh_host_key, + .confirm_weak_crypto_primitive = sshproxy_confirm_weak_crypto_primitive, + .confirm_weak_cached_hostkey = sshproxy_confirm_weak_cached_hostkey, + .prompt_descriptions = sshproxy_prompt_descriptions, + .is_utf8 = nullseat_is_never_utf8, + .echoedit_update = nullseat_echoedit_update, + .get_x_display = nullseat_get_x_display, + .get_windowid = nullseat_get_windowid, + .get_window_pixel_size = nullseat_get_window_pixel_size, + .stripctrl_new = sshproxy_stripctrl_new, + .set_trust_status = sshproxy_set_trust_status, + .can_set_trust_status = sshproxy_can_set_trust_status, + .has_mixed_input_stream = nullseat_has_mixed_input_stream_no, + .verbose = sshproxy_verbose, + .interactive = sshproxy_interactive, + .get_cursor_position = nullseat_get_cursor_position, +}; + +Socket *sshproxy_new_connection(SockAddr *addr, const char *hostname, + int port, bool privport, + bool oobinline, bool nodelay, bool keepalive, + Plug *plug, Conf *clientconf, + Interactor *clientitr) +{ + SshProxy *sp = snew(SshProxy); + memset(sp, 0, sizeof(*sp)); + + sp->sock.vt = &SshProxy_sock_vt; + sp->logpolicy.vt = &SshProxy_logpolicy_vt; + sp->seat.vt = &SshProxy_seat_vt; + sp->plug = plug; + psb_init(&sp->psb); + bufchain_init(&sp->ssh_to_socket); + + sp->addr = addr; + sp->port = port; + + sp->conf = conf_new(); + /* Try to treat proxy_hostname as the title of a saved session. If + * that fails, set up a default Conf of our own treating it as a + * hostname. */ + const char *proxy_hostname = conf_get_str(clientconf, CONF_proxy_host); + if (do_defaults(proxy_hostname, sp->conf)) { + if (!conf_launchable(sp->conf)) { + sp->errmsg = dupprintf("saved session '%s' is not launchable", + proxy_hostname); + return &sp->sock; + } + } else { + do_defaults(NULL, sp->conf); + /* In hostname mode, we default to PROT_SSH. This is more useful than + * the obvious approach of defaulting to the protocol defined in + * Default Settings, because only SSH (ok, and bare ssh-connection) + * can be used for this kind of proxy. */ + conf_set_int(sp->conf, CONF_protocol, PROT_SSH); + conf_set_str(sp->conf, CONF_host, proxy_hostname); + conf_set_int(sp->conf, CONF_port, + conf_get_int(clientconf, CONF_proxy_port)); + } + const char *proxy_username = conf_get_str(clientconf, CONF_proxy_username); + if (*proxy_username) + conf_set_str(sp->conf, CONF_username, proxy_username); + + const char *proxy_password = conf_get_str(clientconf, CONF_proxy_password); + if (*proxy_password) { + sp->proxy_password = dupstr(proxy_password); + sp->got_proxy_password = true; + } + + const struct BackendVtable *backvt = backend_vt_from_proto( + conf_get_int(sp->conf, CONF_protocol)); + + /* + * We don't actually need an _SSH_ session specifically: it's also + * OK to use PROT_SSHCONN, because really, the criterion is + * whether setting CONF_ssh_nc_host will do anything useful. So + * our check is for whether the backend sets the flag promising + * that it does. + */ + if (!backvt || !(backvt->flags & BACKEND_SUPPORTS_NC_HOST)) { + sp->errmsg = dupprintf("saved session '%s' is not an SSH session", + proxy_hostname); + return &sp->sock; + } + + /* + * We also expect that the backend will announce a willingness to + * notify us that the session has started. Any backend providing + * NC_HOST should also provide this. + */ + assert(backvt->flags & BACKEND_NOTIFIES_SESSION_START && + "Backend provides NC_HOST without SESSION_START!"); + + /* + * Turn off SSH features we definitely don't want. It would be + * awkward and counterintuitive to have the proxy SSH connection + * become a connection-sharing upstream (but it's fine to have it + * be a downstream, if that's configured). And we don't want to + * open X forwardings, agent forwardings or (other) port + * forwardings as a side effect of this one operation. + */ + conf_set_bool(sp->conf, CONF_ssh_connection_sharing_upstream, false); + conf_set_bool(sp->conf, CONF_x11_forward, false); + conf_set_bool(sp->conf, CONF_agentfwd, false); + for (const char *subkey; + (subkey = conf_get_str_nthstrkey(sp->conf, CONF_portfwd, 0)) != NULL;) + conf_del_str_str(sp->conf, CONF_portfwd, subkey); + + /* + * We'll only be running one channel through this connection + * (since we've just turned off all the other things we might have + * done with it), so we can configure it as simple. + */ + conf_set_bool(sp->conf, CONF_ssh_simple, true); + + int proxy_type = conf_get_int(clientconf, CONF_proxy_type); + switch (proxy_type) { + case PROXY_SSH_TCPIP: + /* + * Configure the main channel of this SSH session to be a + * direct-tcpip connection to the destination host/port. + */ + conf_set_str(sp->conf, CONF_ssh_nc_host, hostname); + conf_set_int(sp->conf, CONF_ssh_nc_port, port); + break; + + case PROXY_SSH_SUBSYSTEM: + case PROXY_SSH_EXEC: { + Conf *cmd_conf = conf_copy(clientconf); + + /* + * Unlike the Telnet and Local proxy types, we don't use the + * proxy username and password fields in the formatted + * command, because if we use them at all, it's for + * authenticating to the proxy SSH server. + */ + conf_set_str(cmd_conf, CONF_proxy_username, ""); + conf_set_str(cmd_conf, CONF_proxy_password, ""); + + char *cmd = format_telnet_command(sp->addr, sp->port, cmd_conf, NULL); + conf_free(cmd_conf); + + conf_set_str(sp->conf, CONF_remote_cmd, cmd); + sfree(cmd); + + conf_set_bool(sp->conf, CONF_nopty, true); + + if (proxy_type == PROXY_SSH_SUBSYSTEM) + conf_set_bool(sp->conf, CONF_ssh_subsys, true); + + break; + } + + default: + unreachable("bad SSH proxy type"); + } + + /* + * Do the usual normalisation of things in the Conf like a "user@" + * prefix on the hostname field. + */ + prepare_session(sp->conf); + + sp->logctx = log_init(&sp->logpolicy, sp->conf); + + char *error, *realhost; + error = backend_init(backvt, &sp->seat, &sp->backend, sp->logctx, sp->conf, + conf_get_str(sp->conf, CONF_host), + conf_get_int(sp->conf, CONF_port), + &realhost, nodelay, + conf_get_bool(sp->conf, CONF_tcp_keepalives)); + if (error) { + sp->errmsg = dupprintf("unable to open SSH proxy connection: %s", + error); + return &sp->sock; + } + + sfree(realhost); + + /* + * If we've been given an Interactor by the caller, set ourselves + * up to work with it. + */ + if (clientitr) { + sp->clientitr = clientitr; + interactor_set_child(sp->clientitr, sp->backend->interactor); + + sp->clientlp = interactor_logpolicy(clientitr); + + /* + * We can only borrow the client's Seat if our own backend + * will tell us when to give it back. (SSH-based backends + * _should_ do that, but we check the flag here anyway.) + */ + if (backvt->flags & BACKEND_NOTIFIES_SESSION_START) + sp->clientseat = interactor_borrow_seat(clientitr); + } + + return &sp->sock; +} |