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

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new b2d1b36186 Wait for CONNECT origin handshakes (#13512)
b2d1b36186 is described below

commit b2d1b36186fa9ed99f3e22c004d1924ed3ffa9a3
Author: Brian Neradt <[email protected]>
AuthorDate: Tue Aug 11 19:17:42 2026 -0500

    Wait for CONNECT origin handshakes (#13512)
    
    Explicit proxy CONNECT requests can return 200 before the
    nonblocking origin connection finishes. A refused origin port then
    looks like a successful tunnel followed by a client-side write failure.
    
    This patch waits for write readiness on raw origin connections before
    sending the CONNECT response. It reports connection failures to the
    client and adds an AuTest covering a refused origin port.
    
    Fixes: #7677
---
 src/proxy/http/HttpSM.cc                           | 52 +++++++++++++++--
 tests/gold_tests/connect/connect_handshake.test.py | 65 ++++++++++++++++++++++
 .../connect/replays/connect_handshake.replay.yaml  | 34 +++++++++++
 3 files changed, 146 insertions(+), 5 deletions(-)

diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 7a8f59086a..0d54ad376d 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -1149,11 +1149,37 @@ HttpSM::state_raw_http_server_open(int event, void 
*data)
   pending_action = nullptr;
   switch (event) {
   case NET_EVENT_OPEN: {
+    netvc = static_cast<NetVConnection *>(data);
+    if (plugin_tunnel_type == HttpPluginTunnel_t::NONE) {
+      _netvc             = netvc;
+      _netvc_read_buffer = new_MIOBuffer(HTTP_SERVER_RESP_HDR_BUFFER_INDEX);
+      _netvc_reader      = _netvc_read_buffer->alloc_reader();
+
+      // Wait for write readiness to verify that the nonblocking TCP connection
+      // completed before reporting a successful tunnel to the client.
+      _netvc->do_io_write(this, 1, _netvc_reader);
+      _netvc->set_inactivity_timeout(get_server_connect_timeout());
+      return 0;
+    }
+    [[fallthrough]];
+  }
+  case VC_EVENT_READ_COMPLETE:
+  case VC_EVENT_WRITE_READY:
+  case VC_EVENT_WRITE_COMPLETE: {
+    if (netvc == nullptr) {
+      netvc = _netvc;
+      netvc->do_io_write(nullptr, 0, nullptr);
+      free_MIOBuffer(_netvc_read_buffer);
+      _netvc             = nullptr;
+      _netvc_read_buffer = nullptr;
+      _netvc_reader      = nullptr;
+    }
+
     // Record the VC in our table
-    server_entry     = vc_table.new_entry();
-    server_entry->vc = netvc = static_cast<NetVConnection *>(data);
-    server_entry->vc_type    = HttpVC_t::RAW_SERVER_VC;
-    t_state.current.state    = HttpTransact::CONNECTION_ALIVE;
+    server_entry          = vc_table.new_entry();
+    server_entry->vc      = netvc;
+    server_entry->vc_type = HttpVC_t::RAW_SERVER_VC;
+    t_state.current.state = HttpTransact::CONNECTION_ALIVE;
     ats_ip_copy(&t_state.server_info.src_addr, netvc->get_local_addr());
 
     netvc->set_inactivity_timeout(get_server_inactivity_timeout());
@@ -1166,9 +1192,24 @@ HttpSM::state_raw_http_server_open(int event, void *data)
 
     break;
   }
+  case VC_EVENT_INACTIVITY_TIMEOUT:
+  case VC_EVENT_ACTIVE_TIMEOUT:
+    t_state.set_connect_fail(ETIMEDOUT);
+    [[fallthrough]];
   case VC_EVENT_ERROR:
   case VC_EVENT_EOS:
-  case NET_EVENT_OPEN_FAILED:
+  case NET_EVENT_OPEN_FAILED: {
+    if (_netvc != nullptr) {
+      if (event == VC_EVENT_ERROR || event == NET_EVENT_OPEN_FAILED) {
+        t_state.set_connect_fail(_netvc->lerrno);
+      }
+      _netvc->do_io_write(nullptr, 0, nullptr);
+      _netvc->do_io_close();
+      _netvc = nullptr;
+      free_MIOBuffer(_netvc_read_buffer);
+      _netvc_read_buffer = nullptr;
+      _netvc_reader      = nullptr;
+    }
     if (t_state.cause_of_death_errno == -UNKNOWN_INTERNAL_ERROR) {
       if (event == VC_EVENT_EOS) {
         t_state.set_connect_fail(EPIPE);
@@ -1180,6 +1221,7 @@ HttpSM::state_raw_http_server_open(int event, void *data)
     // use this value just to get around other values
     t_state.hdr_info.response_error = 
HttpTransact::ResponseError_t::STATUS_CODE_SERVER_ERROR;
     break;
+  }
   case EVENT_INTERVAL:
     // If we get EVENT_INTERNAL it means that we moved the transaction
     // to a different thread in do_http_server_open.  Since we didn't
diff --git a/tests/gold_tests/connect/connect_handshake.test.py 
b/tests/gold_tests/connect/connect_handshake.test.py
new file mode 100644
index 0000000000..1d3793dace
--- /dev/null
+++ b/tests/gold_tests/connect/connect_handshake.test.py
@@ -0,0 +1,65 @@
+'''Verify CONNECT waits for the origin TCP handshake.'''
+#  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 ports
+
+Test.Summary = 'Verify CONNECT waits for the origin TCP handshake.'
+
+
+class ConnectHandshakeTest:
+    '''Verify CONNECT failure is reported before establishing a tunnel.'''
+
+    replay_file: str = 'replays/connect_handshake.replay.yaml'
+
+    def __init__(self) -> None:
+        '''Configure the test run.'''
+        tr = Test.AddTestRun('CONNECT to a refused origin port')
+        self._configure_unavailable_origin(tr)
+        self._configure_traffic_server(tr)
+        self._configure_client(tr)
+
+    def _configure_unavailable_origin(self, tr: 'TestRun') -> 'Process':
+        '''Reserve an origin port without starting a listening server.'''
+        origin = tr.Processes.Process('unavailable-origin')
+        ports.get_port(origin, 'Port')
+        self._origin = origin
+        return origin
+
+    def _configure_traffic_server(self, tr: 'TestRun') -> 'Process':
+        '''Configure Traffic Server as an explicit proxy.'''
+        ts = tr.MakeATSProcess('ts', enable_cache=False)
+        self._ts = ts
+
+        origin_port = self._origin.Variables.Port
+        ts.Disk.records_config.update(
+            {
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'http|iocore_net',
+                'proxy.config.http.connect_ports': f'{origin_port}',
+            })
+        ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{origin_port}')
+        ts.addPrivateConnectAllowYaml()
+        return ts
+
+    def _configure_client(self, tr: 'TestRun') -> 'Process':
+        '''Configure a Proxy Verifier client that expects the refusal.'''
+        client = tr.AddVerifierClientProcess('client', self.replay_file, 
http_ports=[self._ts.Variables.port])
+        client.StartBefore(self._ts)
+        return client
+
+
+ConnectHandshakeTest()
diff --git a/tests/gold_tests/connect/replays/connect_handshake.replay.yaml 
b/tests/gold_tests/connect/replays/connect_handshake.replay.yaml
new file mode 100644
index 0000000000..dac446ca89
--- /dev/null
+++ b/tests/gold_tests/connect/replays/connect_handshake.replay.yaml
@@ -0,0 +1,34 @@
+#  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.
+
+meta:
+  version: '1.0'
+
+sessions:
+  - transactions:
+      - client-request:
+          method: CONNECT
+          version: '1.1'
+          url: www.example.com:443
+          headers:
+            fields:
+              - [Host, www.example.com:443]
+              - [uuid, connect-refused]
+
+        # ATS must report the refused origin connection rather than claim the
+        # tunnel was established.
+        proxy-response:
+          status: 502

Reply via email to