diff --git a/include/libssh/callbacks.h b/include/libssh/callbacks.h index 29c1d391..9ab28998 100644 --- a/include/libssh/callbacks.h +++ b/include/libssh/callbacks.h @@ -1074,6 +1074,45 @@ LIBSSH_API int ssh_set_log_callback(ssh_logging_callback cb); */ LIBSSH_API ssh_logging_callback ssh_get_log_callback(void); +/** + * @brief SSH proxyjump before connection callback. Called before calling + * ssh_connect() + * @param session Jump session handler + * @param userdata Userdata to be passed to the callback function. + * + * @return 0 on success, < 0 on error. + */ +typedef int (*ssh_jump_before_connection_callback)(ssh_session session, + void *userdata); + +/** + * @brief SSH proxyjump verify knownhost callback. Verify the host. + * If not specified default function will be used. + * @param session Jump session handler + * @param userdata Userdata to be passed to the callback function. + * + * @return 0 on success, < 0 on error. + */ +typedef int (*ssh_jump_verify_knownhost_callback)(ssh_session session, + void *userdata); + +/** + * @brief SSH proxyjump user authentication callback. Authenticate the user. + * @param session Jump session handler + * @param userdata Userdata to be passed to the callback function. + * + * @return 0 on success, < 0 on error. + */ +typedef int (*ssh_jump_authenticate_callback)(ssh_session session, + void *userdata); + +struct ssh_jump_callbacks_struct { + void *userdata; + ssh_jump_before_connection_callback before_connection; + ssh_jump_verify_knownhost_callback verify_knownhost; + ssh_jump_authenticate_callback authenticate; +}; + #ifdef __cplusplus } #endif diff --git a/include/libssh/config_parser.h b/include/libssh/config_parser.h index 4648614c..f5d1fee8 100644 --- a/include/libssh/config_parser.h +++ b/include/libssh/config_parser.h @@ -30,6 +30,7 @@ extern "C" { #endif +#include "libssh/libssh.h" #include char *ssh_config_get_cmd(char **str); @@ -63,6 +64,21 @@ int ssh_config_parse_uri(const char *tok, char **port, bool ignore_port); +/** + * @brief: Parse the ProxyJump configuration line and if parsing, + * stores the result in the configuration option + * + * @param[in] session The ssh session + * @param[in] s The string to be parsed. + * @param[in] do_parsing Whether to parse or not. + * + * @returns SSH_OK if the provided string is formatted and parsed correctly + * SSH_ERROR on failure + */ +int ssh_config_parse_proxy_jump(ssh_session session, + const char *s, + bool do_parsing); + #ifdef __cplusplus } #endif diff --git a/include/libssh/libssh.h b/include/libssh/libssh.h index afbbbb32..ab6432e7 100644 --- a/include/libssh/libssh.h +++ b/include/libssh/libssh.h @@ -370,53 +370,55 @@ enum ssh_control_master_options_e { }; enum ssh_options_e { - SSH_OPTIONS_HOST, - SSH_OPTIONS_PORT, - SSH_OPTIONS_PORT_STR, - SSH_OPTIONS_FD, - SSH_OPTIONS_USER, - SSH_OPTIONS_SSH_DIR, - SSH_OPTIONS_IDENTITY, - SSH_OPTIONS_ADD_IDENTITY, - SSH_OPTIONS_KNOWNHOSTS, - SSH_OPTIONS_TIMEOUT, - SSH_OPTIONS_TIMEOUT_USEC, - SSH_OPTIONS_SSH1, - SSH_OPTIONS_SSH2, - SSH_OPTIONS_LOG_VERBOSITY, - SSH_OPTIONS_LOG_VERBOSITY_STR, - SSH_OPTIONS_CIPHERS_C_S, - SSH_OPTIONS_CIPHERS_S_C, - SSH_OPTIONS_COMPRESSION_C_S, - SSH_OPTIONS_COMPRESSION_S_C, - SSH_OPTIONS_PROXYCOMMAND, - SSH_OPTIONS_BINDADDR, - SSH_OPTIONS_STRICTHOSTKEYCHECK, - SSH_OPTIONS_COMPRESSION, - SSH_OPTIONS_COMPRESSION_LEVEL, - SSH_OPTIONS_KEY_EXCHANGE, - SSH_OPTIONS_HOSTKEYS, - SSH_OPTIONS_GSSAPI_SERVER_IDENTITY, - SSH_OPTIONS_GSSAPI_CLIENT_IDENTITY, - SSH_OPTIONS_GSSAPI_DELEGATE_CREDENTIALS, - SSH_OPTIONS_HMAC_C_S, - SSH_OPTIONS_HMAC_S_C, - SSH_OPTIONS_PASSWORD_AUTH, - SSH_OPTIONS_PUBKEY_AUTH, - SSH_OPTIONS_KBDINT_AUTH, - SSH_OPTIONS_GSSAPI_AUTH, - SSH_OPTIONS_GLOBAL_KNOWNHOSTS, - SSH_OPTIONS_NODELAY, - SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, - SSH_OPTIONS_PROCESS_CONFIG, - SSH_OPTIONS_REKEY_DATA, - SSH_OPTIONS_REKEY_TIME, - SSH_OPTIONS_RSA_MIN_SIZE, - SSH_OPTIONS_IDENTITY_AGENT, - SSH_OPTIONS_IDENTITIES_ONLY, - SSH_OPTIONS_CONTROL_MASTER, - SSH_OPTIONS_CONTROL_PATH, - SSH_OPTIONS_CERTIFICATE, + SSH_OPTIONS_HOST, + SSH_OPTIONS_PORT, + SSH_OPTIONS_PORT_STR, + SSH_OPTIONS_FD, + SSH_OPTIONS_USER, + SSH_OPTIONS_SSH_DIR, + SSH_OPTIONS_IDENTITY, + SSH_OPTIONS_ADD_IDENTITY, + SSH_OPTIONS_KNOWNHOSTS, + SSH_OPTIONS_TIMEOUT, + SSH_OPTIONS_TIMEOUT_USEC, + SSH_OPTIONS_SSH1, + SSH_OPTIONS_SSH2, + SSH_OPTIONS_LOG_VERBOSITY, + SSH_OPTIONS_LOG_VERBOSITY_STR, + SSH_OPTIONS_CIPHERS_C_S, + SSH_OPTIONS_CIPHERS_S_C, + SSH_OPTIONS_COMPRESSION_C_S, + SSH_OPTIONS_COMPRESSION_S_C, + SSH_OPTIONS_PROXYCOMMAND, + SSH_OPTIONS_BINDADDR, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + SSH_OPTIONS_COMPRESSION, + SSH_OPTIONS_COMPRESSION_LEVEL, + SSH_OPTIONS_KEY_EXCHANGE, + SSH_OPTIONS_HOSTKEYS, + SSH_OPTIONS_GSSAPI_SERVER_IDENTITY, + SSH_OPTIONS_GSSAPI_CLIENT_IDENTITY, + SSH_OPTIONS_GSSAPI_DELEGATE_CREDENTIALS, + SSH_OPTIONS_HMAC_C_S, + SSH_OPTIONS_HMAC_S_C, + SSH_OPTIONS_PASSWORD_AUTH, + SSH_OPTIONS_PUBKEY_AUTH, + SSH_OPTIONS_KBDINT_AUTH, + SSH_OPTIONS_GSSAPI_AUTH, + SSH_OPTIONS_GLOBAL_KNOWNHOSTS, + SSH_OPTIONS_NODELAY, + SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, + SSH_OPTIONS_PROCESS_CONFIG, + SSH_OPTIONS_REKEY_DATA, + SSH_OPTIONS_REKEY_TIME, + SSH_OPTIONS_RSA_MIN_SIZE, + SSH_OPTIONS_IDENTITY_AGENT, + SSH_OPTIONS_IDENTITIES_ONLY, + SSH_OPTIONS_CONTROL_MASTER, + SSH_OPTIONS_CONTROL_PATH, + SSH_OPTIONS_CERTIFICATE, + SSH_OPTIONS_PROXYJUMP, + SSH_OPTIONS_PROXYJUMP_CB_LIST_APPEND, }; enum { diff --git a/include/libssh/misc.h b/include/libssh/misc.h index 272539c5..ab726a0e 100644 --- a/include/libssh/misc.h +++ b/include/libssh/misc.h @@ -34,6 +34,7 @@ #else #include +#include #endif /* _WIN32 */ #ifdef __cplusplus @@ -65,6 +66,12 @@ struct ssh_iterator { const void *data; }; +struct ssh_jump_info_struct { + char *hostname; + char *username; + int port; +}; + struct ssh_timestamp { long seconds; long useconds; @@ -100,6 +107,9 @@ const void *_ssh_list_pop_head(struct ssh_list *list); #define ssh_list_pop_head(type, ssh_list)\ ((type)_ssh_list_pop_head(ssh_list)) +#define SSH_LIST_FREE(x) \ + do { if ((x) != NULL) { ssh_list_free(x); (x) = NULL; } } while(0) + int ssh_make_milliseconds(unsigned long sec, unsigned long usec); void ssh_timestamp_init(struct ssh_timestamp *ts); int ssh_timeout_elapsed(struct ssh_timestamp *ts, int timeout); @@ -123,6 +133,9 @@ ssize_t ssh_writen(int fd, const void *buf, size_t nbytes); int ssh_check_hostname_syntax(const char *hostname); int ssh_check_username_syntax(const char *username); +void ssh_proxyjumps_free(struct ssh_list *proxy_jump_list); +bool ssh_libssh_proxy_jumps(void); + #ifdef __cplusplus } #endif diff --git a/include/libssh/session.h b/include/libssh/session.h index e0404878..aed94072 100644 --- a/include/libssh/session.h +++ b/include/libssh/session.h @@ -140,6 +140,7 @@ struct ssh_session_struct { uint32_t send_seq; uint32_t recv_seq; struct ssh_timestamp last_rekey_time; + bool proxy_root; int connected; /* !=0 when the user got a session handle */ @@ -239,6 +240,8 @@ struct ssh_session_struct { struct ssh_list *identity_non_exp; struct ssh_list *certificate; struct ssh_list *certificate_non_exp; + struct ssh_list *proxy_jumps; + struct ssh_list *proxy_jumps_user_cb; char *username; char *host; char *bindaddr; /* bind the client to an ip addr */ diff --git a/include/libssh/socket.h b/include/libssh/socket.h index 7c011280..50e7db75 100644 --- a/include/libssh/socket.h +++ b/include/libssh/socket.h @@ -41,6 +41,7 @@ int ssh_socket_unix(ssh_socket s, const char *path); void ssh_execute_command(const char *command, socket_t in, socket_t out); int ssh_socket_connect_proxycommand(ssh_socket s, const char *command); #endif +int ssh_socket_connect_proxyjump(ssh_socket s); void ssh_socket_close(ssh_socket s); int ssh_socket_write(ssh_socket s,const void *buffer, uint32_t len); int ssh_socket_is_open(ssh_socket s); diff --git a/src/client.c b/src/client.c index e6451ce2..f4229d89 100644 --- a/src/client.c +++ b/src/client.c @@ -589,6 +589,13 @@ int ssh_connect(ssh_session session) session->session_state = SSH_SESSION_STATE_SOCKET_CONNECTED; ssh_socket_set_fd(session->socket, session->opts.fd); ret = SSH_OK; +#ifndef _WIN32 +#ifdef HAVE_PTHREAD + } else if (ssh_libssh_proxy_jumps() && + ssh_list_count(session->opts.proxy_jumps) != 0) { + ret = ssh_socket_connect_proxyjump(session->socket); +#endif /* HAVE_PTHREAD */ +#endif /* _WIN32 */ } else if (session->opts.ProxyCommand != NULL) { #ifdef WITH_EXEC ret = ssh_socket_connect_proxycommand(session->socket, @@ -758,6 +765,7 @@ ssh_session_set_disconnect_message(ssh_session session, const char *message) return SSH_OK; } +extern int proxy_disconnect; /** * @brief Disconnect from a session (client or server). @@ -780,6 +788,14 @@ ssh_disconnect(ssh_session session) return; } +#ifndef _WIN32 + /* Only send the disconnect to all other threads when the root session calls + * ssh_disconnect() */ + if (session->proxy_root) { + proxy_disconnect = 1; + } +#endif + if (session->disconnect_message == NULL) { session->disconnect_message = strdup("Bye Bye") ; if (session->disconnect_message == NULL) { diff --git a/src/config.c b/src/config.c index 4687cce4..78549a40 100644 --- a/src/config.c +++ b/src/config.c @@ -434,10 +434,18 @@ ssh_match_exec(ssh_session session, const char *command, bool negate) } #endif /* WITH_EXEC */ -/* @brief: Parse the ProxyJump configuration line and if parsing, +/** + * @brief: Parse the ProxyJump configuration line and if parsing, * stores the result in the configuration option + * + * @param[in] session The ssh session + * @param[in] s The string to be parsed. + * @param[in] do_parsing Whether to parse or not. + * + * @returns SSH_OK if the provided string is formatted and parsed correctly + * SSH_ERROR on failure */ -static int +int ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing) { char *c = NULL, *cp = NULL, *endp = NULL; @@ -446,12 +454,16 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing) char *port = NULL; char *next = NULL; int cmp, rv = SSH_ERROR; + struct ssh_jump_info_struct *jump_host = NULL; bool parse_entry = do_parsing; + bool libssh_proxy_jump = ssh_libssh_proxy_jumps(); /* Special value none disables the proxy */ cmp = strcasecmp(s, "none"); - if (cmp == 0 && do_parsing) { - ssh_options_set(session, SSH_OPTIONS_PROXYCOMMAND, s); + if (cmp == 0) { + if (!libssh_proxy_jump && do_parsing) { + ssh_options_set(session, SSH_OPTIONS_PROXYCOMMAND, s); + } return SSH_OK; } @@ -469,25 +481,65 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing) /* Split out the token */ *endp = '\0'; } - if (parse_entry) { + if (parse_entry && libssh_proxy_jump) { + jump_host = calloc(1, sizeof(struct ssh_jump_info_struct)); + if (jump_host == NULL) { + ssh_set_error_oom(session); + rv = SSH_ERROR; + goto out; + } + + rv = ssh_config_parse_uri(cp, + &jump_host->username, + &jump_host->hostname, + &port, + false); + if (rv != SSH_OK) { + ssh_set_error_invalid(session); + SAFE_FREE(jump_host); + goto out; + } + if (port == NULL) { + jump_host->port = 22; + } else { + jump_host->port = strtol(port, NULL, 10); + SAFE_FREE(port); + } + + /* Prepend because we will recursively proxy jump */ + rv = ssh_list_prepend(session->opts.proxy_jumps, jump_host); + if (rv != SSH_OK) { + ssh_set_error_oom(session); + SAFE_FREE(jump_host); + goto out; + } + } else if (parse_entry) { /* We actually care only about the first item */ rv = ssh_config_parse_uri(cp, &username, &hostname, &port, false); + if (rv != SSH_OK) { + ssh_set_error_invalid(session); + goto out; + } /* The rest of the list needs to be passed on */ if (endp != NULL) { next = strdup(endp + 1); if (next == NULL) { ssh_set_error_oom(session); rv = SSH_ERROR; + goto out; } } } else { /* The rest is just sanity-checked to avoid failures later */ rv = ssh_config_parse_uri(cp, NULL, NULL, NULL, false); + if (rv != SSH_OK) { + ssh_set_error_invalid(session); + goto out; + } } - if (rv != SSH_OK) { - goto out; + if (!libssh_proxy_jump) { + parse_entry = 0; } - parse_entry = 0; if (endp != NULL) { cp = endp + 1; } else { @@ -495,7 +547,7 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing) } } while (cp != NULL); - if (hostname != NULL && do_parsing) { + if (!libssh_proxy_jump && hostname != NULL && do_parsing) { char com[512] = {0}; rv = snprintf(com, sizeof(com), "ssh%s%s%s%s%s%s -W '[%%h]:%%p' %s", @@ -511,11 +563,19 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing) rv = SSH_ERROR; goto out; } - ssh_options_set(session, SSH_OPTIONS_PROXYCOMMAND, com); + rv = ssh_options_set(session, SSH_OPTIONS_PROXYCOMMAND, com); + if (rv != SSH_OK) { + ssh_set_error_oom(session); + goto out; + } } + rv = SSH_OK; out: + if (rv != SSH_OK) { + ssh_proxyjumps_free(session->opts.proxy_jumps); + } SAFE_FREE(username); SAFE_FREE(hostname); SAFE_FREE(port); @@ -1063,7 +1123,8 @@ ssh_config_parse_line(ssh_session session, return -1; } /* We share the seen value with the ProxyCommand */ - rv = ssh_config_parse_proxy_jump(session, p, + rv = ssh_config_parse_proxy_jump(session, + p, (*parsing && !seen[SOC_PROXYCOMMAND])); if (rv != SSH_OK) { SAFE_FREE(x); diff --git a/src/misc.c b/src/misc.c index fb53d184..8f9e22b0 100644 --- a/src/misc.c +++ b/src/misc.c @@ -2200,4 +2200,46 @@ int ssh_check_username_syntax(const char *username) return SSH_OK; } + +/** + * @brief Free proxy jump list + * + * Frees everything in a proxy jump list, but doesn't free the ssh_list + * + * @param proxy_jump_list + * + */ +void +ssh_proxyjumps_free(struct ssh_list *proxy_jump_list) +{ + struct ssh_jump_info_struct *jump = NULL; + + for (jump = + ssh_list_pop_head(struct ssh_jump_info_struct *, proxy_jump_list); + jump != NULL; + jump = ssh_list_pop_head(struct ssh_jump_info_struct *, + proxy_jump_list)) { + SAFE_FREE(jump->hostname); + SAFE_FREE(jump->username); + SAFE_FREE(jump); + } +} + +/** + * @brief Check if libssh proxy jumps is enabled + * + * If env variable OPENSSH_PROXYJUMP is set to 1 then proxyjump will be + * through the OpenSSH binary. + * + * @return false if OPENSSH_PROXYJUMP=1 + * true otherwise + */ +bool +ssh_libssh_proxy_jumps(void) +{ + const char *t = getenv("OPENSSH_PROXYJUMP"); + + return !(t != NULL && t[0] == '1'); +} + /** @} */ diff --git a/src/options.c b/src/options.c index 68d2f075..015fe429 100644 --- a/src/options.c +++ b/src/options.c @@ -512,6 +512,20 @@ int ssh_options_set_algo(ssh_session session, * Set the command to be executed in order to connect to * server (const char *). * + * - SSH_OPTIONS_PROXYJUMP: + * Set the comma separated jump hosts in order to connect to + * server (const char *). Set to "none" to disable. + * Example: + * "alice@127.0.0.1:5555,bob@127.0.0.2" + * + * If environment variable OPENSSH_PROXYJUMP is set to 1 then proxyjump will be + * handled by the OpenSSH binary. + * + * - SSH_OPTIONS_PROXYJUMP_CB_LIST_APPEND: + * Append the callbacks struct for a jump in order of + * SSH_OPTIONS_PROXYJUMP. Append as many times + * as the number of jumps (struct ssh_jump_callbacks_struct *). + * * - SSH_OPTIONS_GSSAPI_SERVER_IDENTITY * Set it to specify the GSSAPI server identity that libssh * should expect when connecting to the server (const char *). @@ -637,6 +651,7 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type, unsigned int u; int rc; char **wanted_methods = session->opts.wanted_methods; + struct ssh_jump_callbacks_struct *j = NULL; if (session == NULL) { return -1; @@ -1123,6 +1138,32 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type, } } break; + case SSH_OPTIONS_PROXYJUMP: + v = value; + if (v == NULL || v[0] == '\0') { + ssh_set_error_invalid(session); + return -1; + } else { + ssh_proxyjumps_free(session->opts.proxy_jumps); + rc = ssh_config_parse_proxy_jump(session, v, true); + if (rc != SSH_OK) { + return SSH_ERROR; + } + } + break; + case SSH_OPTIONS_PROXYJUMP_CB_LIST_APPEND: + j = (struct ssh_jump_callbacks_struct *)value; + if (j == NULL) { + ssh_set_error_invalid(session); + return -1; + } else { + rc = ssh_list_prepend(session->opts.proxy_jumps_user_cb, j); + if (rc != SSH_OK) { + ssh_set_error_oom(session); + return SSH_ERROR; + } + } + break; case SSH_OPTIONS_GSSAPI_SERVER_IDENTITY: v = value; if (v == NULL || v[0] == '\0') { diff --git a/src/session.c b/src/session.c index 9750b964..e07f2809 100644 --- a/src/session.c +++ b/src/session.c @@ -96,6 +96,7 @@ ssh_session ssh_new(void) session->auth.supported_methods = 0; ssh_set_blocking(session, 1); session->maxchannel = FIRST_CHANNEL; + session->proxy_root = true; session->agent = ssh_agent_new(session); if (session->agent == NULL) { @@ -138,6 +139,16 @@ ssh_session ssh_new(void) /* the default certificates are loaded automatically from the default * identities later */ + session->opts.proxy_jumps = ssh_list_new(); + if (session->opts.proxy_jumps == NULL) { + goto err; + } + + session->opts.proxy_jumps_user_cb = ssh_list_new(); + if (session->opts.proxy_jumps_user_cb == NULL) { + goto err; + } + id = strdup("%d/id_ed25519"); if (id == NULL) { goto err; @@ -321,6 +332,10 @@ void ssh_free(ssh_session session) ssh_list_free(session->opts.certificate_non_exp); } + ssh_proxyjumps_free(session->opts.proxy_jumps); + SSH_LIST_FREE(session->opts.proxy_jumps); + SSH_LIST_FREE(session->opts.proxy_jumps_user_cb); + while ((b = ssh_list_pop_head(struct ssh_buffer_struct *, session->out_queue)) != NULL) { SSH_BUFFER_FREE(b); diff --git a/src/socket.c b/src/socket.c index dc76b788..deb9ab75 100644 --- a/src/socket.c +++ b/src/socket.c @@ -44,6 +44,9 @@ struct sockaddr_un { #include #include #include +#ifdef HAVE_PTHREAD +#include +#endif #endif /* _WIN32 */ #include "libssh/priv.h" @@ -90,6 +93,15 @@ struct ssh_socket_struct { #endif }; +#ifdef HAVE_PTHREAD +struct jump_thread_data_struct { + ssh_session session; + socket_t fd; +}; + +int proxy_disconnect = 0; +#endif /* HAVE_PTHREAD */ + static int sockets_initialized = 0; static ssize_t ssh_socket_unbuffered_read(ssh_socket s, @@ -987,4 +999,295 @@ ssh_socket_connect_proxycommand(ssh_socket s, const char *command) return SSH_OK; } #endif /* WITH_EXEC */ + +#ifndef _WIN32 +#ifdef HAVE_PTHREAD +static int +verify_knownhost(ssh_session session) +{ + enum ssh_known_hosts_e state; + + state = ssh_session_is_known_server(session); + + switch (state) { + case SSH_KNOWN_HOSTS_OK: + break; /* ok */ + default: + SSH_LOG(SSH_LOG_WARN, "Couldn't verify knownhost during proxyjump."); + return SSH_ERROR; + } + + return SSH_OK; +} + +static void * +jump_thread_func(void *arg) +{ + struct jump_thread_data_struct *jump_thread_data = NULL; + struct ssh_jump_info_struct *jis = NULL; + struct ssh_jump_callbacks_struct *cb = NULL; + ssh_session jump_session = NULL; + ssh_channel caa = NULL; + int rc; + ssh_event event = NULL; + ssh_connector connector_in = NULL, connector_out = NULL; + ssh_session session = NULL; + int next_port; + char *next_hostname = NULL; + + jump_thread_data = (struct jump_thread_data_struct *)arg; + session = jump_thread_data->session; + + next_port = session->opts.port; + next_hostname = strdup(session->opts.host); + + jump_session = ssh_new(); + if (jump_session == NULL) { + goto exit; + } + + jump_session->proxy_root = false; + /* Reset the global variable if it was previously 1 */ + if (session->proxy_root) { + proxy_disconnect = 0; + } + + for (jis = ssh_list_pop_head(struct ssh_jump_info_struct *, + session->opts.proxy_jumps); + jis != NULL; + jis = ssh_list_pop_head(struct ssh_jump_info_struct *, + session->opts.proxy_jumps)) { + rc = ssh_list_append(jump_session->opts.proxy_jumps, jis); + if (rc != SSH_OK) { + ssh_set_error_oom(session); + goto exit; + } + } + for (jis = + ssh_list_pop_head(struct ssh_jump_info_struct *, + session->opts.proxy_jumps_user_cb); + jis != NULL; + jis = ssh_list_pop_head(struct ssh_jump_info_struct *, + session->opts.proxy_jumps_user_cb)) { + rc = ssh_list_append(jump_session->opts.proxy_jumps_user_cb, jis); + if (rc != SSH_OK) { + ssh_set_error_oom(session); + goto exit; + } + } + + ssh_options_set(jump_session, + SSH_OPTIONS_LOG_VERBOSITY, + &session->common.log_verbosity); + + /* Pop the information about the current jump */ + jis = ssh_list_pop_head(struct ssh_jump_info_struct *, + jump_session->opts.proxy_jumps); + + ssh_options_set(jump_session, SSH_OPTIONS_HOST, jis->hostname); + ssh_options_set(jump_session, SSH_OPTIONS_USER, jis->username); + ssh_options_set(jump_session, SSH_OPTIONS_PORT, &jis->port); + + /* Pop the callbacks for the current jump */ + cb = ssh_list_pop_head(struct ssh_jump_callbacks_struct *, + jump_session->opts.proxy_jumps_user_cb); + + if (cb != NULL) { + rc = cb->before_connection(jump_session, cb->userdata); + if (rc != SSH_OK) { + SSH_LOG(SSH_LOG_WARN, "%s", ssh_get_error(jump_session)); + goto exit; + } + } + + /* If there are more jumps then this will make a new thread and call the + * current function again, until there are no jumps. When there are no jumps + * it connects normally. */ + rc = ssh_connect(jump_session); + if (rc != SSH_OK) { + SSH_LOG(SSH_LOG_WARN, "%s", ssh_get_error(jump_session)); + goto exit; + } + + /* Use the callback or default implementation for verifying knownhost */ + if (cb != NULL && cb->verify_knownhost != NULL) { + rc = cb->verify_knownhost(jump_session, cb->userdata); + } else { + rc = verify_knownhost(jump_session); + } + if (rc != SSH_OK) { + goto exit; + } + + /* Use the callback or publickey method to authenticate */ + if (cb != NULL && cb->authenticate != NULL) { + rc = cb->authenticate(jump_session, cb->userdata); + } else { + rc = ssh_userauth_publickey_auto(jump_session, NULL, NULL); + } + if (rc != SSH_OK) { + SSH_LOG(SSH_LOG_WARN, "%s", ssh_get_error(jump_session)); + goto exit; + } + + caa = ssh_channel_new(jump_session); + if (caa == NULL) { + goto exit; + } + /* The origin hostname and port are set to match OpenSSH implementation + * they are only used for logging on the server */ + rc = ssh_channel_open_forward(caa, + next_hostname, + next_port, + "127.0.0.1", + 65535); + if (rc != SSH_OK) { + SSH_LOG(SSH_LOG_WARN, + "Error opening port forwarding channel: %s", + ssh_get_error(jump_session)); + goto exit; + } + + event = ssh_event_new(); + if (event == NULL) { + goto exit; + } + + connector_in = ssh_connector_new(jump_session); + if (connector_in == NULL) { + goto exit; + } + ssh_connector_set_out_channel(connector_in, caa, SSH_CONNECTOR_STDINOUT); + ssh_connector_set_in_fd(connector_in, jump_thread_data->fd); + ssh_event_add_connector(event, connector_in); + + connector_out = ssh_connector_new(jump_session); + if (connector_out == NULL) { + goto exit; + } + ssh_connector_set_out_fd(connector_out, jump_thread_data->fd); + ssh_connector_set_in_channel(connector_out, caa, SSH_CONNECTOR_STDINOUT); + ssh_event_add_connector(event, connector_out); + + while (ssh_channel_is_open(caa)) { + if (proxy_disconnect == 1) { + break; + } + rc = ssh_event_dopoll(event, 60000); + if (rc == SSH_ERROR) { + SSH_LOG(SSH_LOG_WARN, + "Error in ssh_event_dopoll() during proxy jump"); + break; + } + } + +exit: + if (connector_in != NULL) { + ssh_event_remove_connector(event, connector_in); + ssh_connector_free(connector_in); + } + if (connector_out != NULL) { + ssh_event_remove_connector(event, connector_out); + ssh_connector_free(connector_out); + } + SAFE_FREE(next_hostname); + if (jis != NULL) { + SAFE_FREE(jis->hostname); + SAFE_FREE(jis->username); + } + SAFE_FREE(jis); + + ssh_disconnect(jump_session); + ssh_event_free(event); + ssh_free(jump_session); + + SAFE_FREE(jump_thread_data); + + pthread_exit(NULL); +} + +int +ssh_socket_connect_proxyjump(ssh_socket s) +{ + ssh_poll_handle h = NULL; + int rc; + pthread_t jump_thread; + struct jump_thread_data_struct *jump_thread_data = NULL; + socket_t pair[2]; + + if (s->state != SSH_SOCKET_NONE) { + ssh_set_error( + s->session, + SSH_FATAL, + "ssh_socket_connect_proxyjump called on socket not unconnected"); + return SSH_ERROR; + } + + jump_thread_data = calloc(1, sizeof(struct jump_thread_data_struct)); + if (jump_thread_data == NULL) { + ssh_set_error_oom(s->session); + return SSH_ERROR; + } + + rc = socketpair(PF_UNIX, SOCK_STREAM, 0, pair); + if (rc == -1) { + char err_msg[SSH_ERRNO_MSG_MAX] = {0}; + + ssh_set_error(s->session, + SSH_FATAL, + "Creating socket pair failed: %s", + ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX)); + SAFE_FREE(jump_thread_data); + return SSH_ERROR; + } + + jump_thread_data->session = s->session; + jump_thread_data->fd = pair[0]; + + rc = pthread_create(&jump_thread, NULL, jump_thread_func, jump_thread_data); + if (rc != 0) { + char err_msg[SSH_ERRNO_MSG_MAX] = {0}; + + ssh_set_error(s->session, + SSH_FATAL, + "Creating new thread failed: %s", + ssh_strerror(rc, err_msg, SSH_ERRNO_MSG_MAX)); + SAFE_FREE(jump_thread_data); + return SSH_ERROR; + } + rc = pthread_detach(jump_thread); + if (rc != 0) { + char err_msg[SSH_ERRNO_MSG_MAX] = {0}; + + ssh_set_error(s->session, + SSH_FATAL, + "Failed to detach thread: %s", + ssh_strerror(rc, err_msg, SSH_ERRNO_MSG_MAX)); + SAFE_FREE(jump_thread_data); + return SSH_ERROR; + } + + SSH_LOG(SSH_LOG_DEBUG, + "ProxyJump connection pipe: [%d,%d]", + pair[0], + pair[1]); + ssh_socket_set_fd(s, pair[1]); + s->fd_is_socket = 1; + h = ssh_socket_get_poll_handle(s); + if (h == NULL) { + return SSH_ERROR; + } + ssh_socket_set_connected(s, h); + if (s->callbacks && s->callbacks->connected) { + s->callbacks->connected(SSH_SOCKET_CONNECTED_OK, + 0, + s->callbacks->userdata); + } + + return SSH_OK; +} + +#endif /* HAVE_PTHREAD */ + +#endif /* _WIN32 */ /** @} */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8165c308..23d71195 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,12 @@ set(TORTURE_LINK_LIBRARIES ${CMOCKA_LIBRARY} ssh::static) +if (NOT WIN32) + set(TORTURE_LINK_LIBRARIES + ${TORTURE_LINK_LIBRARIES} + pthread) +endif(NOT WIN32) + # create test library add_library(${TORTURE_LIBRARY} STATIC @@ -257,7 +263,7 @@ if (CLIENT_TESTING OR SERVER_TESTING) # ssh_ping add_executable(ssh_ping ssh_ping.c) target_compile_options(ssh_ping PRIVATE ${DEFAULT_C_COMPILE_FLAGS}) - target_link_libraries(ssh_ping ssh::static) + target_link_libraries(ssh_ping ssh::static pthread) # homedir will be used in passwd set(HOMEDIR ${CMAKE_CURRENT_BINARY_DIR}/home) diff --git a/tests/benchmarks/CMakeLists.txt b/tests/benchmarks/CMakeLists.txt index 4074b8f4..c7b245a5 100644 --- a/tests/benchmarks/CMakeLists.txt +++ b/tests/benchmarks/CMakeLists.txt @@ -14,4 +14,4 @@ include_directories(${libssh_BINARY_DIR}) add_executable(benchmarks ${benchmarks_SRCS}) -target_link_libraries(benchmarks ssh::static) +target_link_libraries(benchmarks ssh::static pthread) diff --git a/tests/client/CMakeLists.txt b/tests/client/CMakeLists.txt index b285ec60..44b82964 100755 --- a/tests/client/CMakeLists.txt +++ b/tests/client/CMakeLists.txt @@ -33,6 +33,12 @@ if (WITH_PKCS11_URI) torture_auth_pkcs11) endif() +if (HAVE_PTHREAD) + set(LIBSSH_CLIENT_TESTS + ${LIBSSH_CLIENT_TESTS} + torture_proxyjump) +endif() + if (DEFAULT_C_NO_DEPRECATION_FLAGS) set_source_files_properties(torture_knownhosts.c PROPERTIES diff --git a/tests/client/torture_proxycommand.c b/tests/client/torture_proxycommand.c index 1d3798a5..232080eb 100644 --- a/tests/client/torture_proxycommand.c +++ b/tests/client/torture_proxycommand.c @@ -93,7 +93,7 @@ static void torture_options_set_proxycommand(void **state) #ifdef WITH_EXEC assert_ssh_return_code(session, rc); fd = ssh_get_fd(session); - assert_true(fd != SSH_INVALID_SOCKET); + assert_int_not_equal(fd, SSH_INVALID_SOCKET); rc = fcntl(fd, F_GETFL); assert_int_equal(rc & O_RDWR, O_RDWR); #else @@ -145,7 +145,7 @@ static void torture_options_set_proxycommand_ssh(void **state) #ifdef WITH_EXEC assert_ssh_return_code(session, rc); fd = ssh_get_fd(session); - assert_true(fd != SSH_INVALID_SOCKET); + assert_int_not_equal(fd, SSH_INVALID_SOCKET); rc = fcntl(fd, F_GETFL); assert_int_equal(rc & O_RDWR, O_RDWR); #else @@ -176,7 +176,7 @@ static void torture_options_set_proxycommand_ssh_stderr(void **state) #ifdef WITH_EXEC assert_ssh_return_code(session, rc); fd = ssh_get_fd(session); - assert_true(fd != SSH_INVALID_SOCKET); + assert_int_not_equal(fd, SSH_INVALID_SOCKET); rc = fcntl(fd, F_GETFL); assert_int_equal(rc & O_RDWR, O_RDWR); #else diff --git a/tests/client/torture_proxyjump.c b/tests/client/torture_proxyjump.c new file mode 100644 index 00000000..6e4f88c0 --- /dev/null +++ b/tests/client/torture_proxyjump.c @@ -0,0 +1,222 @@ +#include "config.h" + +#define LIBSSH_STATIC + +#include "torture.h" +#include + +#include +#include +#include + +static int +sshd_setup(void **state) +{ + torture_setup_sshd_server(state, false); + + return 0; +} + +static int +sshd_teardown(void **state) +{ + torture_teardown_sshd_server(state); + + return 0; +} + +static int +session_setup(void **state) +{ + struct torture_state *s = *state; + int verbosity = torture_libssh_verbosity(); + struct passwd *pwd = NULL; + int rc; + bool b = false; + + pwd = getpwnam("bob"); + assert_non_null(pwd); + + rc = setuid(pwd->pw_uid); + assert_return_code(rc, errno); + + s->ssh.session = ssh_new(); + assert_non_null(s->ssh.session); + + rc = ssh_options_set(s->ssh.session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); + assert_ssh_return_code(s->ssh.session, rc); + rc = ssh_options_set(s->ssh.session, SSH_OPTIONS_HOST, TORTURE_SSH_SERVER); + assert_ssh_return_code(s->ssh.session, rc); + rc = ssh_options_set(s->ssh.session, + SSH_OPTIONS_USER, + TORTURE_SSH_USER_ALICE); + assert_ssh_return_code(s->ssh.session, rc); + + /* Make sure no other configuration options from system will get used */ + rc = ssh_options_set(s->ssh.session, SSH_OPTIONS_PROCESS_CONFIG, &b); + assert_ssh_return_code(s->ssh.session, rc); + + unsetenv("SSH_AUTH_SOCK"); + unsetenv("SSH_AGENT_PID"); + + return 0; +} + +static int +session_teardown(void **state) +{ + struct torture_state *s = *state; + + ssh_disconnect(s->ssh.session); + ssh_free(s->ssh.session); + + return 0; +} + +static void +torture_proxyjump_single_jump(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char proxyjump_buf[500] = {0}; + const char *address = torture_server_address(AF_INET); + int rc; + socket_t fd; + + rc = snprintf(proxyjump_buf, sizeof(proxyjump_buf), "alice@%s:22", address); + if (rc < 0 || rc >= (int)sizeof(proxyjump_buf)) { + fail_msg("snprintf failed"); + } + rc = ssh_options_set(session, SSH_OPTIONS_PROXYJUMP, proxyjump_buf); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + fd = ssh_get_fd(session); + assert_int_not_equal(fd, SSH_INVALID_SOCKET); + + rc = fcntl(fd, F_GETFL); + assert_int_equal(rc & O_RDWR, O_RDWR); + + rc = ssh_userauth_publickey_auto(session, NULL, NULL); + assert_int_equal(rc, SSH_AUTH_SUCCESS); +} + +static int +before_connection(ssh_session jump_session, void *user) +{ + (void)jump_session; + (void)user; + + return 0; +} + +static int +verify_knownhost(ssh_session jump_session, void *user) +{ + (void)jump_session; + (void)user; + + return 0; +} + +static int +authenticate(ssh_session jump_session, void *user) +{ + (void)user; + + return ssh_userauth_publickey_auto(jump_session, NULL, NULL); +} + +static void +torture_proxyjump_multiple_jump(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char proxyjump_buf[500] = {0}; + const char *address = torture_server_address(AF_INET); + int rc; + socket_t fd; + + struct ssh_jump_callbacks_struct c = { + .before_connection = before_connection, + .verify_knownhost = verify_knownhost, + .authenticate = authenticate + }; + + rc = snprintf(proxyjump_buf, + sizeof(proxyjump_buf), + "alice@%s:22,alice@%s:22", + address, + address); + if (rc < 0 || rc >= (int)sizeof(proxyjump_buf)) { + fail_msg("snprintf failed"); + } + rc = ssh_options_set(session, SSH_OPTIONS_PROXYJUMP, proxyjump_buf); + assert_ssh_return_code(session, rc); + rc = ssh_options_set(session, SSH_OPTIONS_PROXYJUMP_CB_LIST_APPEND, &c); + assert_ssh_return_code(session, rc); + rc = ssh_options_set(session, SSH_OPTIONS_PROXYJUMP_CB_LIST_APPEND, &c); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + fd = ssh_get_fd(session); + assert_int_not_equal(fd, SSH_INVALID_SOCKET); + + rc = fcntl(fd, F_GETFL); + assert_int_equal(rc & O_RDWR, O_RDWR); + + rc = ssh_userauth_publickey_auto(session, NULL, NULL); + assert_int_equal(rc, SSH_AUTH_SUCCESS); +} + +static void +torture_proxyjump_invalid_jump(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char proxyjump_buf[500] = {0}; + const char *address = torture_server_address(AF_INET); + int rc; + + rc = snprintf(proxyjump_buf, + sizeof(proxyjump_buf), + "doesnotexist@%s:54", + address); + if (rc < 0 || rc >= (int)sizeof(proxyjump_buf)) { + fail_msg("snprintf failed"); + } + rc = ssh_options_set(session, SSH_OPTIONS_PROXYJUMP, proxyjump_buf); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code_equal(session, rc, SSH_ERROR); +} + +int +torture_run_tests(void) +{ + int rc; + struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(torture_proxyjump_single_jump, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown(torture_proxyjump_multiple_jump, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown(torture_proxyjump_invalid_jump, + session_setup, + session_teardown), + }; + + ssh_init(); + + torture_filter_tests(tests); + rc = cmocka_run_group_tests(tests, sshd_setup, sshd_teardown); + ssh_finalize(); + + return rc; +} diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index 40a6eb13..5bdedc4d 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -4,7 +4,7 @@ macro(fuzzer name) add_executable(${name} ${name}.c) target_link_libraries(${name} PRIVATE - ssh::static) + ssh::static pthread) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set_target_properties(${name} PROPERTIES diff --git a/tests/torture.c b/tests/torture.c index 63bc4a81..a361d415 100644 --- a/tests/torture.c +++ b/tests/torture.c @@ -1653,6 +1653,9 @@ void torture_write_file(const char *filename, const char *data){ void torture_reset_config(ssh_session session) { memset(session->opts.options_seen, 0, sizeof(session->opts.options_seen)); + if (ssh_libssh_proxy_jumps()) { + ssh_proxyjumps_free(session->opts.proxy_jumps); + } } void torture_unsetenv(const char *variable) diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c index 004e503e..4d8d8e44 100644 --- a/tests/unittests/torture_config.c +++ b/tests/unittests/torture_config.c @@ -14,6 +14,7 @@ #include "match.c" #include "config.c" #include "libssh/socket.h" +#include "libssh/misc.h" extern LIBSSH_THREAD int ssh_log_level; @@ -691,6 +692,34 @@ static void torture_config_auth_methods_string(void **state) torture_config_auth_methods(state, NULL, LIBSSH_TESTCONFIG_STRING8); } +/** + * @brief Helper for checking hostname, username and port of ssh_jump_info_struct + */ +static void +helper_proxy_jump_check(struct ssh_iterator *jump, + const char *hostname, + const char *username, + const char *port) +{ + struct ssh_jump_info_struct *jis = + ssh_iterator_value(struct ssh_jump_info_struct *, jump); + + assert_string_equal(jis->hostname, hostname); + + if (username != NULL) { + assert_string_equal(jis->username, username); + } else { + assert_null(jis->username); + } + + if (port != NULL) { + int iport = strtol(port, NULL, 10); + assert_int_equal(jis->port, iport); + } else { + assert_int_equal(jis->port, 22); + } +} + /** * @brief Verify the configuration parser does not choke on unknown * or unsupported configuration options @@ -702,15 +731,18 @@ static void torture_config_unknown(void **state, int ret = 0; /* test corner cases */ + /* Without libssh proxy jump */ + torture_setenv("OPENSSH_PROXYJUMP", "1"); _parse_config(session, file, string, SSH_OK); assert_string_equal(session->opts.ProxyCommand, - "ssh -W '[%h]:%p' many-spaces.com"); + "ssh -W '[%h]:%p' many-spaces.com"); assert_string_equal(session->opts.host, "equal.sign"); ret = ssh_config_parse_file(session, "/etc/ssh/ssh_config"); assert_true(ret == 0); ret = ssh_config_parse_file(session, GLOBAL_CLIENT_CONFIG); assert_true(ret == 0); + torture_unsetenv("OPENSSH_PROXYJUMP"); } /** @@ -994,8 +1026,89 @@ static void torture_config_proxyjump(void **state, const char *file, const char *string) { ssh_session session = *state; + const char *config; + + /* Tests for libssh based proxyjump */ + /* Simplest version with just a hostname */ + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "simple"); + _parse_config(session, file, string, SSH_OK); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "jumpbox", + NULL, + NULL); + + /* With username */ + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "user"); + _parse_config(session, file, string, SSH_OK); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "jumpbox", + "user", + NULL); + + /* With port */ + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "port"); + _parse_config(session, file, string, SSH_OK); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "jumpbox", + NULL, + "2222"); + + /* Two step jump */ + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "two-step"); + _parse_config(session, file, string, SSH_OK); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "second", + "u2", + "33"); + helper_proxy_jump_check(session->opts.proxy_jumps->root->next, + "first", + "u1", + "222"); + + /* none */ + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "none"); + _parse_config(session, file, string, SSH_OK); + assert_int_equal(ssh_list_count(session->opts.proxy_jumps), 0); + + /* If also ProxyCommand is specified, the first is applied */ + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "only-command"); + _parse_config(session, file, string, SSH_OK); + assert_string_equal(session->opts.ProxyCommand, PROXYCMD); + assert_int_equal(ssh_list_count(session->opts.proxy_jumps), 0); + + /* If also ProxyCommand is specified, the first is applied */ + torture_reset_config(session); + SAFE_FREE(session->opts.ProxyCommand); + ssh_options_set(session, SSH_OPTIONS_HOST, "only-jump"); + _parse_config(session, file, string, SSH_OK); + assert_null(session->opts.ProxyCommand); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "jumpbox", + NULL, + NULL); + + /* IPv6 address */ + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "ipv6"); + _parse_config(session, file, string, SSH_OK); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "2620:52:0::fed", + NULL, + NULL); + + torture_reset_config(session); + + /* Tests for proxycommand based proxyjump */ + torture_setenv("OPENSSH_PROXYJUMP", "1"); + /* Simplest version with just a hostname */ torture_reset_config(session); ssh_options_set(session, SSH_OPTIONS_HOST, "simple"); @@ -1049,6 +1162,7 @@ static void torture_config_proxyjump(void **state, assert_string_equal(session->opts.ProxyCommand, "ssh -W '[%h]:%p' 2620:52:0::fed"); + /* Multiple @ is allowed in second jump */ config = "Host allowed-hostname\n" "\tProxyJump localhost,user@principal.com@jumpbox:22\n"; @@ -1076,8 +1190,44 @@ static void torture_config_proxyjump(void **state, _parse_config(session, file, string, SSH_OK); assert_string_equal(session->opts.ProxyCommand, "ssh -l user@principal.com -p 22 -W '[%h]:%p' jumpbox"); + torture_unsetenv("OPENSSH_PROXYJUMP"); + + /* Tests for libssh based proxyjump */ + /* Multiple @ is allowed in second jump */ + config = "Host allowed-hostname\n" + "\tProxyJump localhost,user@principal.com@jumpbox:22\n"; + if (file != NULL) { + torture_write_file(file, config); + } else { + string = config; + } + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "allowed-hostname"); + _parse_config(session, file, string, SSH_OK); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "jumpbox", + "user@principal.com", + "22"); + + /* Multiple @ is allowed */ + config = "Host allowed-hostname\n" + "\tProxyJump user@principal.com@jumpbox:22\n"; + if (file != NULL) { + torture_write_file(file, config); + } else { + string = config; + } + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "allowed-hostname"); + _parse_config(session, file, string, SSH_OK); + helper_proxy_jump_check(session->opts.proxy_jumps->root, + "jumpbox", + "user@principal.com", + "22"); + torture_reset_config(session); /* In this part, we try various other config files and strings. */ + torture_setenv("OPENSSH_PROXYJUMP", "1"); /* Try to create some invalid configurations */ /* Non-numeric port */ @@ -1223,6 +1373,8 @@ static void torture_config_proxyjump(void **state, torture_reset_config(session); ssh_options_set(session, SSH_OPTIONS_HOST, "no-port"); _parse_config(session, file, string, SSH_ERROR); + + torture_unsetenv("OPENSSH_PROXYJUMP"); } /**