SSL_shutdown возвращает -1 с бесконечно длинным SSL_ERROR_WANT_READ

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

Я работаю с небольшой устаревшей оболочкой C++ над библиотекой OpenSSL с неблокирующим вводом-выводом для серверных и клиентских сокетов. Обертка работает нормально, за исключением следующего тестового случая (я не привожу код самого модульного теста, потому что он содержит много кода, не связанного с проблемой):

  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 — это просто простая оболочка для выбора, которая бесконечно долго ожидает в заданном сокете указанного 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 на вики OpenSSL.   -  person jww    schedule 06.10.2015
comment
См. также stackoverflow.com/questions/28056056/   -  person rogerdpack    schedule 20.10.2019