Skip to content

Commit 2baa396

Browse files
authored
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.
1 parent 123b668 commit 2baa396

6 files changed

Lines changed: 193 additions & 2 deletions

File tree

include/proxy/hdrs/HTTP.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ enum class SquidLogCode {
162162
ERR_FUTURE_1 = 'I',
163163
ERR_CLIENT_READ_ERROR = 'J', // Client side abort logging
164164
ERR_LOOP_DETECTED = 'K', // Loop or cycle detected, request came back to this server
165+
ERR_TUN_ACTIVE_TIMEOUT = 'T', // Tunnel (CONNECT) active timeout
165166
ERR_UNKNOWN = 'Z'
166167
};
167168

src/proxy/http/HttpSM.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4162,6 +4162,10 @@ HttpSM::tunnel_handler_ssl_producer(int event, HttpTunnelProducer *p)
41624162
case VC_EVENT_ERROR:
41634163
case VC_EVENT_INACTIVITY_TIMEOUT:
41644164
case VC_EVENT_ACTIVE_TIMEOUT:
4165+
// Set squid code for tunnel active timeout
4166+
if (event == VC_EVENT_ACTIVE_TIMEOUT) {
4167+
t_state.squid_codes.log_code = SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT;
4168+
}
41654169
// The other side of the connection is either already dead
41664170
// or rendered inoperative by the error on the connection
41674171
// Note: use tunnel close vc so the tunnel knows we are
@@ -4216,6 +4220,10 @@ HttpSM::tunnel_handler_ssl_consumer(int event, HttpTunnelConsumer *c)
42164220
case VC_EVENT_EOS:
42174221
case VC_EVENT_INACTIVITY_TIMEOUT:
42184222
case VC_EVENT_ACTIVE_TIMEOUT:
4223+
// Set squid code for tunnel active timeout
4224+
if (event == VC_EVENT_ACTIVE_TIMEOUT) {
4225+
t_state.squid_codes.log_code = SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT;
4226+
}
42194227
// we need to mark the producer dead
42204228
// otherwise it can stay alive forever.
42214229
if (c->producer->alive) {

src/proxy/logging/Log.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ Log::init_fields()
665665

666666
Ptr<LogFieldAliasTable> cache_code_map = make_ptr(new LogFieldAliasTable);
667667
cache_code_map->init(
668-
53, SquidLogCode::EMPTY, "UNDEFINED", SquidLogCode::TCP_HIT, "TCP_HIT", SquidLogCode::TCP_DISK_HIT, "TCP_DISK_HIT",
668+
54, SquidLogCode::EMPTY, "UNDEFINED", SquidLogCode::TCP_HIT, "TCP_HIT", SquidLogCode::TCP_DISK_HIT, "TCP_DISK_HIT",
669669
SquidLogCode::TCP_MEM_HIT, "TCP_MEM_HIT", SquidLogCode::TCP_MISS, "TCP_MISS", SquidLogCode::TCP_EXPIRED_MISS,
670670
"TCP_EXPIRED_MISS", SquidLogCode::TCP_REFRESH_HIT, "TCP_REFRESH_HIT", SquidLogCode::TCP_REF_FAIL_HIT, "TCP_REFRESH_FAIL_HIT",
671671
SquidLogCode::TCP_REFRESH_MISS, "TCP_REFRESH_MISS", SquidLogCode::TCP_CLIENT_REFRESH, "TCP_CLIENT_REFRESH_MISS",
@@ -686,7 +686,8 @@ Log::init_fields()
686686
SquidLogCode::ERR_NO_RELAY, "ERR_NO_RELAY", SquidLogCode::ERR_DISK_IO, "ERR_DISK_IO", SquidLogCode::ERR_ZERO_SIZE_OBJECT,
687687
"ERR_ZERO_SIZE_OBJECT", SquidLogCode::ERR_PROXY_DENIED, "ERR_PROXY_DENIED", SquidLogCode::ERR_WEBFETCH_DETECTED,
688688
"ERR_WEBFETCH_DETECTED", SquidLogCode::ERR_FUTURE_1, "ERR_FUTURE_1", SquidLogCode::ERR_LOOP_DETECTED, "ERR_LOOP_DETECTED",
689-
SquidLogCode::ERR_UNKNOWN, "ERR_UNKNOWN", SquidLogCode::TCP_CF_HIT, "TCP_CF_HIT");
689+
SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT, "ERR_TUN_ACTIVE_TIMEOUT", SquidLogCode::ERR_UNKNOWN, "ERR_UNKNOWN",
690+
SquidLogCode::TCP_CF_HIT, "TCP_CF_HIT");
690691

691692
Ptr<LogFieldAliasTable> cache_subcode_map = make_ptr(new LogFieldAliasTable);
692693
cache_subcode_map->init(2, SquidSubcode::EMPTY, "NONE", SquidSubcode::NUM_REDIRECTIONS_EXCEEDED, "NUM_REDIRECTIONS_EXCEEDED");

src/traffic_logstats/logstats.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ class UrlLru
450450
case SquidLogCode::ERR_INVALID_REQ:
451451
case SquidLogCode::ERR_UNKNOWN:
452452
case SquidLogCode::ERR_READ_TIMEOUT:
453+
case SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT:
453454
++(l->errors);
454455
break;
455456
default:
@@ -527,6 +528,7 @@ class UrlLru
527528
case SquidLogCode::ERR_INVALID_REQ:
528529
case SquidLogCode::ERR_UNKNOWN:
529530
case SquidLogCode::ERR_READ_TIMEOUT:
531+
case SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT:
530532
l->errors = 1;
531533
break;
532534
default:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'''
2+
Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code.
3+
'''
4+
# Licensed to the Apache Software Foundation (ASF) under one
5+
# or more contributor license agreements. See the NOTICE file
6+
# distributed with this work for additional information
7+
# regarding copyright ownership. The ASF licenses this file
8+
# to you under the Apache License, Version 2.0 (the
9+
# "License"); you may not use this file except in compliance
10+
# with the License. You may obtain a copy of the License at
11+
#
12+
# http://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing, software
15+
# distributed under the License is distributed on an "AS IS" BASIS,
16+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# See the License for the specific language governing permissions and
18+
# limitations under the License.
19+
20+
import os
21+
import sys
22+
23+
Test.Summary = '''
24+
Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code.
25+
'''
26+
27+
ts = Test.MakeATSProcess("ts", enable_tls=True)
28+
server = Test.MakeOriginServer("server", ssl=True)
29+
30+
# Simple response from origin
31+
request_header = {"headers": "GET / HTTP/1.1\r\nHost: server\r\n\r\n", "timestamp": "1234", "body": ""}
32+
response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1234", "body": "hello"}
33+
server.addResponse("sessionlog.json", request_header, response_header)
34+
35+
ts.addDefaultSSLFiles()
36+
37+
ts.Disk.ssl_multicert_config.AddLine('dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key')
38+
39+
ts.Disk.records_config.update(
40+
{
41+
'proxy.config.diags.debug.enabled': 1,
42+
'proxy.config.diags.debug.tags': 'http|ssl|tunnel',
43+
'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir,
44+
'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir,
45+
'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE',
46+
'proxy.config.http.connect_ports': f'{server.Variables.SSL_Port}',
47+
# Set a short active timeout for tunnels (2 seconds)
48+
'proxy.config.http.transaction_active_timeout_in': 2,
49+
# Force log flush every second for test reliability
50+
'proxy.config.log.max_secs_per_buffer': 1,
51+
})
52+
53+
ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.SSL_Port}')
54+
55+
# Configure custom log format to capture squid code
56+
ts.Disk.logging_yaml.AddLines(
57+
'''
58+
logging:
59+
formats:
60+
- name: custom
61+
format: '%<crc> %<pssc> %<cqhm>'
62+
logs:
63+
- filename: squid.log
64+
format: custom
65+
'''.split("\n"))
66+
67+
# Test: Perform a CONNECT request that will time out
68+
tr = Test.AddTestRun("Tunnel active timeout test")
69+
tr.Processes.Default.StartBefore(server)
70+
tr.Processes.Default.StartBefore(ts)
71+
72+
# Use the tunnel_timeout_client.py script to establish a CONNECT tunnel and then
73+
# just hold the connection until ATS times it out
74+
tr.Setup.Copy('tunnel_timeout_client.py')
75+
76+
# Connect, establish tunnel, then sleep to trigger active timeout
77+
tr.Processes.Default.Command = (
78+
f'{sys.executable} tunnel_timeout_client.py 127.0.0.1 {ts.Variables.port} '
79+
f'127.0.0.1 {server.Variables.SSL_Port} 5')
80+
# The connection will be closed by ATS due to timeout
81+
tr.Processes.Default.ReturnCode = 0
82+
tr.StillRunningAfter = ts
83+
84+
# Wait for the access log to be written
85+
tr = Test.AddTestRun("Wait for the access log to write out")
86+
tr.DelayStart = 3
87+
tr.StillRunningAfter = ts
88+
tr.Processes.Default.Command = 'echo "waiting for log flush"'
89+
tr.Processes.Default.ReturnCode = 0
90+
91+
# Verify the squid code in the access log
92+
ts.Disk.File(os.path.join(ts.Variables.LOGDIR, 'squid.log')).Content = Testers.ContainsExpression(
93+
'ERR_TUN_ACTIVE_TIMEOUT.*CONNECT', 'Verify the tunnel timeout squid code is logged')
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
'''
3+
A simple client that establishes a CONNECT tunnel and then holds the connection
4+
idle to trigger an active timeout.
5+
'''
6+
# Licensed to the Apache Software Foundation (ASF) under one
7+
# or more contributor license agreements. See the NOTICE file
8+
# distributed with this work for additional information
9+
# regarding copyright ownership. The ASF licenses this file
10+
# to you under the Apache License, Version 2.0 (the
11+
# "License"); you may not use this file except in compliance
12+
# with the License. You may obtain a copy of the License at
13+
#
14+
# http://www.apache.org/licenses/LICENSE-2.0
15+
#
16+
# Unless required by applicable law or agreed to in writing, software
17+
# distributed under the License is distributed on an "AS IS" BASIS,
18+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
# See the License for the specific language governing permissions and
20+
# limitations under the License.
21+
22+
import socket
23+
import sys
24+
25+
26+
def main():
27+
if len(sys.argv) < 6:
28+
print(f"Usage: {sys.argv[0]} proxy_host proxy_port target_host target_port sleep_seconds")
29+
sys.exit(1)
30+
31+
proxy_host = sys.argv[1]
32+
proxy_port = int(sys.argv[2])
33+
target_host = sys.argv[3]
34+
target_port = int(sys.argv[4])
35+
sleep_seconds = int(sys.argv[5])
36+
37+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
38+
sock.settimeout(10)
39+
40+
try:
41+
sock.connect((proxy_host, proxy_port))
42+
print(f"Connected to proxy {proxy_host}:{proxy_port}")
43+
44+
connect_request = f"CONNECT {target_host}:{target_port} HTTP/1.1\r\nHost: {target_host}:{target_port}\r\n\r\n"
45+
sock.sendall(connect_request.encode())
46+
print(f"Sent CONNECT request for {target_host}:{target_port}")
47+
48+
response = b""
49+
while b"\r\n\r\n" not in response:
50+
data = sock.recv(1024)
51+
if not data:
52+
break
53+
response += data
54+
55+
response_str = response.decode()
56+
print(f"Received response: {response_str.strip()}")
57+
58+
if "200" not in response_str:
59+
print(f"CONNECT failed: {response_str}")
60+
sys.exit(1)
61+
62+
print(f"Tunnel established, sleeping for {sleep_seconds} seconds to trigger active timeout...")
63+
64+
sock.settimeout(sleep_seconds + 5)
65+
66+
try:
67+
data = sock.recv(1024)
68+
if not data:
69+
print("Connection closed by server (timeout)")
70+
else:
71+
print(f"Received data: {data}")
72+
except socket.timeout:
73+
print("Socket timeout waiting for server")
74+
except Exception as e:
75+
print(f"Exception: {e}")
76+
77+
except socket.timeout:
78+
print("Socket timeout during connect/handshake")
79+
except Exception as e:
80+
print(f"Error: {e}")
81+
82+
print("Done")
83+
84+
85+
if __name__ == "__main__":
86+
main()

0 commit comments

Comments
 (0)