This is an automated email from the ASF dual-hosted git repository.

eze pushed a commit to branch 9.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/9.2.x by this push:
     new 0d4d981552 Support CLIENT_HELLO split across multiple packets (#12319)
0d4d981552 is described below

commit 0d4d98155281a91196734158b1f94d86b874a9d4
Author: Brian Neradt <[email protected]>
AuthorDate: Mon Jun 30 17:12:31 2025 -0500

    Support CLIENT_HELLO split across multiple packets (#12319)
    
    Our TLS CLIENT_HELLO processing logic assumed all CLIENT_HELLO bytes
    came in a single TCP packet. However, with more recent cryptographic
    ciphers, the CLIENT_HELLO is often greater than the standard 1,500 byte
    MTU, so the CLIENT_HELLO is being delivered in multiple packets. This
    updates our logic to properly buffer and parse data across multiple
    socket reads.
    
    Fixes: #11758
---
 iocore/net/P_SSLNetVConnection.h                   |  71 +++++++-
 iocore/net/SSLNetVConnection.cc                    | 177 +++++++++++---------
 tests/gold_tests/tls/receive_split_client_hello.py | 129 +++++++++++++++
 tests/gold_tests/tls/split_client_hello.py         | 181 +++++++++++++++++++++
 tests/gold_tests/tls/tls_tunnel.test.py            |  64 +++++++-
 5 files changed, 533 insertions(+), 89 deletions(-)

diff --git a/iocore/net/P_SSLNetVConnection.h b/iocore/net/P_SSLNetVConnection.h
index 67ae7ea716..288303e4f4 100644
--- a/iocore/net/P_SSLNetVConnection.h
+++ b/iocore/net/P_SSLNetVConnection.h
@@ -176,6 +176,26 @@ public:
 
   int64_t read_raw_data();
 
+  /** Initialize handshake buffers in which we store TLS handshake data.
+   *
+   * Typically, we would configure the SSL object to use the socket directly,
+   * and call SSL_read on the socket. In this way, the OpenSSL machine would
+   * read and parse the stream for us, handshake and all. We cannot, however,
+   * blindly let OpenSSL read off the socket since we may need to replay the
+   * CLIENT_HELLO raw bytes to the origin if we wind up blind tunneling the
+   * connection.  Therefore, for the initial CLIENT_HELLO, we:
+   *
+   * 1. Manually read bytes off the socket via read_raw_data().
+   * 2. Store the bytes in @a handShakeBuffer.
+   * 3. Configure our SSL object to read from a memory buffer populated from @a
+   *    handshakeReader.
+   *
+   * Once the CLIENT_HELLO is parsed, we either configure the SSL object to 
read
+   * from the socket as normal, or we replay the bytes to the origin via @a
+   * handshakeHolder if we decide to blind tunnel the connection. In the latter
+   * tunnel case, any subsequent bytes are blindly tunneled between the origin
+   * and the client.
+   */
   void
   initialize_handshake_buffers()
   {
@@ -197,10 +217,14 @@ public:
     if (this->handShakeBuffer) {
       free_MIOBuffer(this->handShakeBuffer);
     }
-    this->handShakeReader    = nullptr;
-    this->handShakeHolder    = nullptr;
-    this->handShakeBuffer    = nullptr;
-    this->handShakeBioStored = 0;
+    if (this->coalescedHandShakeBioBuffer != nullptr) {
+      ats_free(this->coalescedHandShakeBioBuffer);
+    }
+    this->handShakeReader             = nullptr;
+    this->handShakeHolder             = nullptr;
+    this->handShakeBuffer             = nullptr;
+    this->handShakeBioStored          = 0;
+    this->coalescedHandShakeBioBuffer = nullptr;
   }
 
   // Returns true if all the hooks reenabled
@@ -457,13 +481,44 @@ private:
   NetProcessor *_getNetProcessor() override;
   void *_prepareForMigration() override;
 
+  /** Return the unconsumed bytes in @a handShakeReader in a contiguous memory 
buffer.
+   *
+   * If @a handShakeReader is a single IOBufferBlock, this returns the pointer
+   * to the data in that block. Otherwise, memory is allocated in @a
+   * handshakeReaderCoalesced and the bytes are copied into it. Regardless, any
+   * previously allocated memory in @a coalescedHandShakeBioBuffer is freed 
when
+   * this function is called.
+   *
+   * @param[in] total_chain_size The total size of the bytes in @a
+   * handShakeReader across all IOBufferBlocks.
+   *
+   * @return A pointer to all unconsumed bytes in @a handShakeReader in a 
single
+   * contiguous memory buffer.
+   */
+  char *_getCoalescedHandShakeBuffer(int64_t total_chain_size);
+
   enum SSLHandshakeStatus sslHandshakeStatus = SSL_HANDSHAKE_ONGOING;
   bool sslClientRenegotiationAbort           = false;
   bool first_ssl_connect                     = true;
-  MIOBuffer *handShakeBuffer                 = nullptr;
-  IOBufferReader *handShakeHolder            = nullptr;
-  IOBufferReader *handShakeReader            = nullptr;
-  int handShakeBioStored                     = 0;
+
+  /** The buffer storing the initial CLIENT_HELLO bytes. */
+  MIOBuffer *handShakeBuffer = nullptr;
+
+  /** Used to incrementally shuffle bytes read off the socket to the SSL 
object. */
+  IOBufferReader *handShakeHolder = nullptr;
+
+  /** If blind tunneling, this supplies the initial raw bytes of the 
CLIENT_HELLO. */
+  IOBufferReader *handShakeReader = nullptr;
+
+  /** A buffer for the Coalesced @a handShakeReader bytes if @a handShakeReader
+   * spans multiple IOBufferBlocks. */
+  char *coalescedHandShakeBioBuffer = nullptr;
+
+  /** The number of bytes last send to the SSL's BIO. */
+  int handShakeBioStored = 0;
+
+  /** Whether we have already checked for Proxy Protocol in the initial 
packet. */
+  bool haveCheckedProxyProtocol = false;
 
   bool transparentPassThrough = false;
 
diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc
index 9124780630..efd41c0a17 100644
--- a/iocore/net/SSLNetVConnection.cc
+++ b/iocore/net/SSLNetVConnection.cc
@@ -414,47 +414,53 @@ SSLNetVConnection::read_raw_data()
   }
   NET_SUM_DYN_STAT(net_read_bytes_stat, r);
 
-  IpMap *pp_ipmap;
-  pp_ipmap = SSLConfigParams::proxy_protocol_ipmap;
-
-  if (this->get_is_proxy_protocol() && this->get_proxy_protocol_version() == 
ProxyProtocolVersion::UNDEFINED) {
-    Debug("proxyprotocol", "proxy protocol is enabled on this port");
-    if (pp_ipmap->count() > 0) {
-      Debug("proxyprotocol", "proxy protocol has a configured allowlist of 
trusted IPs - checking");
-
-      // At this point, using get_remote_addr() will return the ip of the
-      // proxy source IP, not the Proxy Protocol client ip. Since we are
-      // checking the ip of the actual source of this connection, this is
-      // what we want now.
-      void *payload = nullptr;
-      if (!pp_ipmap->contains(get_remote_addr(), &payload)) {
-        Debug("proxyprotocol", "proxy protocol src IP is NOT in the configured 
allowlist of trusted IPs - "
-                               "closing connection");
-        r = -ENOTCONN; // Need a quick close/exit here to refuse the 
connection!!!!!!!!!
-        goto proxy_protocol_bypass;
+  if (!this->haveCheckedProxyProtocol) {
+    // The PROXY Protocol, by spec, is designed to require only the first TCP 
packet of bytes
+    // because it is under typical MTU. So we only need to perform the 
following inspection on the
+    // first packet.
+    this->haveCheckedProxyProtocol = true;
+    IpMap *pp_ipmap;
+    pp_ipmap = SSLConfigParams::proxy_protocol_ipmap;
+
+    if (this->get_is_proxy_protocol() && this->get_proxy_protocol_version() == 
ProxyProtocolVersion::UNDEFINED) {
+      Debug("proxyprotocol", "proxy protocol is enabled on this port");
+      if (pp_ipmap->count() > 0) {
+        Debug("proxyprotocol", "proxy protocol has a configured allowlist of 
trusted IPs - checking");
+
+        // At this point, using get_remote_addr() will return the ip of the
+        // proxy source IP, not the Proxy Protocol client ip. Since we are
+        // checking the ip of the actual source of this connection, this is
+        // what we want now.
+        void *payload = nullptr;
+        if (!pp_ipmap->contains(get_remote_addr(), &payload)) {
+          Debug("proxyprotocol", "proxy protocol src IP is NOT in the 
configured allowlist of trusted IPs - "
+                                 "closing connection");
+          r = -ENOTCONN; // Need a quick close/exit here to refuse the 
connection!!!!!!!!!
+          goto proxy_protocol_bypass;
+        } else {
+          char new_host[INET6_ADDRSTRLEN];
+          Debug("proxyprotocol", "Source IP [%s] is in the trusted allowlist 
for proxy protocol",
+                ats_ip_ntop(this->get_remote_addr(), new_host, 
sizeof(new_host)));
+        }
       } else {
-        char new_host[INET6_ADDRSTRLEN];
-        Debug("proxyprotocol", "Source IP [%s] is in the trusted allowlist for 
proxy protocol",
-              ats_ip_ntop(this->get_remote_addr(), new_host, 
sizeof(new_host)));
+        Debug("proxyprotocol", "proxy protocol DOES NOT have a configured 
allowlist of trusted IPs but "
+                               "proxy protocol is enabled on this port - 
processing all connections");
       }
-    } else {
-      Debug("proxyprotocol", "proxy protocol DOES NOT have a configured 
allowlist of trusted IPs but "
-                             "proxy protocol is enabled on this port - 
processing all connections");
-    }
 
-    if (this->has_proxy_protocol(buffer, &r)) {
-      Debug("proxyprotocol", "ssl has proxy protocol header");
-      set_remote_addr(get_proxy_protocol_src_addr());
-      if (is_debug_tag_set("proxyprotocol")) {
-        IpEndpoint dst;
-        dst.sa = *(this->get_proxy_protocol_dst_addr());
-        ip_port_text_buffer ipb1;
-        ats_ip_nptop(&dst, ipb1, sizeof(ipb1));
-        Debug("proxyprotocol", "ssl_has_proxy_v1, dest IP received [%s]", 
ipb1);
+      if (this->has_proxy_protocol(buffer, &r)) {
+        Debug("proxyprotocol", "ssl has proxy protocol header");
+        set_remote_addr(get_proxy_protocol_src_addr());
+        if (is_debug_tag_set("proxyprotocol")) {
+          IpEndpoint dst;
+          dst.sa = *(this->get_proxy_protocol_dst_addr());
+          ip_port_text_buffer ipb1;
+          ats_ip_nptop(&dst, ipb1, sizeof(ipb1));
+          Debug("proxyprotocol", "ssl_has_proxy_v1, dest IP received [%s]", 
ipb1);
+        }
+      } else {
+        Debug("proxyprotocol", "proxy protocol was enabled, but required 
header was not present in the "
+                               "transaction - closing connection");
       }
-    } else {
-      Debug("proxyprotocol", "proxy protocol was enabled, but required header 
was not present in the "
-                             "transaction - closing connection");
     }
   } // end of Proxy Protocol processing
 
@@ -463,13 +469,13 @@ proxy_protocol_bypass:
   if (r > 0) {
     this->handShakeBuffer->fill(r);
 
-    char *start              = this->handShakeReader->start();
-    char *end                = this->handShakeReader->end();
-    this->handShakeBioStored = end - start;
+    auto const total_chain_size = this->handShakeReader->read_avail();
+    this->handShakeBioStored    = total_chain_size;
+    char *buffer_for_bio        = 
this->_getCoalescedHandShakeBuffer(total_chain_size);
 
     // Sets up the buffer as a read only bio target
     // Must be reset on each read
-    BIO *rbio = BIO_new_mem_buf(start, this->handShakeBioStored);
+    BIO *rbio = BIO_new_mem_buf(buffer_for_bio, this->handShakeBioStored);
     BIO_set_mem_eof_return(rbio, -1);
     SSL_set0_rbio(this->ssl, rbio);
   } else {
@@ -497,23 +503,25 @@ SSLNetVConnection::update_rbio(bool move_to_socket)
 {
   bool retval = false;
   if (BIO_eof(SSL_get_rbio(this->ssl)) && this->handShakeReader != nullptr) {
+    Debug("ssl", "Consuming handShakeBioStored=%d bytes from the handshake 
reader", this->handShakeBioStored);
     this->handShakeReader->consume(this->handShakeBioStored);
     this->handShakeBioStored = 0;
     // Load up the next block if present
     if (this->handShakeReader->is_read_avail_more_than(0)) {
-      // Setup the next iobuffer block to drain
-      char *start              = this->handShakeReader->start();
-      char *end                = this->handShakeReader->end();
-      this->handShakeBioStored = end - start;
+      auto const total_chain_size = this->handShakeReader->read_avail();
+      this->handShakeBioStored    = total_chain_size;
+      char *buffer_for_bio        = 
this->_getCoalescedHandShakeBuffer(total_chain_size);
+      Debug("ssl", "Adding %d bytes to the ssl rbio", 
this->handShakeBioStored);
 
       // Sets up the buffer as a read only bio target
       // Must be reset on each read
-      BIO *rbio = BIO_new_mem_buf(start, this->handShakeBioStored);
+      BIO *rbio = BIO_new_mem_buf(buffer_for_bio, this->handShakeBioStored);
       BIO_set_mem_eof_return(rbio, -1);
       SSL_set0_rbio(this->ssl, rbio);
       retval = true;
       // Handshake buffer is empty but we have read something, move to the 
socket rbio
     } else if (move_to_socket && 
this->handShakeHolder->is_read_avail_more_than(0)) {
+      Debug("ssl", "No other bytes in the handshake reader, moving to socket 
rbio");
       BIO *rbio = BIO_new_fd(this->get_socket(), BIO_NOCLOSE);
       BIO_set_mem_eof_return(rbio, -1);
       SSL_set0_rbio(this->ssl, rbio);
@@ -602,6 +610,7 @@ SSLNetVConnection::net_read_io(NetHandler *nh, EThread 
*lthread)
           int64_t r              = buf.writer()->write(this->handShakeHolder);
           s->vio.nbytes += r;
           s->vio.ndone += r;
+          Debug("ssl", "Copied %" PRId64 " TLS handshake bytes to read.vio", 
r);
 
           // Clean up the handshake buffers
           this->free_handshake_buffers();
@@ -633,7 +642,12 @@ SSLNetVConnection::net_read_io(NetHandler *nh, EThread 
*lthread)
       }
       // move over to the socket if we haven't already
       if (this->handShakeBuffer != nullptr) {
-        read.triggered = update_rbio(true);
+        bool const in_client_hello = sslHandshakeHookState == 
HANDSHAKE_HOOKS_CLIENT_HELLO;
+        // Only transfer buffers to the socket once the CLIENT_HELLO is
+        // finished. We need to keep our buffers updated until then in case we
+        // enter tunnel mode.
+        Debug("ssl", "Updating our buffers, in CLIENT_HELLO: %s", 
in_client_hello ? "true" : "false");
+        read.triggered = update_rbio(!in_client_hello);
       } else {
         read.triggered = 0;
       }
@@ -1238,39 +1252,38 @@ SSLNetVConnection::sslServerHandShakeEvent(int &err)
   Debug("ssl", "Go on with the handshake state=%d", sslHandshakeHookState);
 
   // All the pre-accept hooks have completed, proceed with the actual accept.
-  if (this->handShakeReader) {
+  bool const in_client_hello = sslHandshakeHookState == 
HANDSHAKE_HOOKS_CLIENT_HELLO;
+  // We only feed CLIENT_HELLO bytes into our temporary buffers. If we are past
+  // the CLIENT_HELLO, then no need to buffer.
+  if (in_client_hello && this->handShakeReader) {
     if (BIO_eof(SSL_get_rbio(this->ssl))) { // No more data in the buffer
-      // Is this the first read?
-      if (!this->handShakeReader->is_read_avail_more_than(0) && 
!this->handShakeHolder->is_read_avail_more_than(0)) {
+                                            // Is this the first read?
 #if TS_USE_TLS_ASYNC
-        if (SSLConfigParams::async_handshake_enabled) {
-          SSL_set_mode(ssl, SSL_MODE_ASYNC);
-        }
+      if (SSLConfigParams::async_handshake_enabled) {
+        SSL_set_mode(ssl, SSL_MODE_ASYNC);
+      }
 #endif
 
-        Debug("ssl", "%p first read\n", this);
-        // Read from socket to fill in the BIO buffer with the
-        // raw handshake data before calling the ssl accept calls.
-        int retval = this->read_raw_data();
-        if (retval < 0) {
-          if (retval == -EAGAIN) {
-            // No data at the moment, hang tight
-            SSLVCDebug(this, "SSL handshake: EAGAIN");
-            return SSL_HANDSHAKE_WANT_READ;
-          } else {
-            // An error, make us go away
-            SSLVCDebug(this, "SSL handshake error: read_retval=%d", retval);
-            return EVENT_ERROR;
-          }
-        } else if (retval == 0) {
-          // EOF, go away, we stopped in the handshake
-          SSLVCDebug(this, "SSL handshake error: EOF");
+      Debug("ssl", "%p reading off the socket into our buffers", this);
+      // Read from socket to fill in the BIO buffer with the
+      // raw handshake data before calling the ssl accept calls.
+      int retval = this->read_raw_data();
+      if (retval < 0) {
+        if (retval == -EAGAIN) {
+          // No data at the moment, hang tight
+          SSLVCDebug(this, "SSL handshake: EAGAIN");
+          return SSL_HANDSHAKE_WANT_READ;
+        } else {
+          // An error, make us go away
+          SSLVCDebug(this, "SSL handshake error: read_retval=%d", retval);
           return EVENT_ERROR;
         }
-      } else {
-        update_rbio(false);
+      } else if (retval == 0) {
+        // EOF, go away, we stopped in the handshake
+        SSLVCDebug(this, "SSL handshake error: EOF");
+        return EVENT_ERROR;
       }
-    } // Still data in the BIO
+    } // Still data in the BIO. Let OpenSSL consume that first before doing 
anything else.
   }
 
   ssl_error_t ssl_error = this->_ssl_accept();
@@ -1991,6 +2004,24 @@ SSLNetVConnection::_getNetProcessor()
   return &sslNetProcessor;
 }
 
+char *
+SSLNetVConnection::_getCoalescedHandShakeBuffer(int64_t total_chain_size)
+{
+  if (this->coalescedHandShakeBioBuffer != nullptr) {
+    ats_free(this->coalescedHandShakeBioBuffer);
+    this->coalescedHandShakeBioBuffer = nullptr;
+  }
+  char *start           = this->handShakeReader->start();
+  char *end             = this->handShakeReader->end();
+  char *coalescedBuffer = start;
+  if ((end - start) < total_chain_size) {
+    this->coalescedHandShakeBioBuffer = static_cast<char 
*>(ats_malloc(total_chain_size));
+    this->handShakeReader->memcpy(this->coalescedHandShakeBioBuffer, 
total_chain_size);
+    coalescedBuffer = this->coalescedHandShakeBioBuffer;
+  }
+  return coalescedBuffer;
+}
+
 ssl_curve_id
 SSLNetVConnection::_get_tls_curve() const
 {
diff --git a/tests/gold_tests/tls/receive_split_client_hello.py 
b/tests/gold_tests/tls/receive_split_client_hello.py
new file mode 100644
index 0000000000..559961ca72
--- /dev/null
+++ b/tests/gold_tests/tls/receive_split_client_hello.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+"""Receive a split CLIENT_HELLO over TCP, send a dummy response, then echo all
+further data."""
+
+#  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.
+
+import argparse
+import socket
+import sys
+
+# See split_client_hello.py for the original CLIENT_HELLO packet.
+EXPECTED_CLIENT_HELLO_LENGTH = 1993
+
+
+def parse_args() -> argparse.Namespace:
+    '''Parse command line arguments.
+    :returns: Parsed command line arguments.
+    '''
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("address", help="Address to listen on")
+    parser.add_argument("port", type=int, help="Port to listen on")
+    return parser.parse_args()
+
+
+def get_listening_socket(address: str, port: int) -> socket.socket:
+    '''Create a socket that listens for incoming connections.
+    :param address: The address to listen on.
+    :param port: The port to listen on.
+    :returns: A listening socket.
+    '''
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.setblocking(True)
+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    sock.bind((address, port))
+    sock.listen(1)
+    return sock
+
+
+def accept_connection(listener: socket.socket) -> socket.socket:
+    '''Accept a connection on the given listener socket.
+
+    This will block until a connection comes in and is accepted.
+
+    :param listener: The socket to accept a connection on.
+    :returns: The accepted socket.
+    '''
+    return listener.accept()[0]
+
+
+def handle_client_connection(listener: socket.socket) -> int:
+    '''Handle a client connection by reading data and echoing it back.
+
+    :param listener: The socket listening for client connections.
+    :returns: 0 on success, non-zero on error.
+    '''
+    while True:
+        with accept_connection(listener) as conn:
+            conn.setblocking(True)
+            print(f'Accepted connection from {conn.getpeername()}')
+            try:
+                data = conn.recv(65536)
+            except socket.error as e:
+                print(f"Socket error on recv: {e}")
+                break
+            if not data:
+                # This is probably the PortOpenv4 test. Try again.
+                print("No bytes read on this connection. Trying again.")
+                continue
+            print(f"Received CLIENT_HELLO packet of {len(data)} bytes, sending 
dummy response.")
+            if len(data) != EXPECTED_CLIENT_HELLO_LENGTH:
+                print(f'Incorrect CLIENT_HELLO length: {len(data)} bytes, 
expected {EXPECTED_CLIENT_HELLO_LENGTH} bytes.')
+                return 1
+            try:
+                conn.sendall(b"dummy SERVER_HELLO")
+            except socket.error as e:
+                print(f"Failed to send dummy SERVER_HELLO response: {e}")
+                break
+            # Echo loop.
+            while True:
+                try:
+                    chunk = conn.recv(65536)
+                except socket.error:
+                    break
+                if not chunk:
+                    break
+                print(f'Received chunk of {len(chunk)} bytes, echoing back:')
+                print(chunk)
+                conn.sendall(chunk)
+            # Done with this client connection.
+            print(f"Client {conn.getpeername()} disconnected. Successfully 
handled connection.")
+            return 0
+    return 1
+
+
+def main() -> int:
+    args = parse_args()
+    with get_listening_socket(args.address, args.port) as listener:
+        print(f"Listening on {args.address}:{args.port}")
+        while True:
+            try:
+                ret = handle_client_connection(listener)
+                if ret != 0:
+                    print(f"Error handling client connection")
+                    return ret
+            except KeyboardInterrupt:
+                print("Server shutting down.")
+                break
+            except Exception as e:
+                print(f"An error occurred: {e}")
+                continue
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/tests/gold_tests/tls/split_client_hello.py 
b/tests/gold_tests/tls/split_client_hello.py
new file mode 100644
index 0000000000..6ebf119d13
--- /dev/null
+++ b/tests/gold_tests/tls/split_client_hello.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+'''Send a pre-recorded TLS CLIENT_HELLO as a configurable number
+of packets to a server.'''
+#  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.
+
+import argparse
+import base64
+import socket
+import sys
+import time
+
+# This is a ClientHello packet captured from a real client using Wireshark.
+# The packet includes key material necessary for the Kyber cipher handshake.
+#
+# NOTE: if this changes, update EXPECTED_CLIENT_HELLO_LENGTH in the
+# corresponding receive_split_client_hello.py script.
+client_hello_b64 = \
+"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"
+
+client_hello = base64.b64decode(client_hello_b64, validate=True)
+
+
+def send_clienthello(dest_host: str, dest_port: int, split_size: int, 
num_data_packets: int) -> int:
+    '''Send a pre-recorded TLS CLIENT_HELLO to a server.
+    :param dest_host: The destination host to which to send the CLIENT_HELLO.
+    :param dest_port: The destination port to which to send the CLIENT_HELLO.
+    :param split_size: The size of each split packet. 0 means no split,
+        send the whole packet at once.
+    :param num_data_packets: The number of data packets to send.
+    :return: 0 on success, non-zero on failure.
+    '''
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+        s.setblocking(True)
+        timeout = 3
+        s.settimeout(timeout)
+        print(f'Connecting to: {dest_host}:{dest_port}')
+        try:
+            s.connect((dest_host, dest_port))
+        except socket.error as e:
+            print(f"Failed to connect to {dest_host}:{dest_port} - {e}")
+            return 1
+        print('Connection successful.')
+        split_size = split_size if split_size > 0 else len(client_hello)
+        client_hello_packets = [client_hello[i:i+split_size] \
+                                for i in range(0, len(client_hello), 
split_size)]
+        print(
+            f'Sending ClientHello of {len(client_hello)} bytes in {split_size} 
'
+            f'byte packets, {len(client_hello_packets)} total packets.')
+        for packet in client_hello_packets:
+            print(f'Sending packet of size {len(packet)} bytes.')
+            s.send(packet)
+            time.sleep(0.05)
+        print('CLIENT_HELLO sent, waiting for response.')
+        try:
+            buff = s.recv(10240)
+        except socket.timeout:
+            print(f"Failed: no response received within {timeout} seconds.")
+            return 1
+        except socket.error as e:
+            print(f"Failed to receive response - {e}")
+            return 1
+        except Exception as e:
+            print(f"An unexpected error occurred while receiving response - 
{e}")
+            return 1
+        print("Response received:")
+        print(buff.decode('utf-8', errors='ignore'))
+
+        print(f'Sending {num_data_packets} dummy data packets.')
+        for i in range(num_data_packets):
+            time.sleep(0.05)
+            print(f'Sending dummy data packet {i} of size {len(buff)} bytes.')
+            send_buf = f'data: {i}\n'.encode('utf-8')
+            try:
+                s.send(send_buf)
+            except socket.error as e:
+                print(f"Failed to send dummy data packet {i} - {e}")
+                return 1
+            # Wait for a response after each data packet
+            try:
+                response_buff = s.recv(10240)
+            except socket.timeout:
+                print(f"Failed: no response received after data packet {i}.")
+                return 1
+            except socket.error as e:
+                print(f"Failed to receive response after data packet {i} - 
{e}")
+                return 1
+            except Exception as e:
+                print(f"An unexpected error occurred while receiving response 
after data packet {i} - {e}")
+                return 1
+            print(f"Response after data packet {i} received:")
+            print(response_buff.decode('utf-8', errors='ignore'))
+            # Verify that the response was an echo of the request.
+            if response_buff != send_buf:
+                print(f"Response after data packet {i} did not match sent 
data: {response_buff}")
+                return 1
+            else:
+                print(f"Response after data packet {i} matched sent data.")
+        print("All packets sent successfully.")
+        return 0
+
+
+def parse_args() -> argparse.Namespace:
+    '''Parse command line arguments.
+    :return: The parsed arguments.
+    '''
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("dest_host", help="Destination host to which to send 
the CLIENT_HELLO.")
+    parser.add_argument('dest_port', type=int, help="Destination port to which 
to send the CLIENT_HELLO.")
+    parser.add_argument(
+        '--split_size',
+        '-s',
+        type=int,
+        default=1100,
+        help="Size of each split packet. Default is 1100 bytes. "
+        "0 means no split, send the whole packet at once.")
+    parser.add_argument('--num_data_packets', '-d', type=int, default=2, 
help="Number of data packets to send. Default is 2.")
+    return parser.parse_args()
+
+
+def main() -> int:
+    '''Send the CLIENT_HELLO split into two packets.
+    :return: 0 on success, non-zero on failure.
+    '''
+    args = parse_args()
+    status = send_clienthello(args.dest_host, args.dest_port, args.split_size, 
args.num_data_packets)
+    if status == 0:
+        print("CLIENT_HELLO sent successfully.")
+    else:
+        print("CLIENT_HELLO failed.")
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/tests/gold_tests/tls/tls_tunnel.test.py 
b/tests/gold_tests/tls/tls_tunnel.test.py
index 986c3c91b3..440e76bc45 100644
--- a/tests/gold_tests/tls/tls_tunnel.test.py
+++ b/tests/gold_tests/tls/tls_tunnel.test.py
@@ -16,6 +16,9 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+from ports import get_port
+import sys
+
 Test.Summary = '''
 Test tunneling based on SNI
 '''
@@ -34,6 +37,12 @@ response_bar_header = {"headers": "HTTP/1.1 200 
OK\r\nConnection: close\r\n\r\n"
 server_foo.addResponse("sessionlog.json", request_foo_header, 
response_foo_header)
 server_bar.addResponse("sessionlog.json", request_bar_header, 
response_bar_header)
 
+# Server to interact with a split CLIENT_HELLO in a blind tunnel.
+split_client_hello_server = 
Test.Processes.Process('receive_split_client_hello_server')
+server_port = get_port(split_client_hello_server, 'tcp_port')
+server_command = f'{sys.executable} receive_split_client_hello.py 127.0.0.1 
{server_port}'
+split_client_hello_server.Command = server_command
+
 # add ssl materials like key, certificates for the server
 ts.addSSLfile("ssl/signed-foo.pem")
 ts.addSSLfile("ssl/signed-foo.key")
@@ -59,12 +68,14 @@ ts.Disk.records_config.update(
         'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
         'proxy.config.ssl.server.private_key.path': 
'{0}'.format(ts.Variables.SSLDir),
         'proxy.config.http.connect_ports':
-            '{0} {1} {2}'.format(ts.Variables.ssl_port, 
server_foo.Variables.SSL_Port, server_bar.Variables.SSL_Port),
+            '{0} {1} {2} {3}'.format(ts.Variables.ssl_port, 
server_foo.Variables.SSL_Port, server_bar.Variables.SSL_Port, 
split_client_hello_server.Variables.tcp_port),
         'proxy.config.ssl.client.CA.cert.path': 
'{0}'.format(ts.Variables.SSLDir),
         'proxy.config.ssl.client.CA.cert.filename': 'signer.pem',
         'proxy.config.exec_thread.autoconfig.scale': 1.0,
         'proxy.config.url_remap.pristine_host_hdr': 1,
-        'proxy.config.dns.nameservers': 
'127.0.0.1:{0}'.format(dns.Variables.Port),
+        'proxy.config.diags.debug.enabled': 1,
+        'proxy.config.diags.debug.tags': 'http|ssl|proxyprotocol',
+        'proxy.config.dns.nameservers': f'127.0.0.1:{dns.Variables.Port}',
         'proxy.config.dns.resolv_conf': 'NULL'
     })
 
@@ -75,18 +86,21 @@ ts.Disk.sni_yaml.AddLines(
     [
         'sni:',
         '- fqdn: foo.com',
-        "  tunnel_route: localhost:{0}".format(server_foo.Variables.SSL_Port),
+        f"  tunnel_route: localhost:{server_foo.Variables.SSL_Port}",
+        '- fqdn: slashdot.org',
+        f"  tunnel_route: 
localhost:{split_client_hello_server.Variables.tcp_port}",
         "- fqdn: bob.*.com",
-        "  tunnel_route: localhost:{0}".format(server_foo.Variables.SSL_Port),
+        f"  tunnel_route: localhost:{server_foo.Variables.SSL_Port}",
         "- fqdn: '*.match.com'",
-        "  tunnel_route: 
$1.testmatch:{0}".format(server_foo.Variables.SSL_Port),
+        f"  tunnel_route: $1.testmatch:{server_foo.Variables.SSL_Port}",
         "- fqdn: '*.ok.*.com'",
-        "  tunnel_route: 
$2.example.$1:{0}".format(server_foo.Variables.SSL_Port),
+        f"  tunnel_route: $2.example.$1:{server_foo.Variables.SSL_Port}",
         "- fqdn: ''",  # No SNI sent
-        "  tunnel_route: localhost:{0}".format(server_bar.Variables.SSL_Port)
+        f"  tunnel_route: localhost:{server_bar.Variables.SSL_Port}"
     ])
 
 tr = Test.AddTestRun("foo.com Tunnel-test")
+tr.TimeOut = 5
 tr.Processes.Default.Command = "curl -v --resolve 'foo.com:{0}:127.0.0.1' -k  
https://foo.com:{0}".format(ts.Variables.ssl_port)
 tr.ReturnCode = 0
 tr.Processes.Default.StartBefore(server_foo)
@@ -103,6 +117,7 @@ tr.Processes.Default.Streams.All += 
Testers.ExcludesExpression("ATS", "Do not te
 tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", 
"Should get a response from bar")
 
 tr = Test.AddTestRun("bob.bar.com Tunnel-test")
+tr.TimeOut = 5
 tr.Processes.Default.Command = "curl -v --resolve 'bob.bar.com:{0}:127.0.0.1' 
-k  https://bob.bar.com:{0}".format(
     ts.Variables.ssl_port)
 tr.ReturnCode = 0
@@ -116,6 +131,7 @@ tr.Processes.Default.Streams.All += 
Testers.ExcludesExpression("ATS", "Do not te
 tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", 
"Should get a response from bar")
 
 tr = Test.AddTestRun("bar.com no Tunnel-test")
+tr.TimeOut = 5
 tr.Processes.Default.Command = "curl -v --resolve 'bar.com:{0}:127.0.0.1' -k  
https://bar.com:{0}".format(ts.Variables.ssl_port)
 tr.ReturnCode = 0
 tr.StillRunningAfter = ts
@@ -124,6 +140,7 @@ tr.Processes.Default.Streams.All += 
Testers.ContainsExpression("Not Found on Acc
 tr.Processes.Default.Streams.All += Testers.ContainsExpression("ATS", 
"Terminate on Traffic Server")
 
 tr = Test.AddTestRun("no SNI Tunnel-test")
+tr.TimeOut = 5
 tr.Processes.Default.Command = "curl -v -k  
https://127.0.0.1:{0}".format(ts.Variables.ssl_port)
 tr.ReturnCode = 0
 tr.StillRunningAfter = ts
@@ -135,6 +152,7 @@ tr.Processes.Default.Streams.All += 
Testers.ExcludesExpression("ATS", "Do not te
 tr.Processes.Default.Streams.All += Testers.ContainsExpression("bar ok", 
"Should get a response from bar")
 
 tr = Test.AddTestRun("one.match.com Tunnel-test")
+tr.TimeOut = 5
 tr.Processes.Default.Command = "curl -vvv --resolve 
'one.match.com:{0}:127.0.0.1' -k  https://one.match.com:{0}".format(
     ts.Variables.ssl_port)
 tr.ReturnCode = 0
@@ -148,6 +166,7 @@ tr.Processes.Default.Streams.All += 
Testers.ExcludesExpression("ATS", "Do not te
 tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", 
"Should get a response from tm")
 
 tr = Test.AddTestRun("one.ok.two.com Tunnel-test")
+tr.TimeOut = 5
 tr.Processes.Default.Command = "curl -vvv --resolve 
'one.ok.two.com:{0}:127.0.0.1' -k  https:/one.ok.two.com:{0}".format(
     ts.Variables.ssl_port)
 tr.ReturnCode = 0
@@ -178,6 +197,35 @@ tr.Processes.Default.Env = ts.Env
 tr.Processes.Default.Command = 'echo Updated configs'
 tr.Processes.Default.ReturnCode = 0
 
+# Test a large CLIENT_HELLO.
+tr = Test.AddTestRun("Single large CLIENT_HELLO blind tunnel test")
+tr.TimeOut = 5
+tr.Setup.Copy('split_client_hello.py')
+tr.Setup.Copy('receive_split_client_hello.py')
+p = tr.Processes.Default
+p.StartBefore(split_client_hello_server, ready=When.PortOpen(server_port))
+p.Command = f'{sys.executable} split_client_hello.py 127.0.0.1 
{ts.Variables.ssl_port} -s 0'
+p.ReturnCode = 0
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = dns
+p.Streams.All += Testers.ContainsExpression('dummy SERVER_HELLO', 'Verify a 
dummy SERVER_HELLO response is received')
+p.Streams.All += Testers.ContainsExpression('data: 0', 'Verify that the first 
data packet was received.')
+p.Streams.All += Testers.ContainsExpression('data: 1', 'Verify that the second 
data packet was received.')
+
+# Test a CLIENT_HELLO split over two TCP packets.
+tr = Test.AddTestRun("Split large CLIENT_HELLO blind tunnel test")
+tr.TimeOut = 5
+tr.Setup.Copy('split_client_hello.py')
+tr.Setup.Copy('receive_split_client_hello.py')
+p = tr.Processes.Default
+p.Command = f'{sys.executable} split_client_hello.py 127.0.0.1 
{ts.Variables.ssl_port}'
+p.ReturnCode = 0
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = dns
+p.Streams.All += Testers.ContainsExpression('dummy SERVER_HELLO', 'Verify a 
dummy SERVER_HELLO response is received')
+p.Streams.All += Testers.ContainsExpression('data: 0', 'Verify that the first 
data packet was received.')
+p.Streams.All += Testers.ContainsExpression('data: 1', 'Verify that the second 
data packet was received.')
+
 trreload = Test.AddTestRun("Reload config")
 trreload.StillRunningAfter = ts
 trreload.StillRunningAfter = server_foo
@@ -189,6 +237,7 @@ trreload.Processes.Default.ReturnCode = 0
 
 # Should terminate on traffic_server (not tunnel)
 tr = Test.AddTestRun("foo.com no Tunnel-test")
+tr.TimeOut = 30
 tr.StillRunningAfter = ts
 # Wait for the reload to complete by running the sni_reload_done test
 tr.Processes.Default.StartBefore(server2, 
ready=When.FileContains(ts.Disk.diags_log.Name, 'sni.yaml finished loading', 2))
@@ -196,7 +245,6 @@ tr.Processes.Default.Command = "curl -v --resolve 
'foo.com:{0}:127.0.0.1' -k  ht
 tr.Processes.Default.Streams.All += Testers.ContainsExpression("Not Found on 
Accelerato", "Terminates on on Traffic Server")
 tr.Processes.Default.Streams.All += Testers.ContainsExpression("ATS", 
"Terminate on Traffic Server")
 tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not 
Connect", "Curl attempt should have succeeded")
-tr.TimeOut = 30
 
 # Should tunnel to server_bar
 tr = Test.AddTestRun("bar.com  Tunnel-test")


Reply via email to