sftp: Add support for the users-groups-by-id@openssh.com OpenSSH extension on the server side.

Signed-off-by: Bulitha Kawushika De Zoysa <bulithakaushika99@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
This commit is contained in:
Bulitha Kawushika De Zoysa
2026-03-18 10:48:39 +05:30
committed by Jakub Jelen
parent bc24bba176
commit c9f34ac55f
3 changed files with 678 additions and 15 deletions

View File

@@ -24,11 +24,12 @@
#include "config.h"
#ifndef _WIN32
#include <netinet/in.h>
#include <arpa/inet.h>
#include <dirent.h>
#include <grp.h>
#include <netinet/in.h>
#include <pwd.h>
#include <sys/statvfs.h>
#endif
@@ -239,9 +240,7 @@ sftp_make_client_message(sftp_session sftp, sftp_packet packet)
}
break;
case SSH_FXP_EXTENDED:
rc = ssh_buffer_unpack(payload,
"s",
&msg->submessage);
rc = ssh_buffer_unpack(payload, "s", &msg->submessage);
if (rc != SSH_OK) {
goto error;
}
@@ -256,9 +255,13 @@ sftp_make_client_message(sftp_session sftp, sftp_packet packet)
goto error;
}
} else if (strcmp(msg->submessage, "statvfs@openssh.com") == 0 ){
rc = ssh_buffer_unpack(payload,
"s",
&msg->filename);
rc = ssh_buffer_unpack(payload, "s", &msg->filename);
if (rc != SSH_OK) {
goto error;
}
} else if (strcmp(msg->submessage,
"users-groups-by-id@openssh.com") == 0) {
rc = ssh_buffer_unpack(payload, "SS", &msg->data, &msg->handle);
if (rc != SSH_OK) {
goto error;
}
@@ -834,14 +837,17 @@ int sftp_reply_version(sftp_client_message client_msg)
return -1;
}
rc = ssh_buffer_pack(reply, "dssssss",
rc = ssh_buffer_pack(reply,
"dssssssss",
LIBSFTP_VERSION,
"posix-rename@openssh.com",
"1",
"hardlink@openssh.com",
"1",
"statvfs@openssh.com",
"2");
"2",
"users-groups-by-id@openssh.com",
"1");
if (rc != SSH_OK) {
ssh_set_error_oom(session);
SSH_BUFFER_FREE(reply);
@@ -1067,6 +1073,352 @@ struct sftp_handle
char *name;
};
/**
* @internal
*
* @brief Parse a blob of IDs into a sftp_name_id_map.
*
* This function extracts numeric IDs from the binary blob and populates
* the 'ids' array of the map. Note that each element of the 'names'
* array in the map is initialized to NULL by this function.
*
* @param[in] ids_blob The binary string blob containing uint32_t IDs.
*
* @return A newly allocated sftp_name_id_map on success, or NULL on error.
*/
static sftp_name_id_map sftp_name_id_map_from_ids_blob(ssh_string ids_blob)
{
sftp_name_id_map map = NULL;
ssh_buffer buf = NULL;
size_t len;
uint32_t count, i;
int rc;
if (ids_blob == NULL) {
SSH_LOG(SSH_LOG_WARNING, "IDs blob is NULL");
return NULL;
}
len = ssh_string_len(ids_blob);
if (len % sizeof(uint32_t) != 0) {
SSH_LOG(SSH_LOG_WARNING,
"IDs blob length is not a multiple of 4 bytes");
return NULL;
}
count = len / sizeof(uint32_t);
map = sftp_name_id_map_new(count);
if (map == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Failed to allocate sftp_name_id_map");
return NULL;
}
buf = ssh_buffer_new();
if (buf == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Failed to allocate ssh_buffer");
sftp_name_id_map_free(map);
return NULL;
}
rc = ssh_buffer_add_data(buf, ssh_string_data(ids_blob), len);
if (rc < 0) {
SSH_LOG(SSH_LOG_WARNING, "Failed to copy blob data to buffer");
SSH_BUFFER_FREE(buf);
sftp_name_id_map_free(map);
return NULL;
}
for (i = 0; i < count; i++) {
uint32_t val;
rc = ssh_buffer_unpack(buf, "d", &val);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARNING, "Failed to unpack ID from buffer");
SSH_BUFFER_FREE(buf);
sftp_name_id_map_free(map);
return NULL;
}
map->ids[i] = val;
}
SSH_BUFFER_FREE(buf);
return map;
}
/**
* @internal
*
* @brief Fill the names array using UIDs in the map.
*/
static int sftp_fill_names_using_uids(sftp_name_id_map users_map)
{
struct passwd pwd_struct;
struct passwd *pwd_res = NULL;
long pwd_buf_size = -1;
char *pwd_lookup_buf = NULL;
uint32_t i;
int rc = SSH_OK;
if (users_map == NULL) {
return SSH_ERROR;
}
#ifdef _SC_GETPW_R_SIZE_MAX
pwd_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
#endif
if (pwd_buf_size <= 0) {
pwd_buf_size = 16384;
}
pwd_lookup_buf = calloc(1, pwd_buf_size);
if (pwd_lookup_buf == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Failed to allocate pwd lookup buffer");
return SSH_ERROR;
}
for (i = 0; i < users_map->count; i++) {
int ret = getpwuid_r(users_map->ids[i],
&pwd_struct,
pwd_lookup_buf,
pwd_buf_size,
&pwd_res);
SAFE_FREE(users_map->names[i]);
if (ret == 0 && pwd_res != NULL) {
users_map->names[i] = strdup(pwd_res->pw_name);
} else {
users_map->names[i] = strdup("");
}
if (users_map->names[i] == NULL) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to allocate memory for username string");
rc = SSH_ERROR;
break;
}
}
SAFE_FREE(pwd_lookup_buf);
return rc;
}
/**
* @internal
*
* @brief Fill the names array using GIDs in the map.
*/
static int sftp_fill_names_using_gids(sftp_name_id_map groups_map)
{
struct group grp_struct;
struct group *grp_res = NULL;
long grp_buf_size = -1;
char *grp_lookup_buf = NULL;
uint32_t i;
int rc = SSH_OK;
if (groups_map == NULL) {
return SSH_ERROR;
}
#ifdef _SC_GETGR_R_SIZE_MAX
grp_buf_size = sysconf(_SC_GETGR_R_SIZE_MAX);
#endif
if (grp_buf_size <= 0) {
grp_buf_size = 16384;
}
grp_lookup_buf = calloc(1, grp_buf_size);
if (grp_lookup_buf == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Failed to allocate grp lookup buffer");
return SSH_ERROR;
}
for (i = 0; i < groups_map->count; i++) {
int ret = getgrgid_r(groups_map->ids[i],
&grp_struct,
grp_lookup_buf,
grp_buf_size,
&grp_res);
SAFE_FREE(groups_map->names[i]);
if (ret == 0 && grp_res != NULL) {
groups_map->names[i] = strdup(grp_res->gr_name);
} else {
groups_map->names[i] = strdup("");
}
if (groups_map->names[i] == NULL) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to allocate memory for group name string");
rc = SSH_ERROR;
break;
}
}
SAFE_FREE(grp_lookup_buf);
return rc;
}
/**
* @internal
*
* @brief Pack the resolved names from a map into the output buffer.
*
* This function formats the multiple names according to the
* users-groups-by-id@openssh.com extension specification. Each name in
* the map is individually encoded as an SSH string. The entire concatenated
* sequence of these encoded names is then wrapped and appended to the
* output buffer as one single, large SSH string.
*
* @param[out] out_buffer The destination buffer for the final packed string.
* @param[in] map The map containing the resolved names.
*
* @return SSH_OK on success, or SSH_ERROR on memory allocation failure.
*/
static int sftp_buffer_add_names(ssh_buffer out_buffer, sftp_name_id_map map)
{
ssh_buffer temp_buffer = NULL;
uint32_t i;
int rc;
if (out_buffer == NULL || map == NULL) {
return SSH_ERROR;
}
temp_buffer = ssh_buffer_new();
if (temp_buffer == NULL) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to allocate temporary buffer for names");
return SSH_ERROR;
}
for (i = 0; i < map->count; i++) {
const char *name = map->names[i] != NULL ? map->names[i] : "";
rc = ssh_buffer_pack(temp_buffer, "s", name);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARNING, "Failed to pack name into buffer");
SSH_BUFFER_FREE(temp_buffer);
return SSH_ERROR;
}
}
rc = ssh_buffer_pack(out_buffer,
"dP",
(uint32_t)ssh_buffer_get_len(temp_buffer),
(size_t)ssh_buffer_get_len(temp_buffer),
ssh_buffer_get(temp_buffer));
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to add names string blob to output buffer");
}
SSH_BUFFER_FREE(temp_buffer);
return (rc != SSH_OK) ? SSH_ERROR : SSH_OK;
}
/**
* @internal
*
* @brief Handle users-groups-by-id@openssh.com extension request.
*
* Resolves numeric user IDs (UIDs) and group IDs (GIDs) to their
* corresponding username and group name strings. Returns empty strings
* for IDs that cannot be resolved.
*
* @param[in] client_msg The SFTP client message containing the request.
* client_msg->data contains the UIDs blob.
* client_msg->handle contains the GIDs blob.
*
* @return SSH_OK on success (reply sent to client). On error, an error
* status is sent to the client and SSH_ERROR is returned.
*/
static int process_users_groups_by_id(sftp_client_message client_msg)
{
ssh_buffer out = NULL;
sftp_name_id_map users_map = NULL;
sftp_name_id_map groups_map = NULL;
int rc;
SSH_LOG(SSH_LOG_PROTOCOL, "Processing users-groups-by-id extension");
if (client_msg->data == NULL || client_msg->handle == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Missing UIDs or GIDs blob");
goto error;
}
users_map = sftp_name_id_map_from_ids_blob(client_msg->data);
if (users_map == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Failed to parse UIDs blob");
goto error;
}
groups_map = sftp_name_id_map_from_ids_blob(client_msg->handle);
if (groups_map == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Failed to parse GIDs blob");
goto error;
}
rc = sftp_fill_names_using_uids(users_map);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARNING, "Failed to resolve UIDs");
goto error;
}
rc = sftp_fill_names_using_gids(groups_map);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARNING, "Failed to resolve GIDs");
goto error;
}
out = ssh_buffer_new();
if (out == NULL) {
SSH_LOG(SSH_LOG_WARNING, "Failed to allocate output buffer");
goto error;
}
rc = ssh_buffer_add_u32(out, client_msg->id);
if (rc < 0) {
SSH_LOG(SSH_LOG_WARNING, "Failed to add request ID to buffer");
goto error;
}
rc = sftp_buffer_add_names(out, users_map);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARNING, "Failed to add users to buffer");
goto error;
}
rc = sftp_buffer_add_names(out, groups_map);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARNING, "Failed to add groups to buffer");
goto error;
}
rc = sftp_packet_write(client_msg->sftp, SSH_FXP_EXTENDED_REPLY, out);
if (rc < 0) {
SSH_LOG(SSH_LOG_WARNING, "Failed to send extended reply");
goto error;
}
sftp_name_id_map_free(users_map);
sftp_name_id_map_free(groups_map);
SSH_BUFFER_FREE(out);
SSH_LOG(SSH_LOG_PROTOCOL, "Successfully processed request");
return SSH_OK;
error:
sftp_name_id_map_free(users_map);
sftp_name_id_map_free(groups_map);
SSH_BUFFER_FREE(out);
SSH_LOG(SSH_LOG_WARNING, "Sending error response");
sftp_reply_status(client_msg, SSH_FX_FAILURE, "Internal processing error");
return SSH_ERROR;
}
SSH_SFTP_CALLBACK(process_unsupported);
SSH_SFTP_CALLBACK(process_open);
SSH_SFTP_CALLBACK(process_read);
@@ -1111,6 +1463,10 @@ const struct sftp_message_handler message_handlers[] = {
const struct sftp_message_handler extended_handlers[] = {
/* here are some extended type handlers */
{"statvfs", "statvfs@openssh.com", 0, process_extended_statvfs},
{"users-groups-by-id",
"users-groups-by-id@openssh.com",
0,
process_users_groups_by_id},
{NULL, NULL, 0, NULL},
};

View File

@@ -26,10 +26,11 @@
#define LIBSSH_STATIC
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#ifdef HAVE_VALGRIND_VALGRIND_H
#include <valgrind/valgrind.h>
@@ -48,6 +49,9 @@
#define TORTURE_KNOWN_HOSTS_FILE "libssh_torture_knownhosts"
#define TEST_INVALID_ID_1 99999
#define TEST_INVALID_ID_2 88888
const char template[] = "temp_dir_XXXXXX";
struct test_server_st {
@@ -1300,6 +1304,274 @@ static void torture_server_sftp_payload_overrun(void **state)
free(handle);
}
static void torture_sftp_users_groups_by_id_basic(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
sftp_name_id_map users_map = NULL;
sftp_name_id_map groups_map = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
rc = sftp_extension_supported(tsftp->sftp,
"users-groups-by-id@openssh.com",
"1");
assert_int_equal(rc, 1);
users_map = sftp_name_id_map_new(2);
assert_non_null(users_map);
groups_map = sftp_name_id_map_new(2);
assert_non_null(groups_map);
users_map->ids[0] = TORTURE_SSH_USER_ID_BOB;
users_map->ids[1] = TORTURE_SSH_USER_ID_ALICE;
groups_map->ids[0] = TORTURE_SSH_GROUP_ID_ROOT;
groups_map->ids[1] = TORTURE_SSH_GROUP_ID_COMMON;
rc = sftp_get_users_groups_by_id(tsftp->sftp, users_map, groups_map);
assert_int_equal(rc, 0);
assert_non_null(users_map->names[0]);
assert_string_equal(users_map->names[0], TORTURE_SSH_USER_BOB);
assert_non_null(users_map->names[1]);
assert_string_equal(users_map->names[1], TORTURE_SSH_USER_ALICE);
assert_non_null(groups_map->names[0]);
assert_string_equal(groups_map->names[0], TORTURE_SSH_GROUP_ROOT);
assert_non_null(groups_map->names[1]);
assert_string_equal(groups_map->names[1], TORTURE_SSH_GROUP_COMMON);
sftp_name_id_map_free(users_map);
sftp_name_id_map_free(groups_map);
}
static void torture_sftp_users_groups_by_id_unknown(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
sftp_name_id_map users_map = NULL;
sftp_name_id_map groups_map = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
rc = sftp_extension_supported(tsftp->sftp,
"users-groups-by-id@openssh.com",
"1");
assert_int_equal(rc, 1);
users_map = sftp_name_id_map_new(1);
assert_non_null(users_map);
groups_map = sftp_name_id_map_new(1);
assert_non_null(groups_map);
/* Set User ID and Group ID that do not exist */
users_map->ids[0] = TEST_INVALID_ID_1;
groups_map->ids[0] = TEST_INVALID_ID_1;
rc = sftp_get_users_groups_by_id(tsftp->sftp, users_map, groups_map);
assert_int_equal(rc, 0);
assert_non_null(users_map->names[0]);
assert_string_equal(users_map->names[0], "");
assert_non_null(groups_map->names[0]);
assert_string_equal(groups_map->names[0], "");
sftp_name_id_map_free(users_map);
sftp_name_id_map_free(groups_map);
}
static void torture_sftp_users_groups_by_id_users_only(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
sftp_name_id_map users_map = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
rc = sftp_extension_supported(tsftp->sftp,
"users-groups-by-id@openssh.com",
"1");
assert_int_equal(rc, 1);
users_map = sftp_name_id_map_new(1);
assert_non_null(users_map);
users_map->ids[0] = TORTURE_SSH_USER_ID_ALICE;
rc = sftp_get_users_groups_by_id(tsftp->sftp, users_map, NULL);
assert_int_equal(rc, 0);
assert_non_null(users_map->names[0]);
assert_string_equal(users_map->names[0], TORTURE_SSH_USER_ALICE);
sftp_name_id_map_free(users_map);
}
static void torture_sftp_users_groups_by_id_groups_only(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
sftp_name_id_map groups_map = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
rc = sftp_extension_supported(tsftp->sftp,
"users-groups-by-id@openssh.com",
"1");
assert_int_equal(rc, 1);
groups_map = sftp_name_id_map_new(1);
assert_non_null(groups_map);
groups_map->ids[0] = TORTURE_SSH_GROUP_ID_ROOT;
rc = sftp_get_users_groups_by_id(tsftp->sftp, NULL, groups_map);
assert_int_equal(rc, 0);
assert_non_null(groups_map->names[0]);
assert_string_equal(groups_map->names[0], TORTURE_SSH_GROUP_ROOT);
sftp_name_id_map_free(groups_map);
}
static void torture_sftp_users_groups_by_id_multiple(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
sftp_name_id_map users_map = NULL;
sftp_name_id_map groups_map = NULL;
uint32_t i = 0;
int rc;
struct {
uid_t id;
const char *name;
} expected_users[] = {
{TORTURE_SSH_USER_ID_BOB, TORTURE_SSH_USER_BOB},
{TEST_INVALID_ID_1, ""},
{TORTURE_SSH_USER_ID_ALICE, TORTURE_SSH_USER_ALICE},
{TEST_INVALID_ID_2, ""},
{TORTURE_SSH_USER_ID_CHARLIE, TORTURE_SSH_USER_CHARLIE}};
size_t num_expected_users = ARRAY_SIZE(expected_users);
struct {
gid_t id;
const char *name;
} expected_groups[] = {
{TORTURE_SSH_GROUP_ID_ROOT, TORTURE_SSH_GROUP_ROOT},
{TEST_INVALID_ID_1, ""},
{TORTURE_SSH_GROUP_ID_COMMON, TORTURE_SSH_GROUP_COMMON},
{TEST_INVALID_ID_2, ""},
{TORTURE_SSH_GROUP_ID_SSHD, TORTURE_SSH_GROUP_SSHD}};
size_t num_expected_groups = ARRAY_SIZE(expected_groups);
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
rc = sftp_extension_supported(tsftp->sftp,
"users-groups-by-id@openssh.com",
"1");
assert_int_equal(rc, 1);
users_map = sftp_name_id_map_new(num_expected_users);
assert_non_null(users_map);
groups_map = sftp_name_id_map_new(num_expected_groups);
assert_non_null(groups_map);
for (i = 0; i < num_expected_users; i++) {
users_map->ids[i] = expected_users[i].id;
}
for (i = 0; i < num_expected_groups; i++) {
groups_map->ids[i] = expected_groups[i].id;
}
rc = sftp_get_users_groups_by_id(tsftp->sftp, users_map, groups_map);
assert_int_equal(rc, 0);
for (i = 0; i < num_expected_users; i++) {
assert_non_null(users_map->names[i]);
assert_string_equal(users_map->names[i], expected_users[i].name);
}
for (i = 0; i < num_expected_groups; i++) {
assert_non_null(groups_map->names[i]);
assert_string_equal(groups_map->names[i], expected_groups[i].name);
}
sftp_name_id_map_free(users_map);
sftp_name_id_map_free(groups_map);
}
static void torture_sftp_users_groups_by_id_negative(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
rc = sftp_extension_supported(tsftp->sftp,
"users-groups-by-id@openssh.com",
"1");
assert_int_equal(rc, 1);
rc = sftp_get_users_groups_by_id(NULL, NULL, NULL);
assert_int_equal(rc, SSH_ERROR);
rc = sftp_get_users_groups_by_id(tsftp->sftp, NULL, NULL);
assert_int_equal(rc, SSH_ERROR);
}
int torture_run_tests(void) {
int rc;
struct CMUnitTest tests[] = {
@@ -1330,15 +1602,38 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_server_sftp_handles_exhaustion,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_server_sftp_opendir_handles_exhaustion,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_server_sftp_opendir_handles_exhaustion,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_server_sftp_handle_overrun,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_server_sftp_payload_overrun,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_sftp_users_groups_by_id_basic,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_sftp_users_groups_by_id_unknown,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_sftp_users_groups_by_id_users_only,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_sftp_users_groups_by_id_groups_only,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_sftp_users_groups_by_id_multiple,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_sftp_users_groups_by_id_negative,
session_setup_sftp,
session_teardown),
};
ssh_init();

View File

@@ -53,6 +53,18 @@
#define TORTURE_SSH_USER_CHARLIE "charlie"
#define TORTURE_SSH_USER_NONEUSER "noneuser"
#define TORTURE_SSH_GROUP_ROOT "root"
#define TORTURE_SSH_GROUP_COMMON "users"
#define TORTURE_SSH_GROUP_SSHD "sshd"
#define TORTURE_SSH_USER_ID_BOB 5000
#define TORTURE_SSH_USER_ID_ALICE 5001
#define TORTURE_SSH_USER_ID_CHARLIE 5002
#define TORTURE_SSH_GROUP_ID_ROOT 0
#define TORTURE_SSH_GROUP_ID_COMMON 9000
#define TORTURE_SSH_GROUP_ID_SSHD 65531
/* Used by main to communicate with parse_opt. */
struct argument_s {
const char *pattern;