Не мога да разбера как правилно да използвам командата SSL_Shutdown в OpenSSL. Подобни въпроси възникнаха няколко пъти на различни места, но не можах да намеря решение, което да отговаря точно на моята ситуация. Използвам пакет libssl-dev 1.0.1f-1ubuntu2.15 (най-новият за сега) под Ubuntu във VirtualBox.
Работя с малка наследена C++ обвивка върху OpenSSL библиотеката с неблокиращ IO за сървърни и клиентски сокети. Обвивката изглежда работи добре, освен в следния тестов случай (не предоставям кода на самия модулен тест, защото съдържа много код, който не е свързан с проблема):
- Инициализирайте сървърен сокет със самоподписан сертификат
- Свържете се към този контакт. SSL ръкостискането завършва успешно, с изключение на това, че засега игнорирам X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT връщането на SSL_get_verify_result.
- Успешно изпращане/получаване на някои данни чрез връзката. Тази стъпка не е задължителна и не засяга проблема, който следва. Споменавам го само за да покажа, че връзката наистина е установена и зададена в правилно състояние.
- Опит за спиране на SSL връзка (сървър или клиент, няма значение кой), което води до безкрайно чакане при избор.
Всички извиквания към SSL_read и SSL_write са синхронизирани, locking_callback също е зададен. След стъпка 3 няма други операции върху гнезда, освен изключване на един от тях.
В кодовия фрагмент по-долу пропускам цялата обработка на грешки и кода за отстраняване на грешки за яснота, нито едно от извикванията на OpenSSL/POSIX не е неуспешно (с изключение на случаите, когато съм оставил обработката на грешки на място). Предоставям и функции за свързване, в случай че това е важно:
void OpenSslWrapper::ConnectToHost( ErrorCode& ec )
{
ctx_ = SSL_CTX_new(SSLv23_client_method());
SSL_CTX_load_verify_locations(ctx_, NULL, config_.verify_locations.c_str());
if (config_.use_standard_verify_locations)
{
SSL_CTX_set_default_verify_paths(ctx_);
}
bio_ = BIO_new_ssl_connect(ctx_);
BIO_get_ssl(bio_, &ssl_);
SSL_set_mode(ssl_, SSL_MODE_AUTO_RETRY);
std::string hostname = config_.address + ":" + to_string(config_.port);
BIO_set_conn_hostname(bio_, hostname.c_str());
BIO_set_nbio(bio_, 1);
int res = 0;
while ((res = BIO_do_connect(bio_)) <= 0)
{
BIO_get_fd(bio_, &fd_);
if (!BIO_should_retry(bio_))
{ /* Never happens */}
WaitAfterError(res);
}
res = SSL_get_verify_result(ssl_);
if (res != X509_V_OK && res != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT)
{ /* Never happens */ }
SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE);
SSL_set_mode(ssl_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
}
// config_.handle is a file descriptor which was got from
// accept function, stx_ is also set in advance
void OpenSslWrapper::ConnectAsServer( ErrorCode& ec )
{
ssl_ = SSL_new(ctx_);
int flags = fcntl(config_.handle, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(config_.handle, F_SETFL, flags);
SSL_set_fd(ssl_, config_.handle);
while (true)
{
int res = SSL_accept(ssl_);
if( res > 0) {break;}
if( !WaitAfterError(res).isSucceded() )
{ /* never happens */ }
}
SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE);
SSL_set_mode(ssl_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
}
// The trouble is here
void OpenSSLWrapper::Shutdown()
{
// ...
while (true)
{
int ret = SSL_shutdown(ssl_);
if (ret > 0) {break;}
else if (ret == 0) {continue;}
else {WaitAfterError(ret);}
}
// ...
}
ErrorCode OpenSSLWrapper::WaitAfterError(int res)
{
int err = SSL_get_error(ssl_, res);
switch (ret)
{
case SSL_ERROR_WANT_READ:
WaitForFd(fd_, k_WaitRead);
return ErrorCode::Success;
case SSL_ERROR_WANT_WRITE:
case SSL_ERROR_WANT_CONNECT:
case SSL_ERROR_WANT_ACCEPT:
WaitForFd(fd_, k_WaitWrite);
return ErrorCode::Success;
default:
return ErrorCode::Fail;
}
}
WaitForFd е просто обвивка на select, която чака на даден сокет безкрайно дълго на определен FD_SET за четене или запис.
Когато клиентът извика Shutdown, първото извикване на SSL_Shutdown връща 0. След второто извикване връща -1 и SSL_get_error връща SSL_ERROR_WANT_READ, но избирането на файловия дескриптор за четене никога не се връща. Ако посочите изчакване при избор, SSL_Shutdown ще продължи да връща -1 и SSL_get_error ще продължи да връща SSL_ERROR_WANT_READ. Цикълът никога не излиза. След първото извикване на SSL_Shutdown състоянието на изключване винаги е SSL_SENT_SHUTDOWN.
Няма значение дали затварям сървър или клиент: и двата имат едно и също поведение.
Има и странна ситуация, когато се свързвам с някакъв външен хост. Първото извикване на SSL_Shutdown връща 0, второто -1 с SSL_ERROR_WANT_READ. Избирането на сокета завършва успешно, но когато извикам SSL_Shutdown следващия път, отново получих -1 с грешка SSL_ERROR_SYSCALL и errno=0. Както прочетох на други места, не е голяма работа, въпреки че все още изглежда странно и може би е свързано по някакъв начин, затова го споменавам тук.
UPD. Пренесох същия код за Windows, поведението не се промени.
P.S. Съжалявам за грешките в моя английски, ще бъда благодарен, ако някой коригира моя език.
ctx_ = SSL_CTX_new(SSLv23_client_method());
. Има контекстни опции, които трябва да бъдат променени и трябва да използвате Индикация за име на сървър (SNI) . Вижте например SSL/TLS Client в OpenSSL wiki. - person jww   schedule 06.10.2015