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

bcall 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 2baa396b90 Add ERR_TUN_ACTIVE_TIMEOUT squid code for tunnel timeouts 
(#12786)
2baa396b90 is described below

commit 2baa396b90b2550101c5acc2273123afcc1fcb14
Author: Bryan Call <[email protected]>
AuthorDate: Mon Apr 13 13:51:20 2026 -0700

    Add ERR_TUN_ACTIVE_TIMEOUT squid code for tunnel timeouts (#12786)
    
    * Add ERR_TUN_ACTIVE_TIMEOUT squid code ('T') so CONNECT
      tunnel timeouts are visible in access logs via the crc
      field instead of only in DEBUG output.
    * Set the code in tunnel_handler_ssl_producer() and
      tunnel_handler_ssl_consumer() on VC_EVENT_ACTIVE_TIMEOUT.
    * Add the code to traffic_logstats switch statements so
      tunnel timeouts count in the errors bucket.
    * Add autest with Python CONNECT client that triggers a
      tunnel timeout and verifies the log entry.
---
 include/proxy/hdrs/HTTP.h                          |  1 +
 src/proxy/http/HttpSM.cc                           |  8 ++
 src/proxy/logging/Log.cc                           |  5 +-
 src/traffic_logstats/logstats.cc                   |  2 +
 .../timeout/tunnel_active_timeout.test.py          | 93 ++++++++++++++++++++++
 tests/gold_tests/timeout/tunnel_timeout_client.py  | 86 ++++++++++++++++++++
 6 files changed, 193 insertions(+), 2 deletions(-)

diff --git a/include/proxy/hdrs/HTTP.h b/include/proxy/hdrs/HTTP.h
index dd69181120..104743e593 100644
--- a/include/proxy/hdrs/HTTP.h
+++ b/include/proxy/hdrs/HTTP.h
@@ -162,6 +162,7 @@ enum class SquidLogCode {
   ERR_FUTURE_1              = 'I',
   ERR_CLIENT_READ_ERROR     = 'J', // Client side abort logging
   ERR_LOOP_DETECTED         = 'K', // Loop or cycle detected, request came 
back to this server
+  ERR_TUN_ACTIVE_TIMEOUT    = 'T', // Tunnel (CONNECT) active timeout
   ERR_UNKNOWN               = 'Z'
 };
 
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 85d1b4caf4..fcdf912e75 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -4162,6 +4162,10 @@ HttpSM::tunnel_handler_ssl_producer(int event, 
HttpTunnelProducer *p)
   case VC_EVENT_ERROR:
   case VC_EVENT_INACTIVITY_TIMEOUT:
   case VC_EVENT_ACTIVE_TIMEOUT:
+    // Set squid code for tunnel active timeout
+    if (event == VC_EVENT_ACTIVE_TIMEOUT) {
+      t_state.squid_codes.log_code = SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT;
+    }
     // The other side of the connection is either already dead
     //   or rendered inoperative by the error on the connection
     //   Note: use tunnel close vc so the tunnel knows we are
@@ -4216,6 +4220,10 @@ HttpSM::tunnel_handler_ssl_consumer(int event, 
HttpTunnelConsumer *c)
   case VC_EVENT_EOS:
   case VC_EVENT_INACTIVITY_TIMEOUT:
   case VC_EVENT_ACTIVE_TIMEOUT:
+    // Set squid code for tunnel active timeout
+    if (event == VC_EVENT_ACTIVE_TIMEOUT) {
+      t_state.squid_codes.log_code = SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT;
+    }
     // we need to mark the producer dead
     // otherwise it can stay alive forever.
     if (c->producer->alive) {
diff --git a/src/proxy/logging/Log.cc b/src/proxy/logging/Log.cc
index 3462bfaefb..49a21978b7 100644
--- a/src/proxy/logging/Log.cc
+++ b/src/proxy/logging/Log.cc
@@ -665,7 +665,7 @@ Log::init_fields()
 
   Ptr<LogFieldAliasTable> cache_code_map = make_ptr(new LogFieldAliasTable);
   cache_code_map->init(
-    53, SquidLogCode::EMPTY, "UNDEFINED", SquidLogCode::TCP_HIT, "TCP_HIT", 
SquidLogCode::TCP_DISK_HIT, "TCP_DISK_HIT",
+    54, SquidLogCode::EMPTY, "UNDEFINED", SquidLogCode::TCP_HIT, "TCP_HIT", 
SquidLogCode::TCP_DISK_HIT, "TCP_DISK_HIT",
     SquidLogCode::TCP_MEM_HIT, "TCP_MEM_HIT", SquidLogCode::TCP_MISS, 
"TCP_MISS", SquidLogCode::TCP_EXPIRED_MISS,
     "TCP_EXPIRED_MISS", SquidLogCode::TCP_REFRESH_HIT, "TCP_REFRESH_HIT", 
SquidLogCode::TCP_REF_FAIL_HIT, "TCP_REFRESH_FAIL_HIT",
     SquidLogCode::TCP_REFRESH_MISS, "TCP_REFRESH_MISS", 
SquidLogCode::TCP_CLIENT_REFRESH, "TCP_CLIENT_REFRESH_MISS",
@@ -686,7 +686,8 @@ Log::init_fields()
     SquidLogCode::ERR_NO_RELAY, "ERR_NO_RELAY", SquidLogCode::ERR_DISK_IO, 
"ERR_DISK_IO", SquidLogCode::ERR_ZERO_SIZE_OBJECT,
     "ERR_ZERO_SIZE_OBJECT", SquidLogCode::ERR_PROXY_DENIED, 
"ERR_PROXY_DENIED", SquidLogCode::ERR_WEBFETCH_DETECTED,
     "ERR_WEBFETCH_DETECTED", SquidLogCode::ERR_FUTURE_1, "ERR_FUTURE_1", 
SquidLogCode::ERR_LOOP_DETECTED, "ERR_LOOP_DETECTED",
-    SquidLogCode::ERR_UNKNOWN, "ERR_UNKNOWN", SquidLogCode::TCP_CF_HIT, 
"TCP_CF_HIT");
+    SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT, "ERR_TUN_ACTIVE_TIMEOUT", 
SquidLogCode::ERR_UNKNOWN, "ERR_UNKNOWN",
+    SquidLogCode::TCP_CF_HIT, "TCP_CF_HIT");
 
   Ptr<LogFieldAliasTable> cache_subcode_map = make_ptr(new LogFieldAliasTable);
   cache_subcode_map->init(2, SquidSubcode::EMPTY, "NONE", 
SquidSubcode::NUM_REDIRECTIONS_EXCEEDED, "NUM_REDIRECTIONS_EXCEEDED");
diff --git a/src/traffic_logstats/logstats.cc b/src/traffic_logstats/logstats.cc
index f9099b8c9c..8904ded0a8 100644
--- a/src/traffic_logstats/logstats.cc
+++ b/src/traffic_logstats/logstats.cc
@@ -450,6 +450,7 @@ public:
       case SquidLogCode::ERR_INVALID_REQ:
       case SquidLogCode::ERR_UNKNOWN:
       case SquidLogCode::ERR_READ_TIMEOUT:
+      case SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT:
         ++(l->errors);
         break;
       default:
@@ -527,6 +528,7 @@ public:
       case SquidLogCode::ERR_INVALID_REQ:
       case SquidLogCode::ERR_UNKNOWN:
       case SquidLogCode::ERR_READ_TIMEOUT:
+      case SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT:
         l->errors = 1;
         break;
       default:
diff --git a/tests/gold_tests/timeout/tunnel_active_timeout.test.py 
b/tests/gold_tests/timeout/tunnel_active_timeout.test.py
new file mode 100644
index 0000000000..80c5840ace
--- /dev/null
+++ b/tests/gold_tests/timeout/tunnel_active_timeout.test.py
@@ -0,0 +1,93 @@
+'''
+Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code.
+'''
+#  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 os
+import sys
+
+Test.Summary = '''
+Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code.
+'''
+
+ts = Test.MakeATSProcess("ts", enable_tls=True)
+server = Test.MakeOriginServer("server", ssl=True)
+
+# Simple response from origin
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: server\r\n\r\n", 
"timestamp": "1234", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", 
"timestamp": "1234", "body": "hello"}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addDefaultSSLFiles()
+
+ts.Disk.ssl_multicert_config.AddLine('dest_ip=* ssl_cert_name=server.pem 
ssl_key_name=server.key')
+
+ts.Disk.records_config.update(
+    {
+        'proxy.config.diags.debug.enabled': 1,
+        'proxy.config.diags.debug.tags': 'http|ssl|tunnel',
+        'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir,
+        'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir,
+        'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE',
+        'proxy.config.http.connect_ports': f'{server.Variables.SSL_Port}',
+        # Set a short active timeout for tunnels (2 seconds)
+        'proxy.config.http.transaction_active_timeout_in': 2,
+        # Force log flush every second for test reliability
+        'proxy.config.log.max_secs_per_buffer': 1,
+    })
+
+ts.Disk.remap_config.AddLine(f'map / 
https://127.0.0.1:{server.Variables.SSL_Port}')
+
+# Configure custom log format to capture squid code
+ts.Disk.logging_yaml.AddLines(
+    '''
+logging:
+  formats:
+    - name: custom
+      format: '%<crc> %<pssc> %<cqhm>'
+  logs:
+    - filename: squid.log
+      format: custom
+'''.split("\n"))
+
+# Test: Perform a CONNECT request that will time out
+tr = Test.AddTestRun("Tunnel active timeout test")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(ts)
+
+# Use the tunnel_timeout_client.py script to establish a CONNECT tunnel and 
then
+# just hold the connection until ATS times it out
+tr.Setup.Copy('tunnel_timeout_client.py')
+
+# Connect, establish tunnel, then sleep to trigger active timeout
+tr.Processes.Default.Command = (
+    f'{sys.executable} tunnel_timeout_client.py 127.0.0.1 {ts.Variables.port} '
+    f'127.0.0.1 {server.Variables.SSL_Port} 5')
+# The connection will be closed by ATS due to timeout
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+
+# Wait for the access log to be written
+tr = Test.AddTestRun("Wait for the access log to write out")
+tr.DelayStart = 3
+tr.StillRunningAfter = ts
+tr.Processes.Default.Command = 'echo "waiting for log flush"'
+tr.Processes.Default.ReturnCode = 0
+
+# Verify the squid code in the access log
+ts.Disk.File(os.path.join(ts.Variables.LOGDIR, 'squid.log')).Content = 
Testers.ContainsExpression(
+    'ERR_TUN_ACTIVE_TIMEOUT.*CONNECT', 'Verify the tunnel timeout squid code 
is logged')
diff --git a/tests/gold_tests/timeout/tunnel_timeout_client.py 
b/tests/gold_tests/timeout/tunnel_timeout_client.py
new file mode 100644
index 0000000000..d96da1c974
--- /dev/null
+++ b/tests/gold_tests/timeout/tunnel_timeout_client.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+'''
+A simple client that establishes a CONNECT tunnel and then holds the connection
+idle to trigger an active timeout.
+'''
+#  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 socket
+import sys
+
+
+def main():
+    if len(sys.argv) < 6:
+        print(f"Usage: {sys.argv[0]} proxy_host proxy_port target_host 
target_port sleep_seconds")
+        sys.exit(1)
+
+    proxy_host = sys.argv[1]
+    proxy_port = int(sys.argv[2])
+    target_host = sys.argv[3]
+    target_port = int(sys.argv[4])
+    sleep_seconds = int(sys.argv[5])
+
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+        sock.settimeout(10)
+
+        try:
+            sock.connect((proxy_host, proxy_port))
+            print(f"Connected to proxy {proxy_host}:{proxy_port}")
+
+            connect_request = f"CONNECT {target_host}:{target_port} 
HTTP/1.1\r\nHost: {target_host}:{target_port}\r\n\r\n"
+            sock.sendall(connect_request.encode())
+            print(f"Sent CONNECT request for {target_host}:{target_port}")
+
+            response = b""
+            while b"\r\n\r\n" not in response:
+                data = sock.recv(1024)
+                if not data:
+                    break
+                response += data
+
+            response_str = response.decode()
+            print(f"Received response: {response_str.strip()}")
+
+            if "200" not in response_str:
+                print(f"CONNECT failed: {response_str}")
+                sys.exit(1)
+
+            print(f"Tunnel established, sleeping for {sleep_seconds} seconds 
to trigger active timeout...")
+
+            sock.settimeout(sleep_seconds + 5)
+
+            try:
+                data = sock.recv(1024)
+                if not data:
+                    print("Connection closed by server (timeout)")
+                else:
+                    print(f"Received data: {data}")
+            except socket.timeout:
+                print("Socket timeout waiting for server")
+            except Exception as e:
+                print(f"Exception: {e}")
+
+        except socket.timeout:
+            print("Socket timeout during connect/handshake")
+        except Exception as e:
+            print(f"Error: {e}")
+
+    print("Done")
+
+
+if __name__ == "__main__":
+    main()

Reply via email to