diff --git a/source/common/quic/BUILD b/source/common/quic/BUILD index 9e4dae98192f..ec301be6b985 100644 --- a/source/common/quic/BUILD +++ b/source/common/quic/BUILD @@ -112,6 +112,17 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "envoy_quic_session_cache_lib", + srcs = ["envoy_quic_session_cache.cc"], + hdrs = ["envoy_quic_session_cache.h"], + external_deps = ["quiche_quic_platform"], + tags = ["nofips"], + deps = [ + "@com_googlesource_quiche//:quic_core_crypto_crypto_handshake_lib", + ], +) + envoy_cc_library( name = "spdy_server_push_utils_for_envoy_lib", srcs = ["spdy_server_push_utils_for_envoy.cc"], @@ -150,6 +161,7 @@ envoy_cc_library( ":envoy_quic_connection_helper_lib", ":envoy_quic_proof_verifier_lib", ":envoy_quic_server_session_lib", + ":envoy_quic_session_cache_lib", ":envoy_quic_utils_lib", "//include/envoy/http:codec_interface", "//include/envoy/registry", diff --git a/source/common/quic/client_connection_factory_impl.cc b/source/common/quic/client_connection_factory_impl.cc index ffb6951237f9..e41c8089c549 100644 --- a/source/common/quic/client_connection_factory_impl.cc +++ b/source/common/quic/client_connection_factory_impl.cc @@ -1,5 +1,6 @@ #include "common/quic/client_connection_factory_impl.h" +#include "common/quic/envoy_quic_session_cache.h" #include "common/quic/quic_transport_socket_factory.h" namespace Envoy { @@ -20,9 +21,10 @@ PersistentQuicInfoImpl::PersistentQuicInfoImpl( : conn_helper_(dispatcher), alarm_factory_(dispatcher, *conn_helper_.GetClock()), server_id_{getConfig(transport_socket_factory).serverNameIndication(), static_cast(server_addr->ip()->port()), false}, - crypto_config_( - std::make_unique(std::make_unique( - stats_scope, getConfig(transport_socket_factory), time_source))) { + crypto_config_(std::make_unique( + std::make_unique(stats_scope, getConfig(transport_socket_factory), + time_source), + std::make_unique(time_source))) { quiche::FlagRegistry::getInstance(); } diff --git a/source/common/quic/envoy_quic_session_cache.cc b/source/common/quic/envoy_quic_session_cache.cc new file mode 100644 index 000000000000..7404f132f153 --- /dev/null +++ b/source/common/quic/envoy_quic_session_cache.cc @@ -0,0 +1,171 @@ +#include "common/quic/envoy_quic_session_cache.h" + +namespace Envoy { +namespace Quic { +namespace { + +// This value was chosen arbitrarily. We can make this configurable if needed. +// TODO(14829) ensure this is tested and scaled for upstream. +constexpr size_t MaxSessionCacheEntries = 1024; + +// Returns false if the SSL session doesn't exist or it is expired. +bool isSessionValid(SSL_SESSION* session, SystemTime now) { + if (session == nullptr) { + return false; + } + const time_t now_time_t = std::chrono::system_clock::to_time_t(now); + if (now_time_t < 0) { + return false; + } + const uint64_t now_u64 = static_cast(now_time_t); + const uint64_t session_time = SSL_SESSION_get_time(session); + const uint64_t session_expiration = session_time + SSL_SESSION_get_timeout(session); + // now_u64 may be slightly behind because of differences in how time is calculated at this layer + // versus BoringSSL. Add a second of wiggle room to account for this. + return session_time <= now_u64 + 1 && now_u64 < session_expiration; +} + +bool doApplicationStatesMatch(const quic::ApplicationState* state, + const quic::ApplicationState* other) { + if (state == other) { + return true; + } + if ((state != nullptr && other == nullptr) || (state == nullptr && other != nullptr)) { + return false; + } + return (*state == *other); +} + +} // namespace + +EnvoyQuicSessionCache::EnvoyQuicSessionCache(TimeSource& time_source) : time_source_(time_source) {} + +EnvoyQuicSessionCache::~EnvoyQuicSessionCache() = default; + +void EnvoyQuicSessionCache::Insert(const quic::QuicServerId& server_id, + bssl::UniquePtr session, + const quic::TransportParameters& params, + const quic::ApplicationState* application_state) { + auto it = cache_.find(server_id); + if (it == cache_.end()) { + // New server ID, add a new entry. + createAndInsertEntry(server_id, std::move(session), params, application_state); + return; + } + Entry& entry = it->second; + if (params == *entry.params && + doApplicationStatesMatch(application_state, entry.application_state.get())) { + // The states are both the same, so we only need to insert the session. + entry.pushSession(std::move(session)); + return; + } + // States are different, replace the entry. + cache_.erase(it); + createAndInsertEntry(server_id, std::move(session), params, application_state); +} + +std::unique_ptr +EnvoyQuicSessionCache::Lookup(const quic::QuicServerId& server_id, const SSL_CTX* /*ctx*/) { + auto it = cache_.find(server_id); + if (it == cache_.end()) { + return nullptr; + } + Entry& entry = it->second; + const SystemTime system_time = time_source_.systemTime(); + if (!isSessionValid(entry.peekSession(), system_time)) { + cache_.erase(it); + return nullptr; + } + auto state = std::make_unique(); + state->tls_session = entry.popSession(); + if (entry.params != nullptr) { + state->transport_params = std::make_unique(*entry.params); + } + if (entry.application_state != nullptr) { + state->application_state = std::make_unique(*entry.application_state); + } + return state; +} + +void EnvoyQuicSessionCache::ClearEarlyData(const quic::QuicServerId& server_id) { + auto it = cache_.find(server_id); + if (it == cache_.end()) { + return; + } + for (bssl::UniquePtr& session : it->second.sessions) { + if (session != nullptr) { + session.reset(SSL_SESSION_copy_without_early_data(session.get())); + } + } +} + +void EnvoyQuicSessionCache::prune() { + quic::QuicServerId oldest_id; + uint64_t oldest_expiration = std::numeric_limits::max(); + const SystemTime system_time = time_source_.systemTime(); + auto it = cache_.begin(); + while (it != cache_.end()) { + Entry& entry = it->second; + SSL_SESSION* session = entry.peekSession(); + if (!isSessionValid(session, system_time)) { + it = cache_.erase(it); + } else { + if (cache_.size() >= MaxSessionCacheEntries) { + // Only track the oldest session if we are at the size limit. + const uint64_t session_expiration = + SSL_SESSION_get_time(session) + SSL_SESSION_get_timeout(session); + if (session_expiration < oldest_expiration) { + oldest_expiration = session_expiration; + oldest_id = it->first; + } + } + ++it; + } + } + if (cache_.size() >= MaxSessionCacheEntries) { + cache_.erase(oldest_id); + } +} + +void EnvoyQuicSessionCache::createAndInsertEntry(const quic::QuicServerId& server_id, + bssl::UniquePtr session, + const quic::TransportParameters& params, + const quic::ApplicationState* application_state) { + prune(); + ASSERT(cache_.size() < MaxSessionCacheEntries); + Entry entry; + entry.pushSession(std::move(session)); + entry.params = std::make_unique(params); + if (application_state != nullptr) { + entry.application_state = std::make_unique(*application_state); + } + cache_.insert(std::make_pair(server_id, std::move(entry))); +} + +size_t EnvoyQuicSessionCache::size() const { return cache_.size(); } + +EnvoyQuicSessionCache::Entry::Entry() = default; +EnvoyQuicSessionCache::Entry::Entry(Entry&&) noexcept = default; +EnvoyQuicSessionCache::Entry::~Entry() = default; + +void EnvoyQuicSessionCache::Entry::pushSession(bssl::UniquePtr session) { + if (sessions[0] != nullptr) { + sessions[1] = std::move(sessions[0]); + } + sessions[0] = std::move(session); +} + +bssl::UniquePtr EnvoyQuicSessionCache::Entry::popSession() { + if (sessions[0] == nullptr) { + return nullptr; + } + bssl::UniquePtr session = std::move(sessions[0]); + sessions[0] = std::move(sessions[1]); + sessions[1] = nullptr; + return session; +} + +SSL_SESSION* EnvoyQuicSessionCache::Entry::peekSession() { return sessions[0].get(); } + +} // namespace Quic +} // namespace Envoy diff --git a/source/common/quic/envoy_quic_session_cache.h b/source/common/quic/envoy_quic_session_cache.h new file mode 100644 index 000000000000..c15fe0d0e658 --- /dev/null +++ b/source/common/quic/envoy_quic_session_cache.h @@ -0,0 +1,65 @@ +#pragma once + +#include "envoy/common/time.h" + +#include "quiche/quic/core/crypto/quic_crypto_client_config.h" + +namespace Envoy { +namespace Quic { + +// Implementation of quic::SessionCache using an Envoy time source. +class EnvoyQuicSessionCache : public quic::SessionCache { +public: + explicit EnvoyQuicSessionCache(TimeSource& time_source); + ~EnvoyQuicSessionCache() override; + + // From quic::SessionCache. + void Insert(const quic::QuicServerId& server_id, bssl::UniquePtr session, + const quic::TransportParameters& params, + const quic::ApplicationState* application_state) override; + std::unique_ptr Lookup(const quic::QuicServerId& server_id, + const SSL_CTX* ctx) override; + void ClearEarlyData(const quic::QuicServerId& server_id) override; + + // Returns number of entries in the cache. + size_t size() const; + +private: + struct Entry { + Entry(); + Entry(Entry&&) noexcept; + ~Entry(); + + // Adds a new session onto sessions, dropping the oldest one if two are + // already stored. + void pushSession(bssl::UniquePtr session); + + // Retrieves and removes the latest session from the entry. + bssl::UniquePtr popSession(); + + SSL_SESSION* peekSession(); + + // We only save the last two sessions per server as that is sufficient in practice. This is + // because we only need one to create a new connection, and that new connection should send + // us another ticket. We keep two instead of one in case that connection attempt fails. + bssl::UniquePtr sessions[2]; + std::unique_ptr params; + std::unique_ptr application_state; + }; + + // Remove all entries that are no longer valid. If all entries were valid but the cache is at its + // size limit, instead remove the oldest entry. This walks the entire list of entries. + void prune(); + + // Creates a new entry and insert into cache_. This walks the entire list of entries. + void createAndInsertEntry(const quic::QuicServerId& server_id, + bssl::UniquePtr session, + const quic::TransportParameters& params, + const quic::ApplicationState* application_state); + + std::map cache_; + TimeSource& time_source_; +}; + +} // namespace Quic +} // namespace Envoy diff --git a/test/common/quic/BUILD b/test/common/quic/BUILD index 53967319be88..b15ff0a8c7f7 100644 --- a/test/common/quic/BUILD +++ b/test/common/quic/BUILD @@ -54,6 +54,16 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "envoy_quic_session_cache_test", + srcs = ["envoy_quic_session_cache_test.cc"], + external_deps = ["quiche_quic_platform"], + tags = ["nofips"], + deps = [ + "//source/common/quic:envoy_quic_session_cache_lib", + ], +) + envoy_cc_test( name = "envoy_quic_proof_verifier_test", srcs = ["envoy_quic_proof_verifier_test.cc"], diff --git a/test/common/quic/envoy_quic_session_cache_test.cc b/test/common/quic/envoy_quic_session_cache_test.cc new file mode 100644 index 000000000000..d2129d14cf10 --- /dev/null +++ b/test/common/quic/envoy_quic_session_cache_test.cc @@ -0,0 +1,342 @@ +#include "common/quic/envoy_quic_session_cache.h" + +#include "absl/strings/escaping.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Quic { +namespace { + +constexpr uint32_t Timeout = 1000; +const quic::QuicVersionLabel FakeVersionLabel = 0x01234567; +const quic::QuicVersionLabel FakeVersionLabel2 = 0x89ABCDEF; +const uint64_t FakeIdleTimeoutMilliseconds = 12012; +const uint8_t FakeStatelessResetTokenData[16] = {0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F}; +const uint64_t FakeMaxPacketSize = 9001; +const uint64_t FakeInitialMaxData = 101; +const bool FakeDisableMigration = true; +const auto CustomParameter1 = static_cast(0xffcd); +const char* CustomParameter1Value = "foo"; +const auto CustomParameter2 = static_cast(0xff34); +const char* CustomParameter2Value = "bar"; + +std::vector createFakeStatelessResetToken() { + return std::vector(FakeStatelessResetTokenData, + FakeStatelessResetTokenData + sizeof(FakeStatelessResetTokenData)); +} + +// Make a TransportParameters that has a few fields set to help test comparison. +std::unique_ptr makeFakeTransportParams() { + auto params = std::make_unique(); + params->perspective = quic::Perspective::IS_CLIENT; + params->version = FakeVersionLabel; + params->supported_versions.push_back(FakeVersionLabel); + params->supported_versions.push_back(FakeVersionLabel2); + params->max_idle_timeout_ms.set_value(FakeIdleTimeoutMilliseconds); + params->stateless_reset_token = createFakeStatelessResetToken(); + params->max_udp_payload_size.set_value(FakeMaxPacketSize); + params->initial_max_data.set_value(FakeInitialMaxData); + params->disable_active_migration = FakeDisableMigration; + params->custom_parameters[CustomParameter1] = CustomParameter1Value; + params->custom_parameters[CustomParameter2] = CustomParameter2Value; + return params; +} + +// Generated with SSL_SESSION_to_bytes. +static constexpr char CachedSession[] = + "3082068702010102020304040213010420b9c2a657e565db0babd09e192a9fc4d768fbd706" + "9f03f9278a4a0be62392e55b0420d87ed2ab8cafc986fd2e288bd2d654cd57c3a2bed1d532" + "20726e55fed39d021ea10602045ed16771a205020302a300a382025f3082025b30820143a0" + "03020102020104300d06092a864886f70d01010b0500302c3110300e060355040a13074163" + "6d6520436f311830160603550403130f496e7465726d656469617465204341301e170d3133" + "303130313130303030305a170d3233313233313130303030305a302d3110300e060355040a" + "130741636d6520436f3119301706035504031310746573742e6578616d706c652e636f6d30" + "59301306072a8648ce3d020106082a8648ce3d030107034200040526220e77278300d06bc0" + "86aff4f999a828a2ed5cc75adc2972794befe885aa3a9b843de321b36b0a795289cebff1a5" + "428bad5e34665ce5e36daad08fb3ffd8a3523050300e0603551d0f0101ff04040302078030" + "130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301b06" + "03551d11041430128210746573742e6578616d706c652e636f6d300d06092a864886f70d01" + "010b050003820101008c1f1e380831b6437a8b9284d28d4ead38d9503a9fc936db89048aa2" + "edd6ec2fb830d962ef7a4f384e679504f4d5520f3272e0b9e702b110aff31711578fa5aeb1" + "11e9d184c994b0f97e7b17d1995f3f477f25bc1258398ec0ec729caed55d594a009f48093a" + "17f33a7f3bb6e420cc3499838398a421d93c7132efa8bee5ed2645cbc55179c400da006feb" + "761badd356cac3bd7a0e6b22a511106a355ec62a4c0ac2541d2996adb4a918c866d10c3e31" + "62039a91d4ce600b276740d833380b37f66866d261bf6efa8855e7ae6c7d12a8a864cd9a1f" + "4663e07714b0204e51bbc189a2d04c2a5043202379ff1c8cbf30cbb44fde4ee9a1c0c976dc" + "4943df2c132ca4020400aa7f047d494e534543555245003072020101020203040402130104" + "000420d87ed2ab8cafc986fd2e288bd2d654cd57c3a2bed1d53220726e55fed39d021ea106" + "02045ed16771a205020302a300a4020400b20302011db5060404bd909308b807020500ffff" + "ffffb9050203093a80ba07040568332d3238bb030101ffbc03040100b20302011db3820307" + "30820303308201eba003020102020102300d06092a864886f70d01010b050030243110300e" + "060355040a130741636d6520436f3110300e06035504031307526f6f74204341301e170d31" + "33303130313130303030305a170d3233313233313130303030305a302c3110300e06035504" + "0a130741636d6520436f311830160603550403130f496e7465726d65646961746520434130" + "820122300d06092a864886f70d01010105000382010f003082010a0282010100cd3550e70a" + "6880e52bf0012b93110c50f723e1d8d2ed489aea3b649f82fae4ad2396a8a19b31d1d64ab2" + "79f1c18003184154a5303a82bd57109cfd5d34fd19d3211bcb06e76640e1278998822dd72e" + "0d5c059a740d45de325e784e81b4c86097f08b2a8ce057f6b9db5a53641d27e09347d993ee" + "acf67be7d297b1a6853775ffaaf78fae924e300b5654fd32f99d3cd82e95f56417ff26d265" + "e2b1786c835d67a4d8ae896b6eb34b35a5b1033c209779ed0bf8de25a13a507040ae9e0475" + "a26a2f15845b08c3e0554e47dbbc7925b02e580dbcaaa6f2eecde6b8028c5b00b33d44d0a6" + "bfb3e72e9d4670de45d1bd79bdc0f2470b71286091c29873152db4b1f30203010001a33830" + "36300e0603551d0f0101ff04040302020430130603551d25040c300a06082b060105050703" + "01300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101" + "00bc4f8234860558dd404a626403819bfc759029d625a002143e75ebdb2898d1befdd326c3" + "4b14dc3507d732bb29af7e6af31552db53052a2be0d950efee5e0f699304231611ed8bf73a" + "6f216a904c6c2f1a2186d1ed08a8005a7914394d71e7d4b643c808f86365c5fecad8b52934" + "2d3b3f03447126d278d75b1dab3ed53f23e36e9b3d695f28727916e5ee56ce22d387c81f05" + "919b2a37bd4981eb67d9f57b7072285dbbb61f48b6b14768c069a092aad5a094cf295dafd2" + "3ca008f89a5f5ab37a56e5f68df45091c7cb85574677127087a2887ba3baa6d4fc436c6e40" + "40885e81621d38974f0c7f0d792418c5adebb10e92a165f8d79b169617ff575c0d4a85b506" + "0404bd909308b603010100b70402020403b807020500ffffffffb9050203093a80ba070405" + "68332d3238bb030101ff"; + +class FakeTimeSource : public TimeSource { +public: + // From TimeSource. + SystemTime systemTime() override { return fake_time_; } + MonotonicTime monotonicTime() override { + // EnvoyQuicSessionCache does not use monotonic time, return empty value. + ADD_FAILURE() << "Unexpected call to monotonicTime"; + return MonotonicTime(); + } + + void advance(int seconds) { fake_time_ += std::chrono::seconds(seconds); } + +private: + SystemTime fake_time_{}; +}; + +} // namespace + +class EnvoyQuicSessionCacheTest : public ::testing::Test { +public: + EnvoyQuicSessionCacheTest() + : ssl_ctx_(SSL_CTX_new(TLS_method())), cache_(time_source_), + params_(makeFakeTransportParams()) {} + +protected: + bssl::UniquePtr makeSession(uint32_t timeout = Timeout) { + std::string cached_session = + absl::HexStringToBytes(absl::string_view(CachedSession, sizeof(CachedSession))); + SSL_SESSION* session = + SSL_SESSION_from_bytes(reinterpret_cast(cached_session.data()), + cached_session.size(), ssl_ctx_.get()); + SSL_SESSION_set_time(session, std::chrono::system_clock::to_time_t(time_source_.systemTime())); + SSL_SESSION_set_timeout(session, timeout); + return bssl::UniquePtr(session); + } + + bssl::UniquePtr ssl_ctx_; + FakeTimeSource time_source_; + EnvoyQuicSessionCache cache_; + std::unique_ptr params_; +}; + +// Tests that simple insertion and lookup work correctly. +TEST_F(EnvoyQuicSessionCacheTest, SingleSession) { + bssl::UniquePtr session = makeSession(); + quic::QuicServerId id1("a.com", 443); + + std::unique_ptr params2 = makeFakeTransportParams(); + bssl::UniquePtr session2 = makeSession(); + SSL_SESSION* unowned2 = session2.get(); + quic::QuicServerId id2("b.com", 443); + + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); + EXPECT_EQ(nullptr, cache_.Lookup(id2, ssl_ctx_.get())); + EXPECT_EQ(0u, cache_.size()); + + cache_.Insert(id1, std::move(session), *params_, nullptr); + EXPECT_EQ(1u, cache_.size()); + std::unique_ptr resumption_state = cache_.Lookup(id1, ssl_ctx_.get()); + ASSERT_NE(resumption_state, nullptr); + ASSERT_NE(resumption_state->transport_params, nullptr); + EXPECT_EQ(*params_, *(resumption_state->transport_params)); + EXPECT_EQ(nullptr, cache_.Lookup(id2, ssl_ctx_.get())); + // No session is available for id1, even though the entry exists. + EXPECT_EQ(1u, cache_.size()); + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); + // Lookup() will trigger a deletion of invalid entry. + EXPECT_EQ(0u, cache_.size()); + + bssl::UniquePtr session3 = makeSession(); + SSL_SESSION* unowned3 = session3.get(); + quic::QuicServerId id3("c.com", 443); + cache_.Insert(id3, std::move(session3), *params_, nullptr); + cache_.Insert(id2, std::move(session2), *params2, nullptr); + EXPECT_EQ(2u, cache_.size()); + std::unique_ptr resumption_state2 = cache_.Lookup(id2, ssl_ctx_.get()); + ASSERT_NE(resumption_state2, nullptr); + EXPECT_EQ(unowned2, resumption_state2->tls_session.get()); + std::unique_ptr resumption_state3 = cache_.Lookup(id3, ssl_ctx_.get()); + ASSERT_NE(resumption_state3, nullptr); + EXPECT_EQ(unowned3, resumption_state3->tls_session.get()); + + // Verify that the cache is cleared after Lookups. + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); + EXPECT_EQ(nullptr, cache_.Lookup(id2, ssl_ctx_.get())); + EXPECT_EQ(nullptr, cache_.Lookup(id3, ssl_ctx_.get())); + EXPECT_EQ(0u, cache_.size()); +} + +TEST_F(EnvoyQuicSessionCacheTest, MultipleSessions) { + bssl::UniquePtr session = makeSession(); + quic::QuicServerId id1("a.com", 443); + bssl::UniquePtr session2 = makeSession(); + SSL_SESSION* unowned2 = session2.get(); + bssl::UniquePtr session3 = makeSession(); + SSL_SESSION* unowned3 = session3.get(); + + cache_.Insert(id1, std::move(session), *params_, nullptr); + cache_.Insert(id1, std::move(session2), *params_, nullptr); + cache_.Insert(id1, std::move(session3), *params_, nullptr); + // The latest session is popped first. + std::unique_ptr resumption_state1 = cache_.Lookup(id1, ssl_ctx_.get()); + ASSERT_NE(resumption_state1, nullptr); + EXPECT_EQ(unowned3, resumption_state1->tls_session.get()); + std::unique_ptr resumption_state2 = cache_.Lookup(id1, ssl_ctx_.get()); + ASSERT_NE(resumption_state2, nullptr); + EXPECT_EQ(unowned2, resumption_state2->tls_session.get()); + // Only two sessions are cache. + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); +} + +// Test that when a different TransportParameter is inserted for +// the same server id, the existing entry is removed. +TEST_F(EnvoyQuicSessionCacheTest, DifferentTransportParams) { + bssl::UniquePtr session = makeSession(); + quic::QuicServerId id1("a.com", 443); + bssl::UniquePtr session2 = makeSession(); + bssl::UniquePtr session3 = makeSession(); + SSL_SESSION* unowned3 = session3.get(); + + cache_.Insert(id1, std::move(session), *params_, nullptr); + cache_.Insert(id1, std::move(session2), *params_, nullptr); + // tweak the transport parameters a little bit. + params_->perspective = quic::Perspective::IS_SERVER; + cache_.Insert(id1, std::move(session3), *params_, nullptr); + std::unique_ptr resumption_state = cache_.Lookup(id1, ssl_ctx_.get()); + ASSERT_NE(resumption_state, nullptr); + EXPECT_EQ(unowned3, resumption_state->tls_session.get()); + EXPECT_EQ(*params_.get(), *resumption_state->transport_params); + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); +} + +TEST_F(EnvoyQuicSessionCacheTest, DifferentApplicationState) { + bssl::UniquePtr session = makeSession(); + quic::QuicServerId id1("a.com", 443); + bssl::UniquePtr session2 = makeSession(); + bssl::UniquePtr session3 = makeSession(); + SSL_SESSION* unowned3 = session3.get(); + quic::ApplicationState state; + state.push_back('a'); + + cache_.Insert(id1, std::move(session), *params_, &state); + cache_.Insert(id1, std::move(session2), *params_, &state); + cache_.Insert(id1, std::move(session3), *params_, nullptr); + std::unique_ptr resumption_state = cache_.Lookup(id1, ssl_ctx_.get()); + ASSERT_NE(resumption_state, nullptr); + EXPECT_EQ(unowned3, resumption_state->tls_session.get()); + EXPECT_EQ(nullptr, resumption_state->application_state); + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); +} + +TEST_F(EnvoyQuicSessionCacheTest, BothStatesDifferent) { + bssl::UniquePtr session = makeSession(); + quic::QuicServerId id1("a.com", 443); + bssl::UniquePtr session2 = makeSession(); + bssl::UniquePtr session3 = makeSession(); + SSL_SESSION* unowned3 = session3.get(); + quic::ApplicationState state; + state.push_back('a'); + + cache_.Insert(id1, std::move(session), *params_, &state); + cache_.Insert(id1, std::move(session2), *params_, &state); + params_->perspective = quic::Perspective::IS_SERVER; + cache_.Insert(id1, std::move(session3), *params_, nullptr); + std::unique_ptr resumption_state = cache_.Lookup(id1, ssl_ctx_.get()); + ASSERT_NE(resumption_state, nullptr); + EXPECT_EQ(unowned3, resumption_state->tls_session.get()); + EXPECT_EQ(*params_, *resumption_state->transport_params); + EXPECT_EQ(nullptr, resumption_state->application_state); + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); +} + +// When the size limit is exceeded, the oldest entry should be erased. +TEST_F(EnvoyQuicSessionCacheTest, SizeLimit) { + constexpr size_t size_limit = 1024; + std::array unowned_sessions; + for (size_t i = 0; i <= size_limit; i++) { + time_source_.advance(1); + bssl::UniquePtr session = makeSession(/*timeout=*/10000); + unowned_sessions[i] = session.get(); + quic::QuicServerId id(absl::StrCat("domain", i, ".example.com"), 443); + cache_.Insert(id, std::move(session), *params_, nullptr); + } + EXPECT_EQ(cache_.size(), size_limit); + // First entry has been removed. + quic::QuicServerId id0("domain0.example.com", 443); + EXPECT_EQ(nullptr, cache_.Lookup(id0, ssl_ctx_.get())); + // All other entries all present. + for (size_t i = 1; i <= size_limit; i++) { + quic::QuicServerId id(absl::StrCat("domain", i, ".example.com"), 443); + std::unique_ptr resumption_state = cache_.Lookup(id, ssl_ctx_.get()); + ASSERT_NE(resumption_state, nullptr) << i; + EXPECT_EQ(resumption_state->tls_session.get(), unowned_sessions[i]); + } +} + +TEST_F(EnvoyQuicSessionCacheTest, ClearEarlyData) { + SSL_CTX_set_early_data_enabled(ssl_ctx_.get(), 1); + bssl::UniquePtr session = makeSession(); + quic::QuicServerId id1("a.com", 443); + bssl::UniquePtr session2 = makeSession(); + + EXPECT_TRUE(SSL_SESSION_early_data_capable(session.get())); + EXPECT_TRUE(SSL_SESSION_early_data_capable(session2.get())); + + cache_.Insert(id1, std::move(session), *params_, nullptr); + cache_.Insert(id1, std::move(session2), *params_, nullptr); + + cache_.ClearEarlyData(id1); + + std::unique_ptr resumption_state = cache_.Lookup(id1, ssl_ctx_.get()); + ASSERT_NE(resumption_state, nullptr); + EXPECT_FALSE(SSL_SESSION_early_data_capable(resumption_state->tls_session.get())); + resumption_state = cache_.Lookup(id1, ssl_ctx_.get()); + EXPECT_FALSE(SSL_SESSION_early_data_capable(resumption_state->tls_session.get())); + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); +} + +// Expired session isn't considered valid and nullptr will be returned upon +// Lookup. +TEST_F(EnvoyQuicSessionCacheTest, Expiration) { + bssl::UniquePtr session = makeSession(); + quic::QuicServerId id1("a.com", 443); + + bssl::UniquePtr session2 = makeSession(3 * Timeout); + SSL_SESSION* unowned2 = session2.get(); + quic::QuicServerId id2("b.com", 443); + + cache_.Insert(id1, std::move(session), *params_, nullptr); + cache_.Insert(id2, std::move(session2), *params_, nullptr); + + EXPECT_EQ(2u, cache_.size()); + // Expire the session. + time_source_.advance(Timeout * 2); + // The entry has not been removed yet. + EXPECT_EQ(2u, cache_.size()); + + EXPECT_EQ(nullptr, cache_.Lookup(id1, ssl_ctx_.get())); + EXPECT_EQ(1u, cache_.size()); + std::unique_ptr resumption_state = cache_.Lookup(id2, ssl_ctx_.get()); + ASSERT_NE(resumption_state, nullptr); + EXPECT_EQ(unowned2, resumption_state->tls_session.get()); + EXPECT_EQ(1u, cache_.size()); +} + +} // namespace Quic +} // namespace Envoy diff --git a/test/integration/BUILD b/test/integration/BUILD index 7d0ed082a021..b4b02eec880f 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -1726,5 +1726,6 @@ envoy_cc_test( "@envoy_api//envoy/config/overload/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/transport_sockets/quic/v3:pkg_cc_proto", + "@com_googlesource_quiche//:quic_test_tools_session_peer_lib", ]), ) diff --git a/test/integration/quic_http_integration_test.cc b/test/integration/quic_http_integration_test.cc index a3d3910d7a17..78799cde11a7 100644 --- a/test/integration/quic_http_integration_test.cc +++ b/test/integration/quic_http_integration_test.cc @@ -21,6 +21,7 @@ #include "quiche/quic/core/http/quic_client_push_promise_index.h" #include "quiche/quic/core/quic_utils.h" #include "quiche/quic/test_tools/quic_test_utils.h" +#include "quiche/quic/test_tools/quic_session_peer.h" #if defined(__GNUC__) #pragma GCC diagnostic pop @@ -273,6 +274,36 @@ TEST_P(QuicHttpIntegrationTest, GetRequestAndEmptyResponse) { testRouterHeaderOnlyRequestAndResponse(); } +TEST_P(QuicHttpIntegrationTest, ZeroRtt) { + // Make sure both connections use the same PersistentQuicInfoImpl. + concurrency_ = 1; + initialize(); + // Start the first connection. + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + // Send a complete request on the first connection. + auto response1 = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + waitForNextUpstreamRequest(0); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response1->waitForEndStream()); + // Close the first connection. + codec_client_->close(); + // Start a second connection. + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + // Send a complete request on the second connection. + auto response2 = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + waitForNextUpstreamRequest(0); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response2->waitForEndStream()); + // Ensure 0-RTT was used by second connection. + EnvoyQuicClientSession* quic_session = + static_cast(codec_client_->connection()); + EXPECT_TRUE(static_cast( + quic::test::QuicSessionPeer::GetMutableCryptoStream(quic_session)) + ->EarlyDataAccepted()); + // Close the second connection. + codec_client_->close(); +} + TEST_P(QuicHttpIntegrationTest, GetRequestAndResponseWithBody) { initialize(); sendRequestAndVerifyResponse(default_request_headers_, /*request_size=*/0,