From: uazo Date: Wed, 15 Oct 2025 13:58:11 +0000 Subject: Enable ClientHello Segmentation Allows the ClientHello packet to be sent by segmenting it into multiple packets. Can be useful for circumventing censorship via DIP introspection. The patch automatically disables the mode in case of outdated or incompatible servers, if the TLS handshake fails or times out. The mode is accessible via the flag clienthello-fragmentation and is currently disabled. Based on the work of Shukan (https://habr.com/ru/articles/954284) License: GPL-2.0-or-later - https://spdx.org/licenses/GPL-2.0-or-later.html --- .../Enable-clienthello-fragmentation.inc | 8 +++++ .../Enable-clienthello-fragmentation.inc | 3 ++ .../Enable-clienthello-fragmentation.inc | 1 + net/http/http_proxy_connect_job.cc | 3 +- net/http/http_proxy_connect_job.h | 2 +- net/socket/connect_job.cc | 9 ++++-- net/socket/connect_job.h | 2 +- net/socket/ssl_client_socket.h | 2 ++ net/socket/ssl_client_socket_impl.cc | 14 ++++++++- net/socket/ssl_client_socket_impl.h | 3 ++ net/socket/ssl_connect_job.cc | 29 +++++++++++++++++-- net/socket/ssl_connect_job.h | 3 ++ net/ssl/ssl_config.h | 2 ++ third_party/boringssl/src/ssl/s3_both.cc | 5 +++- 14 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 cromite_flags/chrome/browser/about_flags_cc/Enable-clienthello-fragmentation.inc create mode 100644 cromite_flags/net/base/features_cc/Enable-clienthello-fragmentation.inc create mode 100644 cromite_flags/net/base/features_h/Enable-clienthello-fragmentation.inc diff --git a/cromite_flags/chrome/browser/about_flags_cc/Enable-clienthello-fragmentation.inc b/cromite_flags/chrome/browser/about_flags_cc/Enable-clienthello-fragmentation.inc new file mode 100644 --- /dev/null +++ b/cromite_flags/chrome/browser/about_flags_cc/Enable-clienthello-fragmentation.inc @@ -0,0 +1,8 @@ +#ifdef FLAG_SECTION + + {"clienthello-fragmentation", + "Enable ClientHello Segmentation", + "Enable packet segmentation when sending ClientHello to bypass censorship.", kOsAll, + FEATURE_VALUE_TYPE(net::features::kClientHelloFragmentation)}, + +#endif // ifdef FLAG_SECTION diff --git a/cromite_flags/net/base/features_cc/Enable-clienthello-fragmentation.inc b/cromite_flags/net/base/features_cc/Enable-clienthello-fragmentation.inc new file mode 100644 --- /dev/null +++ b/cromite_flags/net/base/features_cc/Enable-clienthello-fragmentation.inc @@ -0,0 +1,3 @@ +CROMITE_FEATURE(kClientHelloFragmentation, + "ClientHelloFragmentation", + base::FEATURE_ENABLED_BY_DEFAULT); diff --git a/cromite_flags/net/base/features_h/Enable-clienthello-fragmentation.inc b/cromite_flags/net/base/features_h/Enable-clienthello-fragmentation.inc new file mode 100644 --- /dev/null +++ b/cromite_flags/net/base/features_h/Enable-clienthello-fragmentation.inc @@ -0,0 +1 @@ +NET_EXPORT BASE_DECLARE_FEATURE(kClientHelloFragmentation); diff --git a/net/http/http_proxy_connect_job.cc b/net/http/http_proxy_connect_job.cc --- a/net/http/http_proxy_connect_job.cc +++ b/net/http/http_proxy_connect_job.cc @@ -884,7 +884,7 @@ void HttpProxyConnectJob::ChangePriorityInternal(RequestPriority priority) { } } -void HttpProxyConnectJob::OnTimedOutInternal() { +bool HttpProxyConnectJob::OnTimedOutInternal() { // Only record latency for connections to the first proxy in a chain. if (next_state_ == STATE_TRANSPORT_CONNECT_COMPLETE && params_->proxy_chain_index() == 0) { @@ -893,6 +893,7 @@ void HttpProxyConnectJob::OnTimedOutInternal() { HttpConnectResult::kTimedOut, base::TimeTicks::Now() - connect_start_time_); } + return true; } void HttpProxyConnectJob::OnAuthChallenge() { diff --git a/net/http/http_proxy_connect_job.h b/net/http/http_proxy_connect_job.h --- a/net/http/http_proxy_connect_job.h +++ b/net/http/http_proxy_connect_job.h @@ -280,7 +280,7 @@ class NET_EXPORT_PRIVATE HttpProxyConnectJob : public ConnectJob, // ConnectJob implementation. void ChangePriorityInternal(RequestPriority priority) override; - void OnTimedOutInternal() override; + bool OnTimedOutInternal() override; void OnAuthChallenge(); diff --git a/net/socket/connect_job.cc b/net/socket/connect_job.cc --- a/net/socket/connect_job.cc +++ b/net/socket/connect_job.cc @@ -230,16 +230,19 @@ void ConnectJob::StopTimerAndLogConnectCompletion(int net_error) { } void ConnectJob::OnTimeout() { + if (!OnTimedOutInternal()) { + Connect(); + return; + } + // Make sure the socket is NULL before calling into |delegate|. SetSocket(nullptr, std::nullopt /* dns_aliases */); - OnTimedOutInternal(); - net_log_.AddEvent(NetLogEventType::CONNECT_JOB_TIMED_OUT); NotifyDelegateOfCompletion(ERR_TIMED_OUT); } -void ConnectJob::OnTimedOutInternal() {} +bool ConnectJob::OnTimedOutInternal() { return true; } } // namespace net diff --git a/net/socket/connect_job.h b/net/socket/connect_job.h --- a/net/socket/connect_job.h +++ b/net/socket/connect_job.h @@ -344,7 +344,7 @@ class NET_EXPORT_PRIVATE ConnectJob { void OnTimeout(); // Invoked to notify subclasses that the has request timed out. - virtual void OnTimedOutInternal(); + virtual bool OnTimedOutInternal(); const base::TimeDelta timeout_duration_; RequestPriority priority_; diff --git a/net/socket/ssl_client_socket.h b/net/socket/ssl_client_socket.h --- a/net/socket/ssl_client_socket.h +++ b/net/socket/ssl_client_socket.h @@ -111,6 +111,8 @@ class NET_EXPORT SSLClientSocket : public SSLSocket { // advertised in DNS. virtual std::vector> GetServerTrustAnchorIDs() = 0; + virtual bool IsTlsFragmentationFailed() const = 0; + // Log SSL key material to |logger|. Must be called before any // SSLClientSockets are created. // diff --git a/net/socket/ssl_client_socket_impl.cc b/net/socket/ssl_client_socket_impl.cc --- a/net/socket/ssl_client_socket_impl.cc +++ b/net/socket/ssl_client_socket_impl.cc @@ -336,6 +336,10 @@ void SSLClientSocketImpl::SetSSLKeyLogger( SSLContext::GetInstance()->SetSSLKeyLogger(std::move(logger)); } +bool SSLClientSocketImpl::IsTlsFragmentationFailed() const { + return tls_fragmentation_error_; +} + std::vector SSLClientSocketImpl::GetECHRetryConfigs() { const uint8_t* retry_configs; size_t retry_configs_len; @@ -403,6 +407,10 @@ int SSLClientSocketImpl::Connect(CompletionOnceCallback callback) { return rv; } + if (ssl_config_.enable_client_hello_fragmentation) { + SSL_set_max_send_fragment(ssl_.get(), 500); + } + // Set SSL to client mode. Handshake happens in the loop below. SSL_set_connect_state(ssl_.get()); @@ -934,6 +942,7 @@ int SSLClientSocketImpl::DoHandshake() { int rv = SSL_do_handshake(ssl_.get()); int net_error = OK; + tls_fragmentation_error_ = false; if (rv <= 0) { int ssl_error = SSL_get_error(ssl_.get(), rv); if (ssl_error == SSL_ERROR_WANT_X509_LOOKUP && !send_client_cert_) { @@ -959,8 +968,11 @@ int SSLClientSocketImpl::DoHandshake() { return ERR_IO_PENDING; } + tls_fragmentation_error_ = ssl_config_.enable_client_hello_fragmentation; LOG(ERROR) << "handshake failed; returned " << rv << ", SSL error code " - << ssl_error << ", net_error " << net_error; + << ssl_error << ", net_error " << net_error + << ", enable_client_hello_fragmentation " + << ssl_config_.enable_client_hello_fragmentation; NetLogOpenSSLError(net_log_, NetLogEventType::SSL_HANDSHAKE_ERROR, net_error, ssl_error, error_info); } diff --git a/net/socket/ssl_client_socket_impl.h b/net/socket/ssl_client_socket_impl.h --- a/net/socket/ssl_client_socket_impl.h +++ b/net/socket/ssl_client_socket_impl.h @@ -77,6 +77,7 @@ class NET_EXPORT_PRIVATE SSLClientSocketImpl // SSLClientSocket implementation. std::vector GetECHRetryConfigs() override; std::vector> GetServerTrustAnchorIDs() override; + bool IsTlsFragmentationFailed() const override; // SSLSocket implementation. int ExportKeyingMaterial(std::string_view label, @@ -253,6 +254,8 @@ class NET_EXPORT_PRIVATE SSLClientSocketImpl // network. bool was_ever_used_ = false; + bool tls_fragmentation_error_ = false; + const raw_ptr context_; // Stores the value of SSLClientSessionCache's generation number at the time diff --git a/net/socket/ssl_connect_job.cc b/net/socket/ssl_connect_job.cc --- a/net/socket/ssl_connect_job.cc +++ b/net/socket/ssl_connect_job.cc @@ -43,6 +43,7 @@ namespace { // Timeout for the SSL handshake portion of the connect. constexpr base::TimeDelta kSSLHandshakeTimeout(base::Seconds(30)); +constexpr base::TimeDelta kSSLHandshakeTimeoutWithFragmentation(base::Seconds(2)); } // namespace @@ -101,7 +102,10 @@ SSLConnectJob::SSLConnectJob( NetLogEventType::SSL_CONNECT_JOB_CONNECT), params_(std::move(params)), callback_(base::BindRepeating(&SSLConnectJob::OnIOComplete, - base::Unretained(this))) {} + base::Unretained(this))) { + enable_client_hello_fragmentation_ = + base::FeatureList::IsEnabled(net::features::kClientHelloFragmentation); +} SSLConnectJob::~SSLConnectJob() { // In the case the job was canceled, need to delete nested job first to @@ -370,7 +374,10 @@ int SSLConnectJob::DoSSLConnect() { next_state_ = STATE_SSL_CONNECT_COMPLETE; // Set the timeout to just the time allowed for the SSL handshake. - ResetTimer(kSSLHandshakeTimeout); + if (enable_client_hello_fragmentation_) + ResetTimer(kSSLHandshakeTimeoutWithFragmentation); + else + ResetTimer(kSSLHandshakeTimeout); // Get the transport's connect start and DNS times. const LoadTimingInfo::ConnectTiming& socket_connect_timing = @@ -435,6 +442,8 @@ int SSLConnectJob::DoSSLConnect() { } } + ssl_config.enable_client_hello_fragmentation = enable_client_hello_fragmentation_; + net_log().AddEvent(NetLogEventType::SSL_CONNECT_JOB_SSL_CONNECT, [&] { base::DictValue dict; dict.Set("ech_enabled", ssl_client_context()->config().ech_enabled); @@ -492,6 +501,12 @@ int SSLConnectJob::DoSSLConnectComplete(int result) { // retry on a potentially unreliably network connection. // // TODO(crbug.com/40085786): Remove this now redundant retry. + if (enable_client_hello_fragmentation_ && ssl_socket_->IsTlsFragmentationFailed()) { + ResetStateForRestart(); + enable_client_hello_fragmentation_ = false; + next_state_ = GetInitialState(params_->GetConnectionType()); + return OK; + } if (disable_legacy_crypto_with_fallback_ && (result == ERR_CONNECTION_CLOSED || result == ERR_CONNECTION_RESET || result == ERR_SSL_PROTOCOL_ERROR || @@ -597,6 +612,16 @@ SSLConnectJob::State SSLConnectJob::GetInitialState( NOTREACHED(); } +bool SSLConnectJob::OnTimedOutInternal() { + if (enable_client_hello_fragmentation_ && ssl_negotiation_started_) { + ResetStateForRestart(); + enable_client_hello_fragmentation_ = false; + next_state_ = GetInitialState(params_->GetConnectionType()); + return false; + } + return true; +} + int SSLConnectJob::ConnectInternal() { next_state_ = GetInitialState(params_->GetConnectionType()); return DoLoop(OK); diff --git a/net/socket/ssl_connect_job.h b/net/socket/ssl_connect_job.h --- a/net/socket/ssl_connect_job.h +++ b/net/socket/ssl_connect_job.h @@ -169,6 +169,8 @@ class NET_EXPORT_PRIVATE SSLConnectJob : public ConnectJob, int DoSSLConnect(); int DoSSLConnectComplete(int result); + bool OnTimedOutInternal() override; + // Returns the initial state for the state machine based on the // |connection_type|. static State GetInitialState(SSLSocketParams::ConnectionType connection_type); @@ -201,6 +203,7 @@ class NET_EXPORT_PRIVATE SSLConnectJob : public ConnectJob, // attempt. On error, the connection will be retried with legacy crypto // enabled. bool disable_legacy_crypto_with_fallback_ = true; + bool enable_client_hello_fragmentation_ = false; scoped_refptr ssl_cert_request_info_; diff --git a/net/ssl/ssl_config.h b/net/ssl/ssl_config.h --- a/net/ssl/ssl_config.h +++ b/net/ssl/ssl_config.h @@ -137,6 +137,8 @@ struct NET_EXPORT SSLConfig { // retry the connection. std::vector ech_config_list; + bool enable_client_hello_fragmentation = false; + // An additional boolean to partition the session cache by. // // TODO(https://crbug.com/775438, https://crbug.com/951205): This should diff --git a/third_party/boringssl/src/ssl/s3_both.cc b/third_party/boringssl/src/ssl/s3_both.cc --- a/third_party/boringssl/src/ssl/s3_both.cc +++ b/third_party/boringssl/src/ssl/s3_both.cc @@ -98,8 +98,11 @@ bool tls_add_message(SSL *ssl, Array msg) { // implementations. // // TODO(crbug.com/374991962): See if we can do this uniformly. + bool is_max_send_fragment_changed = + ssl->max_send_fragment != SSL3_RT_MAX_PLAIN_LENGTH; Span rest = msg; - if (!SSL_is_quic(ssl) && ssl->s3->aead_write_ctx->is_null_cipher()) { + if (is_max_send_fragment_changed || + (!SSL_is_quic(ssl) && ssl->s3->aead_write_ctx->is_null_cipher())) { while (!rest.empty()) { Span chunk = rest.first(std::min(size_t{ssl->max_send_fragment}, rest.size())); --