SSL_shutdown връща -1 с SSL_ERROR_WANT_READ безкрайно дълго

Не мога да разбера как правилно да използвам командата SSL_Shutdown в OpenSSL. Подобни въпроси възникнаха няколко пъти на различни места, но не можах да намеря решение, което да отговаря точно на моята ситуация. Използвам пакет libssl-dev 1.0.1f-1ubuntu2.15 (най-новият за сега) под Ubuntu във VirtualBox.

Работя с малка наследена C++ обвивка върху OpenSSL библиотеката с неблокиращ IO за сървърни и клиентски сокети. Обвивката изглежда работи добре, освен в следния тестов случай (не предоставям кода на самия модулен тест, защото съдържа много код, който не е свързан с проблема):

  1. Инициализирайте сървърен сокет със самоподписан сертификат
  2. Свържете се към този контакт. SSL ръкостискането завършва успешно, с изключение на това, че засега игнорирам X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT връщането на SSL_get_verify_result.
  3. Успешно изпращане/получаване на някои данни чрез връзката. Тази стъпка не е задължителна и не засяга проблема, който следва. Споменавам го само за да покажа, че връзката наистина е установена и зададена в правилно състояние.
  4. Опит за спиране на 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. Съжалявам за грешките в моя английски, ще бъда благодарен, ако някой коригира моя език.


person Roman Dobrovenskii    schedule 06.10.2015    source източник
comment
Малка гнида вероятно не е свързана с вашия проблем: трябва да правите повече от ctx_ = SSL_CTX_new(SSLv23_client_method());. Има контекстни опции, които трябва да бъдат променени и трябва да използвате Индикация за име на сървър (SNI) . Вижте например SSL/TLS Client в OpenSSL wiki.   -  person jww    schedule 06.10.2015
comment
Вижте също stackoverflow.com/questions/28056056/   -  person rogerdpack    schedule 20.10.2019