1
0
mirror of https://git.libssh.org/projects/libssh.git synced 2025-07-31 00:03:07 +03:00

feat: implement proxy jump using libssh

tests: modify proxyjump tests to check for ssh_jump_info_struct

tests: add proxyjump functionality test

feat: add SSH_OPTIONS_PROXYJUMP

tests: proxyjump, check authentication

fix: ssh_socket_connect_proxyjump add exit label to exit on error

feat: implement io forwarding using pthread

feat: proxyjump: use threading instead of forking

feat: proxyjump: cancel forwarding threads on ssh_disconnect

fix: proxyjump remove ProxyJump bool and put pthread ifdefs

feat: use ssh_event for io forwarding instead of threads

reformat: tests to use assert_int_not_equal

fix: link to pthread

refactor: make function to free proxy jump list

docs: add comment for proxy jump channel

feat: add env variable to enable libssh proxy jump

feat: open channel for proxyjump like OpenSSH

feat: add more tests for proxy jump

fix: use a global variable to close io forwarding, this prevents segfaults

fix: handle proxy list in thread without creating copy
Signed-off-by: Gauravsingh Sisodia <xaerru@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Eshan Kelkar <eshankelkar@galorithm.com>
This commit is contained in:
Gauravsingh Sisodia
2024-02-28 17:20:52 +00:00
committed by Sahana Prasad
parent fe53cdfabd
commit 6d1ed76c7a
20 changed files with 1006 additions and 65 deletions

View File

@ -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

View File

@ -30,6 +30,7 @@
extern "C" {
#endif
#include "libssh/libssh.h"
#include <stdbool.h>
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

View File

@ -417,6 +417,8 @@ enum ssh_options_e {
SSH_OPTIONS_CONTROL_MASTER,
SSH_OPTIONS_CONTROL_PATH,
SSH_OPTIONS_CERTIFICATE,
SSH_OPTIONS_PROXYJUMP,
SSH_OPTIONS_PROXYJUMP_CB_LIST_APPEND,
};
enum {

View File

@ -34,6 +34,7 @@
#else
#include <sys/types.h>
#include <stdbool.h>
#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

View File

@ -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 */

View File

@ -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);

View File

@ -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) {

View File

@ -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) {
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 (!libssh_proxy_jump) {
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);

View File

@ -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');
}
/** @} */

View File

@ -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') {

View File

@ -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);

View File

@ -44,6 +44,9 @@ struct sockaddr_un {
#include <sys/socket.h>
#include <sys/un.h>
#include <signal.h>
#ifdef HAVE_PTHREAD
#include <pthread.h>
#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 */
/** @} */

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,222 @@
#include "config.h"
#define LIBSSH_STATIC
#include "torture.h"
#include <libssh/libssh.h>
#include <pwd.h>
#include <errno.h>
#include <fcntl.h>
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;
}

View File

@ -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

View File

@ -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)

View File

@ -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,6 +731,8 @@ 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");
@ -711,6 +742,7 @@ static void torture_config_unknown(void **state,
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");
}
/**