| #ifndef P2P_DTLS_DTLS_ICE_INTEGRATION_FIXTURE_H_ |
| #define P2P_DTLS_DTLS_ICE_INTEGRATION_FIXTURE_H_ |
| /* |
| * Copyright 2025 The WebRTC Project Authors. All rights reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| #include <atomic> |
| #include <cstdint> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "api/candidate.h" |
| #include "api/crypto/crypto_options.h" |
| #include "api/environment/environment.h" |
| #include "api/environment/environment_factory.h" |
| #include "api/field_trials.h" |
| #include "api/ice_transport_interface.h" |
| #include "api/make_ref_counted.h" |
| #include "api/numerics/samples_stats_counter.h" |
| #include "api/scoped_refptr.h" |
| #include "api/test/create_network_emulation_manager.h" |
| #include "api/test/network_emulation_manager.h" |
| #include "api/test/simulated_network.h" |
| #include "api/test/time_controller.h" |
| #include "api/units/data_rate.h" |
| #include "api/units/time_delta.h" |
| #include "api/units/timestamp.h" |
| #include "p2p/base/basic_packet_socket_factory.h" |
| #include "p2p/base/ice_transport_internal.h" |
| #include "p2p/base/p2p_transport_channel.h" |
| #include "p2p/base/port_allocator.h" |
| #include "p2p/base/transport_description.h" |
| #include "p2p/client/basic_port_allocator.h" |
| #include "p2p/dtls/dtls_transport.h" |
| #include "p2p/test/fake_ice_lite_agent.h" |
| #include "p2p/test/fake_ice_transport.h" |
| #include "rtc_base/async_packet_socket.h" |
| #include "rtc_base/checks.h" |
| #include "rtc_base/fake_network.h" |
| #include "rtc_base/logging.h" |
| #include "rtc_base/network.h" |
| #include "rtc_base/rtc_certificate.h" |
| #include "rtc_base/socket_address.h" |
| #include "rtc_base/ssl_fingerprint.h" |
| #include "rtc_base/ssl_identity.h" |
| #include "rtc_base/ssl_stream_adapter.h" |
| #include "rtc_base/thread.h" |
| #include "rtc_base/virtual_socket_server.h" |
| #include "test/create_test_field_trials.h" |
| #include "test/time_controller/simulated_time_controller.h" |
| #include "test/wait_until.h" |
| |
| namespace webrtc { |
| namespace dtls_ice_integration_fixture { |
| |
| constexpr int kDefaultTimeout = 30000; |
| |
| struct EndpointConfig { |
| SSLProtocolVersion max_protocol_version; |
| IceRole ice_role; |
| SSLRole ssl_role; |
| bool ice_lite = false; |
| bool dtls_in_stun = false; |
| bool pqc = false; |
| |
| template <typename Sink> |
| friend void AbslStringify(Sink& sink, const EndpointConfig& config) { |
| sink.Append("[ dtls: "); |
| sink.Append(config.ssl_role == SSL_SERVER ? "server/" : "client/"); |
| switch (config.max_protocol_version) { |
| case SSL_PROTOCOL_DTLS_10: |
| sink.Append("1.0"); |
| break; |
| case SSL_PROTOCOL_DTLS_12: |
| sink.Append("1.2"); |
| break; |
| case SSL_PROTOCOL_DTLS_13: |
| sink.Append("1.3"); |
| break; |
| default: |
| sink.Append("<unknown>"); |
| break; |
| } |
| if (config.ice_role == ICEROLE_CONTROLLED) { |
| if (config.ice_lite) { |
| sink.Append(" ice: lite"); |
| } else { |
| sink.Append(" ice: controlled"); |
| } |
| } |
| absl::Format(&sink, " pqc: %u dtls_in_stun: %u ", config.pqc, |
| config.dtls_in_stun); |
| sink.Append(" ]"); |
| } |
| }; |
| |
| struct TestConfig { |
| int pct_loss = -1; |
| int client_interface_count = -1; |
| int server_interface_count = -1; |
| |
| bool ice_lite = false; |
| bool client_ssl_client = true; |
| SSLProtocolVersion protocol_version; |
| |
| // Configuration for the endpoint acting as the ICE controllering. |
| EndpointConfig client_config; |
| // Configuration for the endpoint acting as the ICE controlled. |
| EndpointConfig server_config; |
| |
| TestConfig& fix() { |
| client_config.ice_role = ICEROLE_CONTROLLING; |
| server_config.ice_role = ICEROLE_CONTROLLED; |
| client_config.ice_lite = ice_lite; |
| server_config.ice_lite = ice_lite; |
| if (client_ssl_client) { |
| client_config.ssl_role = SSL_CLIENT; |
| server_config.ssl_role = SSL_SERVER; |
| } else { |
| client_config.ssl_role = SSL_SERVER; |
| server_config.ssl_role = SSL_CLIENT; |
| } |
| client_config.max_protocol_version = protocol_version; |
| server_config.max_protocol_version = protocol_version; |
| return *this; |
| } |
| |
| template <typename Sink> |
| friend void AbslStringify(Sink& sink, const TestConfig& config) { |
| if (config.pct_loss >= 0) { |
| absl::Format(&sink, "loss: %u ", config.pct_loss); |
| } |
| if (config.client_interface_count > 1) { |
| absl::Format(&sink, "client_interface_count: %u ", |
| config.client_interface_count); |
| } |
| if (config.server_interface_count > 1) { |
| absl::Format(&sink, "server_interface_count: %u ", |
| config.server_interface_count); |
| } |
| sink.Append("[ client: "); |
| AbslStringify(sink, config.client_config); |
| sink.Append("[ server: "); |
| AbslStringify(sink, config.server_config); |
| sink.Append("]"); |
| } |
| |
| static constexpr EndpointConfig kEndpointVariants[] = { |
| { |
| .dtls_in_stun = false, |
| .pqc = false, |
| }, |
| { |
| .dtls_in_stun = true, |
| .pqc = false, |
| }, |
| { |
| .dtls_in_stun = false, |
| .pqc = true, |
| }, |
| { |
| .dtls_in_stun = true, |
| .pqc = true, |
| }, |
| }; |
| |
| static std::vector<TestConfig> AllVariants() { |
| std::vector<TestConfig> out; |
| for (auto cc : kEndpointVariants) { |
| for (auto sc : kEndpointVariants) { |
| for (auto use_ice_lite : {false, true}) { |
| for (auto cic : {true, false}) { |
| for (auto p : {SSL_PROTOCOL_DTLS_12, SSL_PROTOCOL_DTLS_13}) { |
| auto config = TestConfig{.ice_lite = use_ice_lite, |
| .client_ssl_client = cic, |
| .protocol_version = p, |
| .client_config = cc, |
| .server_config = sc} |
| .fix(); |
| if (p == SSL_PROTOCOL_DTLS_12 && (cc.pqc || sc.pqc)) { |
| continue; |
| } |
| out.push_back(config); |
| } |
| } |
| } |
| } |
| } |
| return out; |
| } |
| }; |
| |
| class Base { |
| public: |
| explicit Base(const TestConfig& config) |
| : config_(config), |
| ss_(std::make_unique<VirtualSocketServer>()), |
| socket_factory_(std::make_unique<BasicPacketSocketFactory>(ss_.get())), |
| client_(/* client= */ true, config.client_config), |
| server_(/* client= */ false, config.server_config), |
| client_ice_parameters_("c_ufrag", |
| "c_icepwd_something_something", |
| false), |
| server_ice_parameters_("s_ufrag", |
| "s_icepwd_something_something", |
| false) {} |
| virtual ~Base() { TearDown(); } |
| |
| struct Endpoint { |
| explicit Endpoint(bool client_, const EndpointConfig& config_) |
| : client(client_), |
| config(config_), |
| env(CreateEnvironment(CreateTestFieldTrialsPtr( |
| config.dtls_in_stun ? "WebRTC-IceHandshakeDtls/Enabled/" : ""))) { |
| } |
| |
| bool client; |
| EmulatedNetworkManagerInterface* emulated_network_manager = nullptr; |
| std::unique_ptr<NetworkManager> network_manager; |
| std::unique_ptr<BasicPacketSocketFactory> packet_socket_factory; |
| std::unique_ptr<PortAllocator> allocator; |
| scoped_refptr<IceTransportInterface> ice_transport; |
| std::unique_ptr<DtlsTransportInternalImpl> dtls; |
| |
| // Convenience getter for the internal transport. |
| IceTransportInternal* ice() { return ice_transport->internal(); } |
| |
| // SetRemoteFingerprintFromCert does not actually set the fingerprint, |
| // but only store it for setting later. |
| bool store_but_dont_set_remote_fingerprint = false; |
| std::unique_ptr<SSLFingerprint> remote_fingerprint; |
| |
| scoped_refptr<RTCCertificate> local_certificate; |
| scoped_refptr<RTCCertificate> remote_certificate; |
| |
| EndpointConfig config; |
| Environment env; |
| |
| void Restart(Base& test) { |
| dtls.reset(); |
| ice_transport = nullptr; |
| allocator.reset(); |
| packet_socket_factory.reset(); |
| |
| packet_socket_factory = std::make_unique<BasicPacketSocketFactory>( |
| emulated_network_manager->socket_factory()); |
| allocator = std::make_unique<BasicPortAllocator>( |
| env, network_manager.get(), packet_socket_factory.get()); |
| test.SetupIceAndDtls(*this); |
| allocator->Initialize(); |
| } |
| }; |
| |
| // Run benchmark for this TestConfig& `iter` iterations, |
| // return statistics. |
| SamplesStatsCounter RunBenchmark(int iter) { |
| ConfigureEmulatedNetwork(config_.pct_loss, config_.client_interface_count, |
| config_.server_interface_count); |
| Prepare(); |
| |
| SamplesStatsCounter stats(iter); |
| for (int i = 0; i < iter; i++) { |
| int client_sent = 0; |
| std::atomic<int> client_recv = 0; |
| int server_sent = 0; |
| std::atomic<int> server_recv = 0; |
| void* id = this; |
| |
| client_thread()->BlockingCall([&]() { |
| return client_.dtls->RegisterReceivedPacketCallback( |
| id, [&](auto, auto) { client_recv++; }); |
| }); |
| server_thread()->BlockingCall([&]() { |
| return server_.dtls->RegisterReceivedPacketCallback( |
| id, [&](auto, auto) { server_recv++; }); |
| }); |
| |
| client_thread()->PostTask( |
| [&]() { client_.ice()->MaybeStartGathering(); }); |
| server_thread()->PostTask( |
| [&]() { server_.ice()->MaybeStartGathering(); }); |
| |
| auto start = CurrentTime(); |
| |
| while (client_recv == 0 || server_recv == 0) { |
| int delay = 50; |
| AdvanceTime(TimeDelta::Millis(delay)); |
| |
| // Send data |
| { |
| int flags = 0; |
| AsyncSocketPacketOptions options; |
| std::string a_string(50, 'a'); |
| |
| if (client_.dtls->writable()) { |
| client_thread()->BlockingCall([&]() { |
| if (client_.dtls->SendPacket(a_string.c_str(), a_string.length(), |
| options, flags) > 0) { |
| client_sent++; |
| } |
| }); |
| } |
| if (server_.dtls->writable()) { |
| server_thread()->BlockingCall([&]() { |
| if (server_.dtls->SendPacket(a_string.c_str(), a_string.length(), |
| options, flags) > 0) { |
| server_sent++; |
| } |
| }); |
| } |
| } |
| } |
| auto end = CurrentTime(); |
| stats.AddSample(SamplesStatsCounter::StatsSample{ |
| .value = static_cast<double>((end - start).ms()), |
| .time = end, |
| }); |
| client_thread()->BlockingCall( |
| [&]() { return client_.dtls->DeregisterReceivedPacketCallback(id); }); |
| server_thread()->BlockingCall( |
| [&]() { return server_.dtls->DeregisterReceivedPacketCallback(id); }); |
| if (i + 1 < iter) { |
| client_thread()->BlockingCall([&]() { client_.Restart(*this); }); |
| server_thread()->BlockingCall([&]() { server_.Restart(*this); }); |
| } |
| } |
| return stats; |
| } |
| |
| static bool IsBoringSsl() { return SSLStreamAdapter::IsBoringSsl(); } |
| |
| protected: |
| void SetUp() {} |
| |
| void TearDown() { |
| if (client_thread() != nullptr) { |
| client_thread()->BlockingCall([&]() { |
| client_.dtls.reset(); |
| client_.ice_transport = nullptr; |
| client_.allocator.reset(); |
| }); |
| } |
| |
| if (server_thread() != nullptr) { |
| server_thread()->BlockingCall([&]() { |
| server_.dtls.reset(); |
| server_.ice_transport = nullptr; |
| server_.allocator.reset(); |
| }); |
| } |
| } |
| |
| void ConfigureEmulatedNetwork(int pct_loss = 25, |
| int client_interface_count = 1, |
| int server_interface_count = 1) { |
| network_emulation_manager_ = |
| CreateNetworkEmulationManager({.time_mode = TimeMode::kSimulated}); |
| |
| BuiltInNetworkBehaviorConfig networkBehavior; |
| networkBehavior.link_capacity = DataRate::KilobitsPerSec(220); |
| networkBehavior |
| .queue_delay_ms = /* this is one way delay, i.e. divide the rtt by 2 */ |
| DtlsTransportInternalImpl::kDefaultHandshakeEstimateRttMs / 2; |
| networkBehavior.queue_length_packets = 30; |
| networkBehavior.loss_percent = pct_loss; |
| |
| auto pair = network_emulation_manager_->CreateEndpointPairWithTwoWayRoutes( |
| networkBehavior, client_interface_count, server_interface_count); |
| client_.emulated_network_manager = pair.first; |
| server_.emulated_network_manager = pair.second; |
| } |
| |
| void Prepare() { |
| auto client_certificate = |
| RTCCertificate::Create(SSLIdentity::Create("test", KT_DEFAULT)); |
| auto server_certificate = |
| RTCCertificate::Create(SSLIdentity::Create("test", KT_DEFAULT)); |
| |
| if (network_emulation_manager_ == nullptr) { |
| global_time_controller_ = std::make_unique<GlobalSimulatedTimeController>( |
| Timestamp::Seconds(10000), ss_.get()); |
| time_controller_ = global_time_controller_.get(); |
| } else { |
| time_controller_ = network_emulation_manager_->time_controller(); |
| } |
| |
| client_thread()->BlockingCall([&]() { |
| SetupEndpoint(client_, client_certificate, server_certificate); |
| }); |
| |
| server_thread()->BlockingCall([&]() { |
| SetupEndpoint(server_, client_certificate, server_certificate); |
| }); |
| |
| // Setup the network. |
| if (network_emulation_manager_ == nullptr) { |
| network_manager_->AddInterface(SocketAddress("192.168.1.1", 0)); |
| } |
| |
| client_thread()->BlockingCall([&]() { client_.allocator->Initialize(); }); |
| server_thread()->BlockingCall([&]() { server_.allocator->Initialize(); }); |
| } |
| |
| Timestamp CurrentTime() { |
| return time_controller_->GetClock()->CurrentTime(); |
| } |
| |
| void AdvanceTime(TimeDelta delta) { time_controller_->AdvanceTime(delta); } |
| |
| WaitUntilSettings wait_until_settings(int timeout_ms = kDefaultTimeout) { |
| return { |
| .timeout = TimeDelta::Millis(timeout_ms), |
| .clock = time_controller_, |
| }; |
| } |
| |
| Thread* thread(Endpoint& ep) { |
| if (ep.emulated_network_manager == nullptr) { |
| return time_controller_->GetMainThread(); |
| } else { |
| return ep.emulated_network_manager->network_thread(); |
| } |
| } |
| |
| Thread* client_thread() { return thread(client_); } |
| Thread* server_thread() { return thread(server_); } |
| |
| Endpoint& dtls_client() { |
| return client_.config.ssl_role == SSL_CLIENT ? client_ : server_; |
| } |
| Endpoint& dtls_server() { |
| return client_.config.ssl_role == SSL_SERVER ? client_ : server_; |
| } |
| |
| void SetRemoteFingerprintFromCert(Endpoint& ep, |
| const scoped_refptr<RTCCertificate>& cert) { |
| ep.remote_fingerprint = SSLFingerprint::CreateFromCertificate(*cert); |
| if (ep.store_but_dont_set_remote_fingerprint) { |
| return; |
| } |
| SetRemoteFingerprint(ep); |
| } |
| |
| void SetRemoteFingerprint(Endpoint& ep) { |
| RTC_CHECK(ep.remote_fingerprint); |
| RTC_LOG(LS_INFO) << ((&ep == &client_) ? "client" : "server") |
| << "::SetRemoteFingerprint"; |
| ep.dtls->SetRemoteParameters( |
| ep.remote_fingerprint->algorithm, |
| reinterpret_cast<const uint8_t*>(ep.remote_fingerprint->digest.data()), |
| ep.remote_fingerprint->digest.size(), std::nullopt); |
| } |
| |
| void SetLocalCertificate(Endpoint& ep, |
| const scoped_refptr<RTCCertificate> certificate) { |
| RTC_CHECK(certificate); |
| RTC_LOG(LS_INFO) << ((&ep == &client_) ? "client" : "server") |
| << "::SetLocalCertificate: "; |
| ep.dtls->SetLocalCertificate(certificate); |
| } |
| |
| TestConfig config_; |
| std::unique_ptr<VirtualSocketServer> ss_; |
| std::unique_ptr<BasicPacketSocketFactory> socket_factory_; |
| std::unique_ptr<NetworkEmulationManager> network_emulation_manager_; |
| std::unique_ptr<GlobalSimulatedTimeController> global_time_controller_; |
| TimeController* time_controller_ = nullptr; |
| std::unique_ptr<FakeNetworkManager> network_manager_; |
| |
| Endpoint client_; |
| Endpoint server_; |
| |
| IceParameters client_ice_parameters_; |
| IceParameters server_ice_parameters_; |
| // Used for simlating an ICE Lite agent. |
| FakeIceLiteAgentIceControllerFactory |
| fake_ice_lite_agent_ice_controller_factory_; |
| |
| private: |
| void CandidateC2S(IceTransportInternal*, const Candidate& c) { |
| server_thread()->PostTask( |
| [this, c = c]() { server_.ice()->AddRemoteCandidate(c); }); |
| } |
| void CandidateS2C(IceTransportInternal*, const Candidate& c) { |
| client_thread()->PostTask( |
| [this, c = c]() { client_.ice()->AddRemoteCandidate(c); }); |
| } |
| |
| void SetupEndpoint(Endpoint& ep, |
| const scoped_refptr<RTCCertificate> client_certificate, |
| const scoped_refptr<RTCCertificate> server_certificate) { |
| thread(ep)->BlockingCall([&]() { |
| if (!network_manager_) { |
| network_manager_ = |
| std::make_unique<FakeNetworkManager>(Thread::Current()); |
| } |
| if (network_emulation_manager_ == nullptr) { |
| ep.allocator = std::make_unique<BasicPortAllocator>( |
| ep.env, network_manager_.get(), socket_factory_.get()); |
| } else { |
| ep.network_manager = |
| ep.emulated_network_manager->ReleaseNetworkManager(); |
| ep.packet_socket_factory = std::make_unique<BasicPacketSocketFactory>( |
| ep.emulated_network_manager->socket_factory()); |
| ep.allocator = std::make_unique<BasicPortAllocator>( |
| ep.env, ep.network_manager.get(), ep.packet_socket_factory.get()); |
| } |
| ep.local_certificate = |
| ep.client ? client_certificate : server_certificate; |
| ep.remote_certificate = |
| ep.client ? server_certificate : client_certificate; |
| SetupIceAndDtls(ep); |
| }); |
| } |
| |
| void SetupIceAndDtls(Endpoint& ep) { |
| // Should we be using the FakeIceLiteAgent? |
| bool ice_lite_agent = |
| ep.config.ice_lite && ep.config.ice_role == ICEROLE_CONTROLLED; |
| ep.allocator->set_flags(ep.allocator->flags() | PORTALLOCATOR_DISABLE_TCP); |
| IceTransportInit init(ep.env); |
| init.set_port_allocator(ep.allocator.get()); |
| if (ice_lite_agent) { |
| init.set_active_ice_controller_factory( |
| &fake_ice_lite_agent_ice_controller_factory_); |
| } |
| auto channel = P2PTransportChannel::Create( |
| ep.client ? "client_transport" : "server_transport", |
| /* component= */ 0, std::move(init)); |
| ep.ice_transport = make_ref_counted<FakeIceTransport>(std::move(channel)); |
| |
| // Enable(or disable) the dtls_in_stun parameter before |
| // DTLS is negotiated. |
| IceConfig config; |
| config.continual_gathering_policy = GATHER_CONTINUALLY; |
| config.dtls_handshake_in_stun = ep.config.dtls_in_stun; |
| ep.ice()->SetIceConfig(config); |
| |
| // Is peer using ice-lite. |
| if (ep.config.ice_lite && ep.config.ice_role == ICEROLE_CONTROLLING) { |
| ep.ice()->SetRemoteIceMode(ICEMODE_LITE); |
| } |
| |
| CryptoOptions crypto_options; |
| if (ep.config.pqc) { |
| FieldTrials field_trials("WebRTC-EnableDtlsPqc/Enabled/"); |
| crypto_options.ephemeral_key_exchange_cipher_groups.Update(&field_trials); |
| } |
| ep.dtls = std::make_unique<DtlsTransportInternalImpl>( |
| ep.env, ep.ice_transport, crypto_options, |
| ep.config.max_protocol_version); |
| |
| if (ice_lite_agent) { |
| ep.dtls->SetFakeIceLite(); |
| } |
| |
| // Setup ICE. |
| ep.ice()->SetIceParameters(ep.client ? client_ice_parameters_ |
| : server_ice_parameters_); |
| ep.ice()->SetRemoteIceParameters(ep.client ? server_ice_parameters_ |
| : client_ice_parameters_); |
| ep.ice()->SetIceRole(ep.config.ice_role); |
| if (ep.client) { |
| ep.ice()->SubscribeCandidateGathered( |
| this, |
| [this](IceTransportInternal* transport, const Candidate& candidate) { |
| CandidateC2S(transport, candidate); |
| }); |
| } else { |
| ep.ice()->SubscribeCandidateGathered( |
| this, |
| [this](IceTransportInternal* transport, const Candidate& candidate) { |
| CandidateS2C(transport, candidate); |
| }); |
| } |
| |
| // Setup DTLS. |
| ep.dtls->SetDtlsRole(ep.config.ssl_role); |
| SetLocalCertificate(ep, ep.local_certificate); |
| SetRemoteFingerprintFromCert(ep, ep.remote_certificate); |
| } |
| }; |
| |
| } // namespace dtls_ice_integration_fixture |
| } // namespace webrtc |
| |
| #endif // P2P_DTLS_DTLS_ICE_INTEGRATION_FIXTURE_H_ |