| /** @file |
| |
| Stats of TLS |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include "SSLStats.h" |
| #include "P_SSLConfig.h" |
| #include "P_SSLUtils.h" |
| #include "iocore/net/SSLMultiCertConfigLoader.h" |
| #include "records/RecProcess.h" |
| |
| #include <openssl/err.h> |
| |
| #include <string_view> |
| |
| SSLStatsBlock ssl_rsb; |
| std::unordered_map<std::string, Metrics::Counter::AtomicType *> cipher_map; |
| |
| #if defined(OPENSSL_IS_BORINGSSL) || HAVE_SSL_CTX_GET0_IMPLEMENTED_GROUPS |
| std::unordered_map<std::string, Metrics::Counter::AtomicType *> tls_group_map; |
| std::unordered_map<std::string, Metrics::Counter::AtomicType *> tls_group_handshake_time_map; |
| #elif HAVE_SSL_GET_NEGOTIATED_GROUP |
| std::unordered_map<int, Metrics::Counter::AtomicType *> tls_group_map; |
| std::unordered_map<int, Metrics::Counter::AtomicType *> tls_group_handshake_time_map; |
| #endif |
| |
| namespace |
| { |
| DbgCtl dbg_ctl_ssl{"ssl"}; |
| |
| #if defined(OPENSSL_IS_BORINGSSL) |
| constexpr std::string_view UNKNOWN_CIPHER{"(NONE)"}; |
| #endif |
| |
| #if defined(OPENSSL_IS_BORINGSSL) || HAVE_SSL_CTX_GET0_IMPLEMENTED_GROUPS || HAVE_SSL_GET_NEGOTIATED_GROUP |
| |
| template <typename T> |
| void |
| add_group_stat(T key, const std::string &name) |
| { |
| // If not already registered ... |
| if (tls_group_map.find(key) == tls_group_map.end()) { |
| Metrics::Counter::AtomicType *metric = Metrics::Counter::createPtr("proxy.process.ssl.group.user_agent." + name); |
| |
| tls_group_map.emplace(key, metric); |
| Dbg(dbg_ctl_ssl, "registering SSL group metric '%s'", name.c_str()); |
| } |
| |
| // Register corresponding handshake time metric |
| if (tls_group_handshake_time_map.find(key) == tls_group_handshake_time_map.end()) { |
| Metrics::Counter::AtomicType *time_metric = |
| Metrics::Counter::createPtr("proxy.process.ssl.group.user_agent." + name + ".handshake_time"); |
| |
| tls_group_handshake_time_map.emplace(key, time_metric); |
| Dbg(dbg_ctl_ssl, "registering SSL group handshake time metric '%s.handshake_time'", name.c_str()); |
| } |
| } |
| #endif // OPENSSL_IS_BORINGSSL or HAVE_SSL_CTX_GET0_IMPLEMENTED_GROUPS or HAVE_SSL_GET_NEGOTIATED_GROUP |
| |
| #if not defined(OPENSSL_IS_BORINGSSL) and not HAVE_SSL_CTX_GET0_IMPLEMENTED_GROUPS and \ |
| HAVE_SSL_GET_NEGOTIATED_GROUP // OPENSSL 3.x without SSL_CTX_get0_implemented_groups |
| |
| struct TLSGroup { |
| int nid; |
| std::string name; |
| }; |
| |
| // NID and Group table. Some groups are not defined by some library. |
| const TLSGroup TLS_GROUPS[] = { |
| {SSL_GROUP_STAT_OTHER_KEY, "OTHER" }, |
| {NID_X9_62_prime256v1, "P-256" }, |
| {NID_secp384r1, "P-384" }, |
| {NID_secp521r1, "P-521" }, |
| {NID_X25519, "X25519" }, |
| #ifdef NID_secp224r1 |
| {NID_secp224r1, "P-224" }, |
| #endif |
| #ifdef NID_X448 |
| {NID_X448, "X448" }, |
| #endif |
| #ifdef NID_ffdhe2048 |
| {NID_ffdhe2048, "ffdhe2048" }, |
| #endif |
| #ifdef NID_ffdhe3072 |
| {NID_ffdhe3072, "ffdhe3072" }, |
| #endif |
| #ifdef NID_ffdhe4096 |
| {NID_ffdhe4096, "ffdhe4096" }, |
| #endif |
| #ifdef NID_ffdhe6144 |
| {NID_ffdhe6144, "ffdhe6144" }, |
| #endif |
| #ifdef NID_ffdhe8192 |
| {NID_ffdhe8192, "ffdhe8192" }, |
| #endif |
| #ifdef NID_X25519MLKEM768 |
| {NID_X25519MLKEM768, "X25519MLKEM768"}, |
| #endif |
| }; |
| |
| #endif // OPENSSL 3.x without SSL_CTX_get0_implemented_groups |
| |
| } // end anonymous namespace |
| |
| // ToDo: This gets called once per global sync, for now at least. |
| void |
| SSLPeriodicMetricsUpdate() |
| { |
| SSLCertificateConfig::scoped_config certLookup; |
| |
| int64_t sessions = 0; |
| int64_t hits = 0; |
| int64_t misses = 0; |
| int64_t timeouts = 0; |
| |
| Dbg(dbg_ctl_ssl, "Starting to update the new session metrics"); |
| if (certLookup) { |
| const unsigned ctxCount = certLookup->count(); |
| for (size_t i = 0; i < ctxCount; i++) { |
| SSLCertContext *cc = certLookup->get(i); |
| if (cc) { |
| shared_SSL_CTX ctx = cc->getCtx(); |
| if (ctx) { |
| sessions += SSL_CTX_sess_accept_good(ctx.get()); |
| hits += SSL_CTX_sess_hits(ctx.get()); |
| misses += SSL_CTX_sess_misses(ctx.get()); |
| timeouts += SSL_CTX_sess_timeouts(ctx.get()); |
| } |
| } |
| } |
| } |
| |
| // Store cumulative session statistics as gauges. These metrics represent cumulative |
| // counters semantically but are implemented as gauges because they need to be "set" |
| // to values read from external counter sources (OpenSSL and/or ATS session cache). |
| Metrics::Gauge::store(ssl_rsb.user_agent_sessions, sessions); |
| Metrics::Gauge::store(ssl_rsb.user_agent_session_hit, hits); |
| Metrics::Gauge::store(ssl_rsb.user_agent_session_miss, misses); |
| Metrics::Gauge::store(ssl_rsb.user_agent_session_timeout, timeouts); |
| } |
| |
| static void |
| add_cipher_stat(const char *cipherName, const std::string &statName) |
| { |
| // If not already registered ... |
| if (cipherName && cipher_map.find(cipherName) == cipher_map.end()) { |
| Metrics::Counter::AtomicType *metric = Metrics::Counter::createPtr(statName); |
| |
| cipher_map.emplace(cipherName, metric); |
| Dbg(dbg_ctl_ssl, "registering SSL cipher metric '%s'", statName.c_str()); |
| } |
| } |
| |
| void |
| SSLInitializeStatistics() |
| { |
| // For now, register with the librecords global sync. |
| RecRegNewSyncStatSync(SSLPeriodicMetricsUpdate); |
| |
| ssl_rsb.cert_compress_zlib = Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zlib"); |
| ssl_rsb.cert_compress_zlib_failure = Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zlib_failure"); |
| ssl_rsb.cert_compress_cache_hit = Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.cache_hit"); |
| ssl_rsb.cert_decompress_zlib = Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zlib"); |
| ssl_rsb.cert_decompress_zlib_failure = Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zlib_failure"); |
| ssl_rsb.cert_compress_brotli = Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.brotli"); |
| ssl_rsb.cert_compress_brotli_failure = Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.brotli_failure"); |
| ssl_rsb.cert_decompress_brotli = Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.brotli"); |
| ssl_rsb.cert_decompress_brotli_failure = Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.brotli_failure"); |
| ssl_rsb.cert_compress_zstd = Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zstd"); |
| ssl_rsb.cert_compress_zstd_failure = Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zstd_failure"); |
| ssl_rsb.cert_decompress_zstd = Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zstd"); |
| ssl_rsb.cert_decompress_zstd_failure = Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zstd_failure"); |
| ssl_rsb.early_data_received_count = Metrics::Counter::createPtr("proxy.process.ssl.early_data_received"); |
| ssl_rsb.error_async = Metrics::Counter::createPtr("proxy.process.ssl.ssl_error_async"); |
| ssl_rsb.error_ssl = Metrics::Counter::createPtr("proxy.process.ssl.ssl_error_ssl"); |
| ssl_rsb.error_syscall = Metrics::Counter::createPtr("proxy.process.ssl.ssl_error_syscall"); |
| ssl_rsb.ocsp_refresh_cert_failure = Metrics::Counter::createPtr("proxy.process.ssl.ssl_ocsp_refresh_cert_failure"); |
| ssl_rsb.ocsp_refreshed_cert = Metrics::Counter::createPtr("proxy.process.ssl.ssl_ocsp_refreshed_cert"); |
| ssl_rsb.ocsp_revoked_cert = Metrics::Counter::createPtr("proxy.process.ssl.ssl_ocsp_revoked_cert"); |
| ssl_rsb.ocsp_unknown_cert = Metrics::Counter::createPtr("proxy.process.ssl.ssl_ocsp_unknown_cert"); |
| ssl_rsb.origin_server_bad_cert = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_bad_cert"); |
| ssl_rsb.origin_server_cert_verify_failed = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_cert_verify_failed"); |
| ssl_rsb.origin_server_decryption_failed = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_decryption_failed"); |
| ssl_rsb.origin_server_expired_cert = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_expired_cert"); |
| ssl_rsb.origin_server_other_errors = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_other_errors"); |
| ssl_rsb.origin_server_revoked_cert = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_revoked_cert"); |
| ssl_rsb.origin_server_unknown_ca = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_unknown_ca"); |
| ssl_rsb.origin_server_unknown_cert = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_unknown_cert"); |
| ssl_rsb.origin_server_wrong_version = Metrics::Counter::createPtr("proxy.process.ssl.origin_server_wrong_version"); |
| ssl_rsb.origin_session_reused_count = Metrics::Counter::createPtr("proxy.process.ssl.origin_session_reused"); |
| ssl_rsb.sni_name_set_failure = Metrics::Counter::createPtr("proxy.process.ssl.ssl_sni_name_set_failure"); |
| ssl_rsb.origin_session_cache_hit = Metrics::Counter::createPtr("proxy.process.ssl.ssl_origin_session_cache_hit"); |
| ssl_rsb.origin_session_cache_miss = Metrics::Counter::createPtr("proxy.process.ssl.ssl_origin_session_cache_miss"); |
| ssl_rsb.origin_session_cache_timeout = Metrics::Counter::createPtr("proxy.process.ssl.ssl_origin_session_cache_timeout"); |
| ssl_rsb.session_cache_eviction = Metrics::Counter::createPtr("proxy.process.ssl.ssl_session_cache_eviction"); |
| ssl_rsb.session_cache_hit = Metrics::Counter::createPtr("proxy.process.ssl.ssl_session_cache_hit"); |
| ssl_rsb.session_cache_lock_contention = Metrics::Counter::createPtr("proxy.process.ssl.ssl_session_cache_lock_contention"); |
| ssl_rsb.session_cache_miss = Metrics::Counter::createPtr("proxy.process.ssl.ssl_session_cache_miss"); |
| ssl_rsb.session_cache_new_session = Metrics::Counter::createPtr("proxy.process.ssl.ssl_session_cache_new_session"); |
| ssl_rsb.total_attempts_handshake_count_in = Metrics::Counter::createPtr("proxy.process.ssl.total_attempts_handshake_count_in"); |
| ssl_rsb.total_attempts_handshake_count_out = Metrics::Counter::createPtr("proxy.process.ssl.total_attempts_handshake_count_out"); |
| ssl_rsb.total_dyn_def_tls_record_count = Metrics::Counter::createPtr("proxy.process.ssl.default_record_size_count"); |
| ssl_rsb.total_dyn_max_tls_record_count = Metrics::Counter::createPtr("proxy.process.ssl.max_record_size_count"); |
| ssl_rsb.total_dyn_redo_tls_record_count = Metrics::Counter::createPtr("proxy.process.ssl.redo_record_size_count"); |
| ssl_rsb.total_handshake_time = Metrics::Counter::createPtr("proxy.process.ssl.total_handshake_time"); |
| ssl_rsb.total_sslv3 = Metrics::Counter::createPtr("proxy.process.ssl.ssl_total_sslv3"); |
| ssl_rsb.total_success_handshake_count_in = Metrics::Counter::createPtr("proxy.process.ssl.total_success_handshake_count_in"); |
| ssl_rsb.total_success_handshake_count_out = Metrics::Counter::createPtr("proxy.process.ssl.total_success_handshake_count_out"); |
| ssl_rsb.handshake_sign_rsa = Metrics::Counter::createPtr("proxy.process.ssl.handshake_sign_rsa"); |
| ssl_rsb.handshake_sign_ecdsa = Metrics::Counter::createPtr("proxy.process.ssl.handshake_sign_ecdsa"); |
| ssl_rsb.handshake_sign_other = Metrics::Counter::createPtr("proxy.process.ssl.handshake_sign_other"); |
| ssl_rsb.connections_closed = Metrics::Counter::createPtr("proxy.process.ssl.connections_closed"); |
| ssl_rsb.total_ticket_keys_renewed = Metrics::Counter::createPtr("proxy.process.ssl.total_ticket_keys_renewed"); |
| ssl_rsb.total_tickets_created = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_created"); |
| ssl_rsb.total_tickets_not_found = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_not_found"); |
| ssl_rsb.total_tickets_no_certificate = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_no_certificate"); |
| ssl_rsb.total_tickets_renewed = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_renewed"); |
| ssl_rsb.total_tickets_verified = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_verified"); |
| ssl_rsb.total_tickets_verified_old_key = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_verified_old_key"); |
| ssl_rsb.total_tlsv1 = Metrics::Counter::createPtr("proxy.process.ssl.ssl_total_tlsv1"); |
| ssl_rsb.total_tlsv11 = Metrics::Counter::createPtr("proxy.process.ssl.ssl_total_tlsv11"); |
| ssl_rsb.total_tlsv12 = Metrics::Counter::createPtr("proxy.process.ssl.ssl_total_tlsv12"); |
| ssl_rsb.total_tlsv13 = Metrics::Counter::createPtr("proxy.process.ssl.ssl_total_tlsv13"); |
| ssl_rsb.user_agent_bad_cert = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_bad_cert"); |
| ssl_rsb.user_agent_cert_verify_failed = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_cert_verify_failed"); |
| ssl_rsb.user_agent_decryption_failed = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_decryption_failed"); |
| ssl_rsb.user_agent_decryption_failed_or_bad_record_mac = |
| Metrics::Counter::createPtr("proxy.process.ssl.user_agent_decryption_failed_or_bad_record_mac"); |
| ssl_rsb.user_agent_expired_cert = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_expired_cert"); |
| ssl_rsb.user_agent_http_request = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_http_request"); |
| ssl_rsb.user_agent_inappropriate_fallback = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_inappropriate_fallback"); |
| ssl_rsb.user_agent_no_shared_cipher = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_no_shared_cipher"); |
| ssl_rsb.user_agent_other_errors = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_other_errors"); |
| ssl_rsb.user_agent_revoked_cert = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_revoked_cert"); |
| ssl_rsb.user_agent_session_hit = Metrics::Gauge::createPtr("proxy.process.ssl.user_agent_session_hit"); |
| ssl_rsb.user_agent_session_miss = Metrics::Gauge::createPtr("proxy.process.ssl.user_agent_session_miss"); |
| ssl_rsb.user_agent_session_timeout = Metrics::Gauge::createPtr("proxy.process.ssl.user_agent_session_timeout"); |
| ssl_rsb.user_agent_sessions = Metrics::Gauge::createPtr("proxy.process.ssl.user_agent_sessions"); |
| ssl_rsb.user_agent_unknown_ca = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_unknown_ca"); |
| ssl_rsb.user_agent_unknown_cert = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_unknown_cert"); |
| ssl_rsb.user_agent_version_too_high = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_version_too_high"); |
| ssl_rsb.user_agent_version_too_low = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_version_too_low"); |
| ssl_rsb.user_agent_wrong_version = Metrics::Counter::createPtr("proxy.process.ssl.user_agent_wrong_version"); |
| ssl_rsb.tls_handshake_bytes_in_total = Metrics::Counter::createPtr("proxy.process.ssl.total_handshake_bytes_read_in"); |
| ssl_rsb.tls_handshake_bytes_out_total = Metrics::Counter::createPtr("proxy.process.ssl.total_handshake_bytes_write_out"); |
| ssl_rsb.ssl_multicert_load_failures = Metrics::Counter::createPtr("proxy.process.ssl.ssl_multicert_load_failures"); |
| |
| #if defined(OPENSSL_IS_BORINGSSL) |
| size_t n = SSL_get_all_cipher_names(nullptr, 0); |
| std::vector<const char *> cipher_list(n); |
| SSL_get_all_cipher_names(cipher_list.data(), cipher_list.size()); |
| for (auto cipher_name : cipher_list) { |
| if (UNKNOWN_CIPHER.compare(cipher_name) == 0) { |
| continue; |
| } |
| |
| std::string stat_name = "proxy.process.ssl.cipher.user_agent." + std::string(cipher_name); |
| |
| add_cipher_stat(cipher_name, stat_name); |
| } |
| #else |
| // Acquire the loaded SSL certificate configuration to enumerate ciphers and groups. |
| // This must be called AFTER SSLCertificateConfig::startup(). |
| SSLCertificateConfig::scoped_config lookup; |
| auto default_ctx = lookup ? lookup->defaultContext() : nullptr; |
| if (!default_ctx) { |
| Dbg(dbg_ctl_ssl, "No SSL configuration, skipping cipher/group statistics initialization"); |
| return; |
| } |
| |
| SSL_CTX *ctx = default_ctx.get(); |
| SSL *ssl = SSL_new(ctx); |
| STACK_OF(SSL_CIPHER) *ciphers = SSL_get_ciphers(ssl); |
| |
| for (int index = 0; index < static_cast<int>(sk_SSL_CIPHER_num(ciphers)); index++) { |
| SSL_CIPHER *cipher = const_cast<SSL_CIPHER *>(sk_SSL_CIPHER_value(ciphers, index)); |
| const char *cipherName = SSL_CIPHER_get_name(cipher); |
| std::string statName = "proxy.process.ssl.cipher.user_agent." + std::string(cipherName); |
| |
| add_cipher_stat(cipherName, statName); |
| } |
| |
| // TLS Group |
| #if HAVE_SSL_CTX_GET0_IMPLEMENTED_GROUPS |
| STACK_OF(OPENSSL_CSTRING) *group_names = sk_OPENSSL_CSTRING_new_null(); |
| if (group_names == nullptr) { |
| Error("Failed to allocate stack for TLS group names"); |
| } else { |
| constexpr int ALL_GROUPS = 1; |
| Dbg(dbg_ctl_ssl, "Calling SSL_CTX_get0_implemented_groups on loaded SSL context"); |
| if (SSL_CTX_get0_implemented_groups(ctx, ALL_GROUPS, group_names) != 1) { |
| Error("Failed to get implemented groups via SSL_CTX_get0_implemented_groups"); |
| } |
| int const num_groups = sk_OPENSSL_CSTRING_num(group_names); |
| Dbg(dbg_ctl_ssl, "SSL_CTX_get0_implemented_groups returned %d groups", num_groups); |
| |
| for (int index = 0; index < num_groups; index++) { |
| const char *name = sk_OPENSSL_CSTRING_value(group_names, index); |
| if (name == nullptr) { |
| Error("NULL group name returned for index %d in SSL_CTX_get0_implemented_groups", index); |
| continue; |
| } |
| add_group_stat<std::string>(name, name); |
| } |
| |
| // Note: We don't free group_names as the strings are owned by OpenSSL's internal structures. |
| sk_OPENSSL_CSTRING_free(group_names); |
| } |
| |
| // Add "OTHER" for groups not discovered. |
| add_group_stat<std::string>("OTHER", "OTHER"); |
| #elif HAVE_SSL_GET_NEGOTIATED_GROUP |
| // Use static NID table for group registration. |
| for (auto group : TLS_GROUPS) { |
| add_group_stat<int>(group.nid, group.name); |
| } |
| #endif // HAVE_SSL_CTX_GET0_IMPLEMENTED_GROUPS or HAVE_SSL_GET_NEGOTIATED_GROUP |
| |
| SSL_free(ssl); |
| #endif |
| |
| // Add "OTHER" for ciphers not on the map |
| add_cipher_stat(SSL_CIPHER_STAT_OTHER.c_str(), "proxy.process.ssl.cipher.user_agent." + SSL_CIPHER_STAT_OTHER); |
| |
| #if defined(OPENSSL_IS_BORINGSSL) |
| size_t list_size = SSL_get_all_group_names(nullptr, 0); |
| std::vector<const char *> group_list(list_size); |
| SSL_get_all_group_names(group_list.data(), group_list.size()); |
| |
| for (const char *name : group_list) { |
| add_group_stat<std::string>(name, name); |
| } |
| #endif // OPENSSL_IS_BORINGSSL |
| } |