From c4f1a70a89664b74f2c5246fda6d3a5b49a73099 Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Mon, 15 Dec 2025 19:16:15 +0000 Subject: [PATCH] connect: Support AddressFamily option * allow parsing of AddressFamily in config and cli * supports options "any", "inet" and "inet6" * introduce SSH_OPTIONS_ADDRESS_FAMILY Signed-off-by: Samir Benmendil Reviewed-by: Jakub Jelen Reviewed-by: Andreas Schneider --- include/libssh/config.h | 1 + include/libssh/libssh.h | 7 +++ include/libssh/session.h | 1 + src/config.c | 31 ++++++++++- src/connect.c | 51 +++++++++++++---- src/options.c | 22 ++++++++ tests/client/torture_connect.c | 46 +++++++++++++++ tests/etc/hosts.in | 5 ++ tests/unittests/torture_config.c | 93 +++++++++++++++++++++++++++++++ tests/unittests/torture_options.c | 2 + 10 files changed, 247 insertions(+), 12 deletions(-) diff --git a/include/libssh/config.h b/include/libssh/config.h index 59b01767..48000d2b 100644 --- a/include/libssh/config.h +++ b/include/libssh/config.h @@ -68,6 +68,7 @@ enum ssh_config_opcode_e { SOC_CONTROLPATH, SOC_CERTIFICATE, SOC_REQUIRED_RSA_SIZE, + SOC_ADDRESSFAMILY, SOC_MAX /* Keep this one last in the list */ }; diff --git a/include/libssh/libssh.h b/include/libssh/libssh.h index 21a69fa0..cd4909c4 100644 --- a/include/libssh/libssh.h +++ b/include/libssh/libssh.h @@ -371,6 +371,12 @@ enum ssh_control_master_options_e { SSH_CONTROL_MASTER_AUTOASK }; +enum ssh_address_family_options_e { + SSH_ADDRESS_FAMILY_ANY, + SSH_ADDRESS_FAMILY_INET, + SSH_ADDRESS_FAMILY_INET6 +}; + enum ssh_options_e { SSH_OPTIONS_HOST, SSH_OPTIONS_PORT, @@ -422,6 +428,7 @@ enum ssh_options_e { SSH_OPTIONS_PROXYJUMP, SSH_OPTIONS_PROXYJUMP_CB_LIST_APPEND, SSH_OPTIONS_PKI_CONTEXT, + SSH_OPTIONS_ADDRESS_FAMILY, }; enum { diff --git a/include/libssh/session.h b/include/libssh/session.h index 0512fe05..83c16cf2 100644 --- a/include/libssh/session.h +++ b/include/libssh/session.h @@ -277,6 +277,7 @@ struct ssh_session_struct { bool identities_only; int control_master; char *control_path; + int address_family; } opts; /* server options */ diff --git a/src/config.c b/src/config.c index c1f1206d..d038c382 100644 --- a/src/config.c +++ b/src/config.c @@ -91,7 +91,7 @@ static struct ssh_config_keyword_table_s ssh_config_keyword_table[] = { {"passwordauthentication", SOC_PASSWORDAUTHENTICATION, true}, {"pubkeyauthentication", SOC_PUBKEYAUTHENTICATION, true}, {"addkeystoagent", SOC_UNSUPPORTED, true}, - {"addressfamily", SOC_UNSUPPORTED, true}, + {"addressfamily", SOC_ADDRESSFAMILY, true}, {"batchmode", SOC_UNSUPPORTED, true}, {"canonicaldomains", SOC_UNSUPPORTED, true}, {"canonicalizefallbacklocal", SOC_UNSUPPORTED, true}, @@ -1564,6 +1564,35 @@ static int ssh_config_parse_line_internal(ssh_session session, ssh_options_set(session, SSH_OPTIONS_RSA_MIN_SIZE, &l); } break; + case SOC_ADDRESSFAMILY: + p = ssh_config_get_str_tok(&s, NULL); + if (p == NULL) { + SSH_LOG(SSH_LOG_WARNING, + "line %d: no argument after keyword \"addressfamily\"", + count); + SAFE_FREE(x); + return SSH_ERROR; + } + if (*parsing) { + int value = -1; + + if (strcasecmp(p, "any") == 0) { + value = SSH_ADDRESS_FAMILY_ANY; + } else if (strcasecmp(p, "inet") == 0) { + value = SSH_ADDRESS_FAMILY_INET; + } else if (strcasecmp(p, "inet6") == 0) { + value = SSH_ADDRESS_FAMILY_INET6; + } else { + SSH_LOG(SSH_LOG_WARNING, + "line %d: invalid argument \"%s\"", + count, + p); + SAFE_FREE(x); + return SSH_ERROR; + } + ssh_options_set(session, SSH_OPTIONS_ADDRESS_FAMILY, &value); + } + break; default: ssh_set_error(session, SSH_FATAL, "ERROR - unimplemented opcode: %d", opcode); diff --git a/src/connect.c b/src/connect.c index 2924309d..e0339514 100644 --- a/src/connect.c +++ b/src/connect.c @@ -109,7 +109,8 @@ static int ssh_connect_socket_close(socket_t s) #endif } -static int getai(const char *host, int port, struct addrinfo **ai) +static int +getai(const char *host, int port, int ai_family, struct addrinfo **ai) { const char *service = NULL; struct addrinfo hints; @@ -118,7 +119,7 @@ static int getai(const char *host, int port, struct addrinfo **ai) ZERO_STRUCT(hints); hints.ai_protocol = IPPROTO_TCP; - hints.ai_family = PF_UNSPEC; + hints.ai_family = ai_family; hints.ai_socktype = SOCK_STREAM; if (port == 0) { @@ -165,16 +166,39 @@ socket_t ssh_connect_host_nonblocking(ssh_session session, const char *host, { socket_t s = -1, first = -1; int rc; + int ai_family; + static const char *ai_family_str = NULL; struct addrinfo *ai = NULL; struct addrinfo *itr = NULL; char addrname[NI_MAXHOST], portname[NI_MAXSERV]; - SSH_LOG(SSH_LOG_PACKET, "Resolve target hostname %s port %d", host, port); - rc = getai(host, port, &ai); + switch (session->opts.address_family) { + case SSH_ADDRESS_FAMILY_INET: + ai_family = PF_INET; + ai_family_str = "inet"; + break; + case SSH_ADDRESS_FAMILY_INET6: + ai_family = PF_INET6; + ai_family_str = "inet6"; + break; + case SSH_ADDRESS_FAMILY_ANY: + default: + ai_family = PF_UNSPEC; + ai_family_str = "any"; + } + SSH_LOG(SSH_LOG_PACKET, + "Resolve target hostname %s port %d (%s)", + host, + port, + ai_family_str); + rc = getai(host, port, ai_family, &ai); if (rc != 0) { - ssh_set_error(session, SSH_FATAL, - "Failed to resolve hostname %s (%s)", - host, gai_strerror(rc)); + ssh_set_error(session, + SSH_FATAL, + "Failed to resolve hostname %s (%s): %s", + host, + ai_family_str, + gai_strerror(rc)); return -1; } @@ -194,13 +218,18 @@ socket_t ssh_connect_host_nonblocking(ssh_session session, const char *host, struct addrinfo *bind_ai = NULL; struct addrinfo *bind_itr = NULL; - SSH_LOG(SSH_LOG_PACKET, "Resolving bind address %s", bind_addr); + SSH_LOG(SSH_LOG_PACKET, + "Resolving bind address %s (%s)", + bind_addr, + ai_family_str); - rc = getai(bind_addr, 0, &bind_ai); + rc = getai(bind_addr, 0, ai_family, &bind_ai); if (rc != 0) { - ssh_set_error(session, SSH_FATAL, - "Failed to resolve bind address %s (%s)", + ssh_set_error(session, + SSH_FATAL, + "Failed to resolve bind address %s (%s): %s", bind_addr, + ai_family_str, gai_strerror(rc)); ssh_connect_socket_close(s); s = -1; diff --git a/src/options.c b/src/options.c index d7a87316..3303631c 100644 --- a/src/options.c +++ b/src/options.c @@ -256,6 +256,7 @@ int ssh_options_copy(ssh_session src, ssh_session *dest) new->opts.nodelay = src->opts.nodelay; new->opts.config_processed = src->opts.config_processed; new->opts.control_master = src->opts.control_master; + new->opts.address_family = src->opts.address_family; new->common.log_verbosity = src->common.log_verbosity; new->common.callbacks = src->common.callbacks; @@ -650,6 +651,13 @@ int ssh_options_set_algo(ssh_session session, * context and can free it after this call. * (ssh_pki_ctx) * + * - SSH_OPTIONS_ADDRESS_FAMILY + * Specify which address family to use when connecting. + * + * Possible options: + * - SSH_ADDRESS_FAMILY_ANY: use any address family + * - SSH_ADDRESS_FAMILY_INET: IPv4 only + * - SSH_ADDRESS_FAMILY_INET6: IPv6 only * * @param value The value to set. This is a generic pointer and the * datatype which is used should be set according to the @@ -1393,6 +1401,20 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type, return -1; } break; + case SSH_OPTIONS_ADDRESS_FAMILY: + if (value == NULL) { + ssh_set_error_invalid(session); + return -1; + } else { + int *x = (int *)value; + if (*x < SSH_ADDRESS_FAMILY_ANY || + *x > SSH_ADDRESS_FAMILY_INET6) { + ssh_set_error_invalid(session); + return -1; + } + session->opts.address_family = *x; + } + break; default: ssh_set_error(session, SSH_REQUEST_DENIED, "Unknown ssh option %d", type); return -1; diff --git a/tests/client/torture_connect.c b/tests/client/torture_connect.c index fd3e3d32..b34e5bfe 100644 --- a/tests/client/torture_connect.c +++ b/tests/client/torture_connect.c @@ -20,6 +20,7 @@ */ #include "config.h" +#include "torture_cmocka.h" #define LIBSSH_STATIC @@ -143,6 +144,48 @@ static void torture_connect_ipv6(void **state) { assert_ssh_return_code(session, rc); } +static void torture_connect_addrfamily(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + int rc; + + struct aftest { + enum ssh_address_family_options_e family; + char const *host; + int return_code; + }; + static struct aftest aftests[] = { + {SSH_ADDRESS_FAMILY_ANY, "afboth", SSH_OK}, + {SSH_ADDRESS_FAMILY_INET, "afboth", SSH_OK}, + {SSH_ADDRESS_FAMILY_INET6, "afboth", SSH_OK}, + {SSH_ADDRESS_FAMILY_ANY, "afinet", SSH_OK}, + {SSH_ADDRESS_FAMILY_INET, "afinet", SSH_OK}, + {SSH_ADDRESS_FAMILY_INET6, "afinet", SSH_ERROR}, + {SSH_ADDRESS_FAMILY_ANY, "afinet6", SSH_OK}, + {SSH_ADDRESS_FAMILY_INET, "afinet6", SSH_ERROR}, + {SSH_ADDRESS_FAMILY_INET6, "afinet6", SSH_OK}, + }; + + int aftest_count = sizeof(aftests) / sizeof(aftests[0]); + for (int i = 0; i < aftest_count; ++i) { + struct aftest const *t = &aftests[i]; + + rc = ssh_options_set(session, SSH_OPTIONS_ADDRESS_FAMILY, &t->family); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOST, t->host); + assert_ssh_return_code(session, rc); + + do { + rc = ssh_connect(session); + } while (rc == SSH_AGAIN); + + assert_ssh_return_code_equal(session, rc, t->return_code); + ssh_disconnect(session); + } +} + #if 0 /* This does not work with socket_wrapper */ static void torture_connect_timeout(void **state) { struct torture_state *s = *state; @@ -329,6 +372,9 @@ int torture_run_tests(void) { cmocka_unit_test_setup_teardown(torture_connect_ipv6, session_setup, session_teardown), + cmocka_unit_test_setup_teardown(torture_connect_addrfamily, + session_setup, + session_teardown), cmocka_unit_test_setup_teardown(torture_connect_double, session_setup, session_teardown), diff --git a/tests/etc/hosts.in b/tests/etc/hosts.in index 3c0b0142..ea519350 100644 --- a/tests/etc/hosts.in +++ b/tests/etc/hosts.in @@ -5,3 +5,8 @@ 123.0.0.11 testing fd00::5357:5f0a testing + +127.0.0.10 afboth +fd00::5357:5f0a afboth +127.0.0.10 afinet +fd00::5357:5f0a afinet6 diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c index 5984b03f..12f6ba41 100644 --- a/tests/unittests/torture_config.c +++ b/tests/unittests/torture_config.c @@ -46,6 +46,7 @@ extern LIBSSH_THREAD int ssh_log_level; #define LIBSSH_TESTCONFIG15 "libssh_testconfig15.tmp" #define LIBSSH_TESTCONFIG16 "libssh_testconfig16.tmp" #define LIBSSH_TESTCONFIG17 "libssh_testconfig17.tmp" +#define LIBSSH_TESTCONFIG18 "libssh_testconfig18.tmp" #define LIBSSH_TESTCONFIGGLOB "libssh_testc*[36].tmp" #define LIBSSH_TEST_PUBKEYTYPES "libssh_test_PubkeyAcceptedKeyTypes.tmp" #define LIBSSH_TEST_PUBKEYALGORITHMS "libssh_test_PubkeyAcceptedAlgorithms.tmp" @@ -222,6 +223,15 @@ extern LIBSSH_THREAD int ssh_log_level; "\tControlMaster yes\n" \ "\tControlPath none\n" +#define LIBSSH_TESTCONFIG_STRING18 \ + "Host simple\n" \ + "Host af\n" \ + "\tAddressFamily any\n" \ + "Host af4\n" \ + "\tAddressFamily inet\n" \ + "Host af6\n" \ + "\tAddressFamily inet6\n" + #define LIBSSH_TEST_PUBKEYTYPES_STRING \ "PubkeyAcceptedKeyTypes "PUBKEYACCEPTEDTYPES"\n" @@ -292,6 +302,7 @@ static int setup_config_files(void **state) unlink(LIBSSH_TESTCONFIG15); unlink(LIBSSH_TESTCONFIG16); unlink(LIBSSH_TESTCONFIG17); + unlink(LIBSSH_TESTCONFIG18); unlink(LIBSSH_TEST_PUBKEYTYPES); unlink(LIBSSH_TEST_PUBKEYALGORITHMS); unlink(LIBSSH_TEST_NONEWLINEEND); @@ -350,6 +361,8 @@ static int setup_config_files(void **state) LIBSSH_TESTCONFIG_STRING16); torture_write_file(LIBSSH_TESTCONFIG17, LIBSSH_TESTCONFIG_STRING17); + torture_write_file(LIBSSH_TESTCONFIG18, + LIBSSH_TESTCONFIG_STRING18); torture_write_file(LIBSSH_TEST_PUBKEYTYPES, LIBSSH_TEST_PUBKEYTYPES_STRING); @@ -392,6 +405,7 @@ static int teardown_config_files(void **state) unlink(LIBSSH_TESTCONFIG15); unlink(LIBSSH_TESTCONFIG16); unlink(LIBSSH_TESTCONFIG17); + unlink(LIBSSH_TESTCONFIG18); unlink(LIBSSH_TEST_PUBKEYTYPES); unlink(LIBSSH_TEST_PUBKEYALGORITHMS); unlink(LIBSSH_TEST_NONEWLINEEND); @@ -1520,6 +1534,79 @@ static void torture_config_control_master_file(void **state) torture_config_control_master(state, LIBSSH_TESTCONFIG17, NULL); } +/** + * @brief Verify we can parse AdressFamily configuration option + */ +static void torture_config_address_family(void **state, + const char *file, + const char *string) +{ + ssh_session session = *state; + + const char *config; + + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "simple"); + _parse_config(session, file, string, SSH_OK); + assert_int_equal(session->opts.address_family, SSH_ADDRESS_FAMILY_ANY); + + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "af"); + _parse_config(session, file, string, SSH_OK); + assert_int_equal(session->opts.address_family, SSH_ADDRESS_FAMILY_ANY); + + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "af4"); + _parse_config(session, file, string, SSH_OK); + assert_int_equal(session->opts.address_family, SSH_ADDRESS_FAMILY_INET); + + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "af6"); + _parse_config(session, file, string, SSH_OK); + assert_int_equal(session->opts.address_family, SSH_ADDRESS_FAMILY_INET6); + + /* test for parsing failures */ + config = "Host afmissing\n" + "\tAddressFamily\n"; + if (file != NULL) { + torture_write_file(file, config); + } else { + string = config; + } + + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "afmissing"); + _parse_config(session, file, string, SSH_ERROR); + + config = "Host afinvalid\n" + "\tAddressFamily wurstkäse\n"; + if (file != NULL) { + torture_write_file(file, config); + } else { + string = config; + } + + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, "afinvalid"); + _parse_config(session, file, string, SSH_ERROR); +} + +/** + * @brief Verify we can parse AdressFamily configuration option from string + */ +static void torture_config_address_family_string(void **state) +{ + torture_config_address_family(state, NULL, LIBSSH_TESTCONFIG_STRING18); +} + +/** + * @brief Verify we can parse AdressFamily configuration option from file + */ +static void torture_config_address_family_file(void **state) +{ + torture_config_address_family(state, LIBSSH_TESTCONFIG18, NULL); +} + /** * @brief Verify the configuration parser handles all the possible * versions of RekeyLimit configuration option. @@ -2707,6 +2794,12 @@ int torture_run_tests(void) cmocka_unit_test_setup_teardown(torture_config_control_master_string, setup, teardown), + cmocka_unit_test_setup_teardown(torture_config_address_family_file, + setup, + teardown), + cmocka_unit_test_setup_teardown(torture_config_address_family_string, + setup, + teardown), cmocka_unit_test_setup_teardown(torture_config_rekey_file, setup, teardown), diff --git a/tests/unittests/torture_options.c b/tests/unittests/torture_options.c index 98ae9766..aea32832 100644 --- a/tests/unittests/torture_options.c +++ b/tests/unittests/torture_options.c @@ -1352,6 +1352,7 @@ static void torture_options_copy(void **state) "GSSAPIDelegateCredentials yes\n" "PubkeyAuthentication yes\n" /* sets flags */ "GSSAPIAuthentication no\n" /* sets flags */ + "AddressFamily inet6\n" "", config); fclose(config); @@ -1428,6 +1429,7 @@ static void torture_options_copy(void **state) assert_true(session->opts.config_processed == new->opts.config_processed); assert_memory_equal(session->opts.options_seen, new->opts.options_seen, sizeof(session->opts.options_seen)); + assert_int_equal(session->opts.address_family, new->opts.address_family); ssh_free(new);