Ниже рассматривается пример написания SSH-клиента на C с использованием libssh.
Сама библиотека libssh уже устарела, и вместо неё рекомендуется libssh2.
Сравнение libssh и libssh2 есть тут>>>.
Тем не менее у libssh отличные примеры (которые и используются в примерах ниже с небольшими отличаями) и документация, поэтому использую её.
RFC 4251 в SSH Protocol Architecture описывает три основных компонента:
SSH-TRANS: The Secure Shell (SSH) Transport Layer Protocol
Предоставляет авторизацию сервера, секретность и целостность данных. так же может предосталвять компрессию. Как правило работает поверх установленного TCP/IP соединения.SSH-USERAUTH: The Secure Shell (SSH) Authentication Protocol
Предоставляет аутентификацию клиента на сервере. Работает поверх SSH Transport Layer Protocol.SSH-CONNECT: The Secure Shell (SSH) Connection Protocol
Разделяет зашифрованный туннель на несколько логических каналов. Предоставляет интерактивные сессии для авторизации, удалённого выполенния команд, форвардинга TCP/IP и X11 соединений. Работает поверх SSH Authentication Protocol.
Последовательность установления сессии хорошо описана тут>>> и тут>>>.
Кратко — стандартная SSH сессия устанавливается следующим образом:
- Установка соединения с SSH-сервером. Выполняется handshake, в результате которого клиент получает от сервера публичный ключ. Клиент выполняет проверку ключа с помощью отпечатка или файла
known_hosts. - Клиент выполняет авторизацию. Стандартный способ — пароль, так же может использоваться авторизация по ключам (RSA или DSA, сгенерированные с помощью
openssl) или SSH-агент. - Как только клиент авторизован — можно открыть один или больше каналов (теоретически — любое кол-во). Каналы являются способом передачи данных через единое SSH-соединение. Каждый такой канал имеет свой
stdoutиstderr. - Открыв канал клиент может:
- выполнить удалённую команду
- открыть командную оболочку (shell)
- использовать подсистемы
sftpилиscpдля передачи данных
- По завершении сеанса — закрывается канал, затем закрывается само соединение.
Содержание
Создание и параметры сессии
Наиболее важным объектом в SSH соединении является SSH сессия.
Для создания сессии — используется функция ssh_new():
#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>
int main() {
ssh_session session;
session = ssh_new();
if (session == NULL) exit(-1);
ssh_free(session);
}
libssh следует модели allocate-it-deallocate-it, т.е. каждый объект, созданный с помощью xxxxx_new() должен быть удалён с помощью xxxxx_free().
В данном случае — мы создаём объект с помощью ssh_new(), и очищаем его из памяти с помощью ssh_free().
Собираем, проверяем:
[simterm]
$ gcc -lssh ssh_client_libssh1.c -o ssh_client_libssh1 $ ./ssh_client_libssh1; echo $? 0
[/simterm]
Для задания параметров сесси используется функция ssh_options_set().
Наиболее важные опции:
SSH_OPTIONS_HOST: имя хоста, к котормоу выполняется подключениеSSH_OPTIONS_PORT: порт для подключения (по умолчанию 22)SSH_OPTIONS_USER: пользовательSSH_OPTIONS_LOG_VERBOSITY: уровень детализации сообщений об ошибках
Обязательным параметром является только SSH_OPTIONS_HOST.
Если вы не указываете SSH_OPTIONS_USER — будет использовано имя пользователя, который выполняет программу:
#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>
int main() {
int port = 22;
int verbosity = SSH_LOG_FUNCTIONS;
ssh_session session;
session = ssh_new();
if (session == NULL) exit(-1);
ssh_options_set(session, SSH_OPTIONS_HOST, "localhost");
ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
ssh_options_set(session, SSH_OPTIONS_PORT, &port);
ssh_options_set(session, SSH_OPTIONS_USER, "setevoy");
ssh_free(session);
}
Проверяем:
[simterm]
$ ./ssh_client_libssh1; echo $? 0
[/simterm]
Подключение
Теперь, когда сессия и параметры готовы — можно выполнить подключение.
Для этого используем ssh_connect().
Перепишем код — вынесем имя хоста в первый аргумент, порт — во второй, и попробуем подключиться:
#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
// check for args num
if (argc < 3) exit(-1);
// assign first arg to host var
char host[20];
strcpy(host, argv[1]);
// assign second arg to port var
int port = atoi(argv[2]);
// set verbosity if need
// int verbosity = SSH_LOG_FUNCTIONS;
// int verbosity = SSH_LOG_PROTOCOL;
int connection;
ssh_session session;
session = ssh_new();
if (session == NULL) exit(-1);
ssh_options_set(session, SSH_OPTIONS_HOST, host);
// ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
ssh_options_set(session, SSH_OPTIONS_PORT, &port);
ssh_options_set(session, SSH_OPTIONS_USER, "setevoy");
printf("Connecting to host %s and port %d\n", host, port);
connection = ssh_connect(session);
if (connection != SSH_OK) {
printf("Error connecting to %s: %s\n", host, ssh_get_error(session));
exit -1;
} else {
printf("Connected.\n");
}
ssh_free(session);
}
Собираем, проверяем:
[simterm]$ gcc -lssh ssh_client_libssh1.c -o ssh_client_libssh1
$ ./ssh_client_libssh1 hostname 22
Connecting to host hostname and port 22
Connected.
$ ./ssh_client_libssh1 hostname 23
Connecting to host hostname and port 23
Error connecting to hostname: Connection refused[/simterm]
Авторизация сервера
Как только вы выполнили подключение к серверу — необходимо убедиться в том, что это именно тот сервер, к которому вы хотели подключиться.
Для этого имеется два способа. Первый, рекомендованный — с помощью функции ssh_is_server_known(). Она проверит файл ~/.ssh/known_hosts в поисках имени хоста.
Другой способ — функция ssh_get_pubkey_hash(), для отображения отпечатка публичного ключа сервера (на случай, если вы его помните на память).
В случае, если подключение выполняется первый раз — вы можете спросить пользователя доверяет ли он этому хосту. Если да — то используем функцию ssh_write_knownhost(), что бы добавить отпечаток ключа сервера в known_hosts.
Добавим в наш клиент новую функцию:
...
int verify_knownhost(ssh_session session) {
int state, hlen;
unsigned char *hash = NULL;
char *hexa;
char buf[10];
state = ssh_is_server_known(session);
hlen = ssh_get_pubkey_hash(session, &hash);
if (hlen < 0)
return -1;
switch (state) {
case SSH_SERVER_KNOWN_OK:
break; /* ok */
case SSH_SERVER_KNOWN_CHANGED:
fprintf(stderr, "Host key for server changed: it is now:\n");
ssh_print_hexa("Public key hash", hash, hlen);
fprintf(stderr, "For security reasons, connection will be stopped\n");
free(hash);
return -1;
case SSH_SERVER_FOUND_OTHER:
fprintf(stderr, "The host key for this server was not found but an other"
"type of key exists.\n");
fprintf(stderr, "An attacker might change the default server key to"
"confuse your client into thinking the key does not exist\n");
free(hash);
return -1;
case SSH_SERVER_FILE_NOT_FOUND:
fprintf(stderr, "Could not find known host file.\n");
fprintf(stderr, "If you accept the host key here, the file will be"
"automatically created.\n");
/* fallback to SSH_SERVER_NOT_KNOWN behavior */
case SSH_SERVER_NOT_KNOWN:
hexa = ssh_get_hexa(hash, hlen);
fprintf(stderr,"The server is unknown. Do you trust the host key? [yes/no]\n");
fprintf(stderr, "Public key hash: %s\n", hexa);
free(hexa);
if (fgets(buf, sizeof(buf), stdin) == NULL)
{
free(hash);
return -1;
}
if (strncasecmp(buf, "yes", 3) != 0)
{
free(hash);
return -1;
}
if (ssh_write_knownhost(session) < 0)
{
fprintf(stderr, "Error %s\n", strerror(errno));
free(hash);
return -1;
}
break;
case SSH_SERVER_ERROR:
fprintf(stderr, "Error %s", ssh_get_error(session));
free(hash);
return -1;
}
free(hash);
return 0;
}
...
И её вызов после подключения:
...
if (verify_knownhost(session) < 0) {
ssh_disconnect(session);
ssh_free(session);
exit(-1);
}
...
Полностью наш клиент теперь выглядит так:
#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int verify_knownhost(ssh_session session) {
int state, hlen;
unsigned char *hash = NULL;
char *hexa;
char buf[10];
state = ssh_is_server_known(session);
hlen = ssh_get_pubkey_hash(session, &hash);
if (hlen < 0)
return -1;
switch (state)
{
case SSH_SERVER_KNOWN_OK:
break; /* ok */
case SSH_SERVER_KNOWN_CHANGED:
fprintf(stderr, "Host key for server changed: it is now:\n");
ssh_print_hexa("Public key hash", hash, hlen);
fprintf(stderr, "For security reasons, connection will be stopped\n");
free(hash);
return -1;
case SSH_SERVER_FOUND_OTHER:
fprintf(stderr, "The host key for this server was not found but an other"
"type of key exists.\n");
fprintf(stderr, "An attacker might change the default server key to"
"confuse your client into thinking the key does not exist\n");
free(hash);
return -1;
case SSH_SERVER_FILE_NOT_FOUND:
fprintf(stderr, "Could not find known host file.\n");
fprintf(stderr, "If you accept the host key here, the file will be"
"automatically created.\n");
/* fallback to SSH_SERVER_NOT_KNOWN behavior */
case SSH_SERVER_NOT_KNOWN:
hexa = ssh_get_hexa(hash, hlen);
fprintf(stderr,"The server is unknown. Do you trust the host key [yes/no]?\n");
fprintf(stderr, "Public key hash: %s\n", hexa);
free(hexa);
if (fgets(buf, sizeof(buf), stdin) == NULL)
{
free(hash);
return -1;
}
if (strncasecmp(buf, "yes", 3) != 0)
{
free(hash);
return -1;
}
if (ssh_write_knownhost(session) < 0)
{
fprintf(stderr, "Error %s\n", strerror(errno));
free(hash);
return -1;
}
break;
case SSH_SERVER_ERROR:
fprintf(stderr, "Error %s", ssh_get_error(session));
free(hash);
return -1;
}
free(hash);
return 0;
}
int main(int argc, char **argv) {
// check for args num
if (argc < 3) exit(-1);
// assign first arg to host var
char host[20];
strcpy(host, argv[1]);
// assign second arg to port var
int port = atoi(argv[2]);
// set verbosity if need
// int verbosity = SSH_LOG_FUNCTIONS;
// int verbosity = SSH_LOG_PROTOCOL;
int connection;
ssh_session session;
session = ssh_new();
if (session == NULL) exit(-1);
ssh_options_set(session, SSH_OPTIONS_HOST, host);
// ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
ssh_options_set(session, SSH_OPTIONS_PORT, &port);
ssh_options_set(session, SSH_OPTIONS_USER, "setevoy");
printf("Connecting to host %s and port %d\n", host, port);
connection = ssh_connect(session);
if (connection != SSH_OK) {
printf("Error connecting to %s: %s\n", host, ssh_get_error(session));
exit -1;
} else {
printf("Connected.\n");
}
if (verify_knownhost(session) < 0) {
ssh_disconnect(session);
ssh_free(session);
exit(-1);
}
ssh_disconnect(session);
ssh_free(session);
}
Собираем, проверяем:
[simterm]
$ gcc -lssh ssh_client_libssh1.c -o ssh_client_libssh1
$ ./ssh_client_libssh1 localhost 22 Connecting to host localhost and port 22 Connected. The server is unknown. Do you trust the host key [yes/no]? Public key hash: d9:a4:5a:26:6d:2c:79:14:d5:6b:fc:c5:f1:8b:21:44 yes
[/simterm]
Проверяем файл know_hosts:
[simterm]
$ cat ~/.ssh/known_hosts localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGHw/Xg9XYq2HiVXrtzgVsO9vOkJoROl4hdLD9SmSdc3
[/simterm]
Авторизация пользователя
Последним шагом — нам надо авторизовать пользователя на удалённой системе.
libssh поддерживает несколько методов авторизации, наиболее используемые — password и public key:
- password: клиент отправляет пароль серверу, сервер принимает или овтергает его
- public key method: на сервере хранится публичная часть ключа пользователя, и пользователь при авторизации указывает его приватную часть.
Ниже используем обычную авторизацию по паролю с помощью ssh_userauth_password().
Больше по авторизации — тут>>>.
Обновляем код, после вызова проверки сервера — вызываем авторизацию пользоватля:
...
int rc;
char *password;
password = getpass("Password: ");
rc = ssh_userauth_password(session, NULL, password);
if (rc != SSH_AUTH_SUCCESS) {
fprintf(stderr, "Error authenticating with password: %s\n",
ssh_get_error(session));
ssh_disconnect(session);
ssh_free(session);
exit(-1);
}
...
Проверяем:
[simterm]
$ ./ssh_client_libssh1 localhost 22 Connecting to host localhost and port 22 Connected. Password:
[/simterm]
И вводим неверный пароль:
[simterm]
$ ./ssh_client_libssh1 localhost 22 Connecting to host localhost and port 22 Connected. Password: Error authenticating with password: Access denied. Authentication that can continue: publickey,password
[/simterm]
Выполнение команд
Теперь, когда сервер и пользователь авторизованы — можно выполнить команду на удалённом хосте.
Самый простой способ выполнить удалённую команду — ssh_channel_request_exec().
Добавляем новую функцию:
...
int show_remote_processes(ssh_session session) {
ssh_channel channel;
int rc;
char buffer[256];
int nbytes;
channel = ssh_channel_new(session);
if (channel == NULL)
return SSH_ERROR;
rc = ssh_channel_open_session(channel);
if (rc != SSH_OK)
{
ssh_channel_free(channel);
return rc;
}
rc = ssh_channel_request_exec(channel, "ps aux | grep ssh");
if (rc != SSH_OK) {
ssh_channel_close(channel);
ssh_channel_free(channel);
return rc;
}
nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
while (nbytes > 0) {
if (write(1, buffer, nbytes) != (unsigned int) nbytes)
{
ssh_channel_close(channel);
ssh_channel_free(channel);
return SSH_ERROR;
}
nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
}
if (nbytes < 0)
{
ssh_channel_close(channel);
ssh_channel_free(channel);
return SSH_ERROR;
}
ssh_channel_send_eof(channel);
ssh_channel_close(channel);
ssh_channel_free(channel);
return SSH_OK;
}
...
И её вызов в main():
...
if (show_remote_processes(session) != SSH_OK) {
printf("Error executing request\n");
ssh_get_error(session);
ssh_disconnect(session);
ssh_free(session);
exit(-1);
} else {
printf("\nRequest completed successfully!\n");
}
...
Проверяем:
[simterm]
$ ./ssh_client_libssh1 localhost 22 Connecting to host localhost and port 22 Connected. Password: setevoy 3223 0.0 0.0 44116 3928 pts/1 S+ May15 1:11 ssh -D 3033 [email protected] -p 2222 setevoy 30089 1.0 0.0 26856 2840 pts/3 SL+ 13:04 0:00 ./ssh_client_libssh1 localhost 22 root 30090 0.5 0.0 102476 7524 ? Ss 13:04 0:00 sshd: setevoy [priv] setevoy 30129 0.0 0.0 102476 3908 ? S 13:04 0:00 sshd: setevoy@notty setevoy 30130 0.0 0.0 11960 2452 ? Ss 13:04 0:00 bash -c ps aux | grep ssh setevoy 30132 0.0 0.0 9024 860 ? S 13:04 0:00 grep ssh setevoy 30962 0.1 0.0 35644 8204 pts/6 S+ 12:35 0:02 vim ssh_client_libssh1.c Request completed successfully!
[/simterm]
Проверим на RTFM:
[simterm]
$ ./ssh_client_libssh1 rtfm.co.ua 22 Connecting to host rtfm.co.ua and port 22 Connected. The server is unknown. Do you trust the host key [yes/no]? Public key hash: 10:ab:0b:0e:38:06:b0:71:53:c7:37:9a:d0:20:23:6e yes Password: root 659 0.0 0.1 55184 1216 ? Ss 2016 0:00 /usr/sbin/sshd -D setevoy 6685 0.0 0.2 44428 2428 pts/4 S+ Jan26 0:00 ssh [email protected] -i /home/setevoy/.ssh/rtfm_prod.pem root 25832 0.0 0.5 80652 5776 ? Ss 10:15 0:00 sshd: setevoy [priv] setevoy 25834 0.0 0.3 80652 4024 ? S 10:15 0:00 sshd: setevoy@notty setevoy 25835 0.0 0.2 13228 2780 ? Ss 10:15 0:00 bash -c ps aux | grep ssh setevoy 25837 0.0 0.2 12732 2192 ? S 10:15 0:00 grep ssh Request completed successfully!
[/simterm]
Готово.
Полностью «клиент» можно посмотреть тут>>>.
Ссылки по теме
The Secure Shell (SSH) Protocol Architecture
Red Hat Enterprise Linux 3: Reference Guide Chapter 19. SSH Protocol




