123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- /**
- * Copyright (c) 2019 Paul-Louis Ageneau
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
- #include "dtlstransport.hpp"
- #include "globals.hpp"
- #include "icetransport.hpp"
- #include <chrono>
- #include <cstring>
- #include <exception>
- #include <iostream>
- #if !USE_GNUTLS
- #ifdef _WIN32
- #include <winsock2.h> // for timeval
- #else
- #include <sys/time.h> // for timeval
- #endif
- #endif
- using namespace std::chrono;
- namespace rtc::impl {
- #if USE_GNUTLS
- void DtlsTransport::Init() {
- gnutls_global_init(); // optional
- }
- void DtlsTransport::Cleanup() { gnutls_global_deinit(); }
- DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, certificate_ptr certificate,
- optional<size_t> mtu, verifier_callback verifierCallback,
- state_callback stateChangeCallback)
- : Transport(lower, std::move(stateChangeCallback)), mMtu(mtu), mCertificate(certificate),
- mVerifierCallback(std::move(verifierCallback)),
- mIsClient(lower->role() == Description::Role::Active), mCurrentDscp(0) {
- PLOG_DEBUG << "Initializing DTLS transport (GnuTLS)";
- gnutls_certificate_credentials_t creds = mCertificate->credentials();
- gnutls_certificate_set_verify_function(creds, CertificateCallback);
- unsigned int flags = GNUTLS_DATAGRAM | (mIsClient ? GNUTLS_CLIENT : GNUTLS_SERVER);
- gnutls::check(gnutls_init(&mSession, flags));
- try {
- // RFC 8261: SCTP performs segmentation and reassembly based on the path MTU.
- // Therefore, the DTLS layer MUST NOT use any compression algorithm.
- // See https://tools.ietf.org/html/rfc8261#section-5
- const char *priorities = "SECURE128:-VERS-SSL3.0:-ARCFOUR-128:-COMP-ALL:+COMP-NULL";
- const char *err_pos = NULL;
- gnutls::check(gnutls_priority_set_direct(mSession, priorities, &err_pos),
- "Failed to set TLS priorities");
- gnutls::check(gnutls_credentials_set(mSession, GNUTLS_CRD_CERTIFICATE, creds));
- gnutls_dtls_set_timeouts(mSession,
- 1000, // 1s retransmission timeout recommended by RFC 6347
- 30000); // 30s total timeout
- gnutls_handshake_set_timeout(mSession, 30000);
- gnutls_session_set_ptr(mSession, this);
- gnutls_transport_set_ptr(mSession, this);
- gnutls_transport_set_push_function(mSession, WriteCallback);
- gnutls_transport_set_pull_function(mSession, ReadCallback);
- gnutls_transport_set_pull_timeout_function(mSession, TimeoutCallback);
- } catch (...) {
- gnutls_deinit(mSession);
- throw;
- }
- }
- DtlsTransport::~DtlsTransport() {
- stop();
- gnutls_deinit(mSession);
- }
- void DtlsTransport::start() {
- Transport::start();
- registerIncoming();
- PLOG_DEBUG << "Starting DTLS recv thread";
- mRecvThread = std::thread(&DtlsTransport::runRecvLoop, this);
- }
- bool DtlsTransport::stop() {
- if (!Transport::stop())
- return false;
- PLOG_DEBUG << "Stopping DTLS recv thread";
- mIncomingQueue.stop();
- mRecvThread.join();
- return true;
- }
- bool DtlsTransport::send(message_ptr message) {
- if (!message || state() != State::Connected)
- return false;
- PLOG_VERBOSE << "Send size=" << message->size();
- ssize_t ret;
- do {
- std::lock_guard lock(mSendMutex);
- mCurrentDscp = message->dscp;
- ret = gnutls_record_send(mSession, message->data(), message->size());
- } while (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN);
- if (ret == GNUTLS_E_LARGE_PACKET)
- return false;
- return gnutls::check(ret);
- }
- void DtlsTransport::incoming(message_ptr message) {
- if (!message) {
- mIncomingQueue.stop();
- return;
- }
- PLOG_VERBOSE << "Incoming size=" << message->size();
- mIncomingQueue.push(message);
- }
- bool DtlsTransport::outgoing(message_ptr message) {
- if (message->dscp == 0)
- message->dscp = mCurrentDscp;
- return Transport::outgoing(std::move(message));
- }
- void DtlsTransport::postHandshake() {
- // Dummy
- }
- void DtlsTransport::runRecvLoop() {
- const size_t bufferSize = 4096;
- // Handshake loop
- try {
- changeState(State::Connecting);
- size_t mtu = mMtu.value_or(DEFAULT_MTU) - 8 - 40; // UDP/IPv6
- gnutls_dtls_set_mtu(mSession, static_cast<unsigned int>(mtu));
- PLOG_VERBOSE << "SSL MTU set to " << mtu;
- int ret;
- do {
- ret = gnutls_handshake(mSession);
- if (ret == GNUTLS_E_LARGE_PACKET)
- throw std::runtime_error("MTU is too low");
- } while (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN ||
- !gnutls::check(ret, "DTLS handshake failed"));
- // RFC 8261: DTLS MUST support sending messages larger than the current path MTU
- // See https://tools.ietf.org/html/rfc8261#section-5
- gnutls_dtls_set_mtu(mSession, bufferSize + 1);
- } catch (const std::exception &e) {
- PLOG_ERROR << "DTLS handshake: " << e.what();
- changeState(State::Failed);
- return;
- }
- // Receive loop
- try {
- PLOG_INFO << "DTLS handshake finished";
- postHandshake();
- changeState(State::Connected);
- char buffer[bufferSize];
- while (true) {
- ssize_t ret;
- do {
- ret = gnutls_record_recv(mSession, buffer, bufferSize);
- } while (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN);
- // RFC 8827: Implementations MUST NOT implement DTLS renegotiation and MUST reject it
- // with a "no_renegotiation" alert if offered.
- // See https://tools.ietf.org/html/rfc8827#section-6.5
- if (ret == GNUTLS_E_REHANDSHAKE) {
- do {
- std::lock_guard lock(mSendMutex);
- ret = gnutls_alert_send(mSession, GNUTLS_AL_WARNING, GNUTLS_A_NO_RENEGOTIATION);
- } while (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN);
- continue;
- }
- // Consider premature termination as remote closing
- if (ret == GNUTLS_E_PREMATURE_TERMINATION) {
- PLOG_DEBUG << "DTLS connection terminated";
- break;
- }
- if (gnutls::check(ret)) {
- if (ret == 0) {
- // Closed
- PLOG_DEBUG << "DTLS connection cleanly closed";
- break;
- }
- auto *b = reinterpret_cast<byte *>(buffer);
- recv(make_message(b, b + ret));
- }
- }
- } catch (const std::exception &e) {
- PLOG_ERROR << "DTLS recv: " << e.what();
- }
- gnutls_bye(mSession, GNUTLS_SHUT_RDWR);
- PLOG_INFO << "DTLS closed";
- changeState(State::Disconnected);
- recv(nullptr);
- }
- int DtlsTransport::CertificateCallback(gnutls_session_t session) {
- DtlsTransport *t = static_cast<DtlsTransport *>(gnutls_session_get_ptr(session));
- if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) {
- return GNUTLS_E_CERTIFICATE_ERROR;
- }
- unsigned int count = 0;
- const gnutls_datum_t *array = gnutls_certificate_get_peers(session, &count);
- if (!array || count == 0) {
- return GNUTLS_E_CERTIFICATE_ERROR;
- }
- gnutls_x509_crt_t crt;
- gnutls::check(gnutls_x509_crt_init(&crt));
- int ret = gnutls_x509_crt_import(crt, &array[0], GNUTLS_X509_FMT_DER);
- if (ret != GNUTLS_E_SUCCESS) {
- gnutls_x509_crt_deinit(crt);
- return GNUTLS_E_CERTIFICATE_ERROR;
- }
- string fingerprint = make_fingerprint(crt);
- gnutls_x509_crt_deinit(crt);
- bool success = t->mVerifierCallback(fingerprint);
- return success ? GNUTLS_E_SUCCESS : GNUTLS_E_CERTIFICATE_ERROR;
- }
- ssize_t DtlsTransport::WriteCallback(gnutls_transport_ptr_t ptr, const void *data, size_t len) {
- DtlsTransport *t = static_cast<DtlsTransport *>(ptr);
- if (len > 0) {
- auto b = reinterpret_cast<const byte *>(data);
- t->outgoing(make_message(b, b + len));
- }
- gnutls_transport_set_errno(t->mSession, 0);
- return ssize_t(len);
- }
- ssize_t DtlsTransport::ReadCallback(gnutls_transport_ptr_t ptr, void *data, size_t maxlen) {
- DtlsTransport *t = static_cast<DtlsTransport *>(ptr);
- if (auto next = t->mIncomingQueue.pop()) {
- message_ptr message = std::move(*next);
- ssize_t len = std::min(maxlen, message->size());
- std::memcpy(data, message->data(), len);
- gnutls_transport_set_errno(t->mSession, 0);
- return len;
- }
- // Closed
- gnutls_transport_set_errno(t->mSession, 0);
- return 0;
- }
- int DtlsTransport::TimeoutCallback(gnutls_transport_ptr_t ptr, unsigned int ms) {
- DtlsTransport *t = static_cast<DtlsTransport *>(ptr);
- bool notEmpty = t->mIncomingQueue.wait(
- ms != GNUTLS_INDEFINITE_TIMEOUT ? std::make_optional(milliseconds(ms)) : nullopt);
- return notEmpty ? 1 : 0;
- }
- #else // USE_GNUTLS==0
- BIO_METHOD *DtlsTransport::BioMethods = NULL;
- int DtlsTransport::TransportExIndex = -1;
- std::mutex DtlsTransport::GlobalMutex;
- void DtlsTransport::Init() {
- std::lock_guard lock(GlobalMutex);
- openssl::init();
- if (!BioMethods) {
- BioMethods = BIO_meth_new(BIO_TYPE_BIO, "DTLS writer");
- if (!BioMethods)
- throw std::runtime_error("Failed to create BIO methods for DTLS writer");
- BIO_meth_set_create(BioMethods, BioMethodNew);
- BIO_meth_set_destroy(BioMethods, BioMethodFree);
- BIO_meth_set_write(BioMethods, BioMethodWrite);
- BIO_meth_set_ctrl(BioMethods, BioMethodCtrl);
- }
- if (TransportExIndex < 0) {
- TransportExIndex = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);
- }
- }
- void DtlsTransport::Cleanup() {
- // Nothing to do
- }
- DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, shared_ptr<Certificate> certificate,
- optional<size_t> mtu, verifier_callback verifierCallback,
- state_callback stateChangeCallback)
- : Transport(lower, std::move(stateChangeCallback)), mMtu(mtu), mCertificate(certificate),
- mVerifierCallback(std::move(verifierCallback)),
- mIsClient(lower->role() == Description::Role::Active), mCurrentDscp(0) {
- PLOG_DEBUG << "Initializing DTLS transport (OpenSSL)";
- try {
- mCtx = SSL_CTX_new(DTLS_method());
- if (!mCtx)
- throw std::runtime_error("Failed to create SSL context");
- openssl::check(SSL_CTX_set_cipher_list(mCtx, "ALL:!LOW:!EXP:!RC4:!MD5:@STRENGTH"),
- "Failed to set SSL priorities");
- // RFC 8261: SCTP performs segmentation and reassembly based on the path MTU.
- // Therefore, the DTLS layer MUST NOT use any compression algorithm.
- // See https://tools.ietf.org/html/rfc8261#section-5
- // RFC 8827: Implementations MUST NOT implement DTLS renegotiation
- // See https://tools.ietf.org/html/rfc8827#section-6.5
- SSL_CTX_set_options(mCtx, SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | SSL_OP_NO_QUERY_MTU |
- SSL_OP_NO_RENEGOTIATION);
- SSL_CTX_set_min_proto_version(mCtx, DTLS1_VERSION);
- SSL_CTX_set_read_ahead(mCtx, 1);
- SSL_CTX_set_quiet_shutdown(mCtx, 1);
- SSL_CTX_set_info_callback(mCtx, InfoCallback);
- SSL_CTX_set_verify(mCtx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
- CertificateCallback);
- SSL_CTX_set_verify_depth(mCtx, 1);
- auto [x509, pkey] = mCertificate->credentials();
- SSL_CTX_use_certificate(mCtx, x509);
- SSL_CTX_use_PrivateKey(mCtx, pkey);
- openssl::check(SSL_CTX_check_private_key(mCtx), "SSL local private key check failed");
- mSsl = SSL_new(mCtx);
- if (!mSsl)
- throw std::runtime_error("Failed to create SSL instance");
- SSL_set_ex_data(mSsl, TransportExIndex, this);
- if (mIsClient)
- SSL_set_connect_state(mSsl);
- else
- SSL_set_accept_state(mSsl);
- mInBio = BIO_new(BIO_s_mem());
- mOutBio = BIO_new(BioMethods);
- if (!mInBio || !mOutBio)
- throw std::runtime_error("Failed to create BIO");
- BIO_set_mem_eof_return(mInBio, BIO_EOF);
- BIO_set_data(mOutBio, this);
- SSL_set_bio(mSsl, mInBio, mOutBio);
- auto ecdh = unique_ptr<EC_KEY, decltype(&EC_KEY_free)>(
- EC_KEY_new_by_curve_name(NID_X9_62_prime256v1), EC_KEY_free);
- SSL_set_options(mSsl, SSL_OP_SINGLE_ECDH_USE);
- SSL_set_tmp_ecdh(mSsl, ecdh.get());
- } catch (...) {
- if (mSsl)
- SSL_free(mSsl);
- if (mCtx)
- SSL_CTX_free(mCtx);
- throw;
- }
- }
- DtlsTransport::~DtlsTransport() {
- stop();
- SSL_free(mSsl);
- SSL_CTX_free(mCtx);
- }
- void DtlsTransport::start() {
- Transport::start();
- registerIncoming();
- PLOG_DEBUG << "Starting DTLS recv thread";
- mRecvThread = std::thread(&DtlsTransport::runRecvLoop, this);
- }
- bool DtlsTransport::stop() {
- if (!Transport::stop())
- return false;
- PLOG_DEBUG << "Stopping DTLS recv thread";
- mIncomingQueue.stop();
- mRecvThread.join();
- SSL_shutdown(mSsl);
- return true;
- }
- bool DtlsTransport::send(message_ptr message) {
- if (!message || state() != State::Connected)
- return false;
- PLOG_VERBOSE << "Send size=" << message->size();
- mCurrentDscp = message->dscp;
- int ret = SSL_write(mSsl, message->data(), int(message->size()));
- return openssl::check(mSsl, ret);
- }
- void DtlsTransport::incoming(message_ptr message) {
- if (!message) {
- mIncomingQueue.stop();
- return;
- }
- PLOG_VERBOSE << "Incoming size=" << message->size();
- mIncomingQueue.push(message);
- }
- bool DtlsTransport::outgoing(message_ptr message) {
- if (message->dscp == 0)
- message->dscp = mCurrentDscp;
- return Transport::outgoing(std::move(message));
- }
- void DtlsTransport::postHandshake() {
- // Dummy
- }
- void DtlsTransport::runRecvLoop() {
- const size_t bufferSize = 4096;
- try {
- changeState(State::Connecting);
- size_t mtu = mMtu.value_or(DEFAULT_MTU) - 8 - 40; // UDP/IPv6
- SSL_set_mtu(mSsl, static_cast<unsigned int>(mtu));
- PLOG_VERBOSE << "SSL MTU set to " << mtu;
- // Initiate the handshake
- int ret = SSL_do_handshake(mSsl);
- openssl::check(mSsl, ret, "Handshake failed");
- byte buffer[bufferSize];
- while (mIncomingQueue.running()) {
- // Process pending messages
- while (auto next = mIncomingQueue.tryPop()) {
- message_ptr message = std::move(*next);
- BIO_write(mInBio, message->data(), int(message->size()));
- if (state() == State::Connecting) {
- // Continue the handshake
- ret = SSL_do_handshake(mSsl);
- if (!openssl::check(mSsl, ret, "Handshake failed"))
- break;
- if (SSL_is_init_finished(mSsl)) {
- // RFC 8261: DTLS MUST support sending messages larger than the current path
- // MTU See https://tools.ietf.org/html/rfc8261#section-5
- SSL_set_mtu(mSsl, bufferSize + 1);
- PLOG_INFO << "DTLS handshake finished";
- postHandshake();
- changeState(State::Connected);
- }
- } else {
- ret = SSL_read(mSsl, buffer, bufferSize);
- if (!openssl::check(mSsl, ret))
- break;
- if (ret > 0)
- recv(make_message(buffer, buffer + ret));
- }
- }
- // No more messages pending, retransmit and rearm timeout if connecting
- optional<milliseconds> duration;
- if (state() == State::Connecting) {
- // Warning: This function breaks the usual return value convention
- ret = DTLSv1_handle_timeout(mSsl);
- if (ret < 0) {
- throw std::runtime_error("Handshake timeout"); // write BIO can't fail
- } else if (ret > 0) {
- LOG_VERBOSE << "OpenSSL did DTLS retransmit";
- }
- struct timeval timeout = {};
- if (state() == State::Connecting && DTLSv1_get_timeout(mSsl, &timeout)) {
- duration = milliseconds(timeout.tv_sec * 1000 + timeout.tv_usec / 1000);
- // Also handle handshake timeout manually because OpenSSL actually doesn't...
- // OpenSSL backs off exponentially in base 2 starting from the recommended 1s
- // so this allows for 5 retransmissions and fails after roughly 30s.
- if (duration > 30s) {
- throw std::runtime_error("Handshake timeout");
- } else {
- LOG_VERBOSE << "OpenSSL DTLS retransmit timeout is " << duration->count()
- << "ms";
- }
- }
- }
- mIncomingQueue.wait(duration);
- }
- } catch (const std::exception &e) {
- PLOG_ERROR << "DTLS recv: " << e.what();
- }
- if (state() == State::Connected) {
- PLOG_INFO << "DTLS closed";
- changeState(State::Disconnected);
- recv(nullptr);
- } else {
- PLOG_ERROR << "DTLS handshake failed";
- changeState(State::Failed);
- }
- }
- int DtlsTransport::CertificateCallback(int /*preverify_ok*/, X509_STORE_CTX *ctx) {
- SSL *ssl =
- static_cast<SSL *>(X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));
- DtlsTransport *t =
- static_cast<DtlsTransport *>(SSL_get_ex_data(ssl, DtlsTransport::TransportExIndex));
- X509 *crt = X509_STORE_CTX_get_current_cert(ctx);
- string fingerprint = make_fingerprint(crt);
- return t->mVerifierCallback(fingerprint) ? 1 : 0;
- }
- void DtlsTransport::InfoCallback(const SSL *ssl, int where, int ret) {
- DtlsTransport *t =
- static_cast<DtlsTransport *>(SSL_get_ex_data(ssl, DtlsTransport::TransportExIndex));
- if (where & SSL_CB_ALERT) {
- if (ret != 256) { // Close Notify
- PLOG_ERROR << "DTLS alert: " << SSL_alert_desc_string_long(ret);
- }
- t->mIncomingQueue.stop(); // Close the connection
- }
- }
- int DtlsTransport::BioMethodNew(BIO *bio) {
- BIO_set_init(bio, 1);
- BIO_set_data(bio, NULL);
- BIO_set_shutdown(bio, 0);
- return 1;
- }
- int DtlsTransport::BioMethodFree(BIO *bio) {
- if (!bio)
- return 0;
- BIO_set_data(bio, NULL);
- return 1;
- }
- int DtlsTransport::BioMethodWrite(BIO *bio, const char *in, int inl) {
- if (inl <= 0)
- return inl;
- auto transport = reinterpret_cast<DtlsTransport *>(BIO_get_data(bio));
- if (!transport)
- return -1;
- auto b = reinterpret_cast<const byte *>(in);
- transport->outgoing(make_message(b, b + inl));
- return inl; // can't fail
- }
- long DtlsTransport::BioMethodCtrl(BIO * /*bio*/, int cmd, long /*num*/, void * /*ptr*/) {
- switch (cmd) {
- case BIO_CTRL_FLUSH:
- return 1;
- case BIO_CTRL_DGRAM_QUERY_MTU:
- return 0; // SSL_OP_NO_QUERY_MTU must be set
- case BIO_CTRL_WPENDING:
- case BIO_CTRL_PENDING:
- return 0;
- default:
- break;
- }
- return 0;
- }
- #endif
- } // namespace rtc::impl
|