Ниже рассматривается пример написания 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