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 013ec3cacb Harden the tunnel_transform Autest (#10726)
013ec3cacb is described below
commit 013ec3cacbba8bdd1c3fa14d08ffde4c8bcb825f
Author: Zhengxi Li <[email protected]>
AuthorDate: Tue Nov 7 12:59:48 2023 -0500
Harden the tunnel_transform Autest (#10726)
The tunnel_transform test failed internally in certain environment as the
actual TLS traffic byte counts fell out of the hard-coded range of expected
value.
The TLS byte count can varied across different openssl/curl versions, so
this PR adds a simple forwarding proxy to keep track of the exact byte count
between the client and server, as suggested in the test comment. The exact byte
count is used in the validation.
Note: originally I was trying a nc + pipe solution. It worked in my
environment but I am concerned about the portability of different versions of
nc and different characteristics of named pipe on different
platforms(unidirectional VS bidirectional). So sticking with the hand-rolled
proxy for portability.
---
tests/gold_tests/tunnel/dumb_proxy.py | 130 +++++++++++++++++++++++
tests/gold_tests/tunnel/tunnel_transform.test.py | 59 ++++++----
2 files changed, 171 insertions(+), 18 deletions(-)
diff --git a/tests/gold_tests/tunnel/dumb_proxy.py
b/tests/gold_tests/tunnel/dumb_proxy.py
new file mode 100644
index 0000000000..bd5b0bdf81
--- /dev/null
+++ b/tests/gold_tests/tunnel/dumb_proxy.py
@@ -0,0 +1,130 @@
+'''
+A simple forwarding proxy that forwards all traffic from one local port to
+another, while keeping track of the number of bytes transferred in each
+direction.
+'''
+# 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 threading
+import argparse
+
+LOCAL_HOST = '127.0.0.1'
+TIMEOUT = 0.5
+# Create a thread-local data object to store the number of bytes transferred.
+thread_local_data = threading.local()
+
+
+def parse_args() -> argparse.Namespace:
+ """Parse command line arguments."""
+ parser = argparse.ArgumentParser(description=__doc__)
+
+ parser.add_argument(
+ '--listening_port',
+ type=int,
+ help='Port where the proxy listens.')
+
+ parser.add_argument(
+ '--forwarding_port',
+ type=int,
+ help='Server port to forward to.')
+
+ return parser.parse_args()
+
+
+def initialize_thread_local_data():
+ thread_local_data.client_to_server_bytes = 0
+ thread_local_data.server_to_client_bytes = 0
+
+
+def forward(source, destination, is_client_to_server):
+ """Forward traffic from source to destination.
+
+ :param source: socket to read from.
+ :param destination: socket to write to.
+ :param is_client_to_server: True if forwarding from client to server.
+ """
+ # Initialize thread-local data.
+ initialize_thread_local_data()
+
+ while True:
+ try:
+ data = source.recv(4096)
+ if not data:
+ break
+ destination.sendall(data)
+ except Exception as e:
+ # Catching all exceptions.
+ break
+ if is_client_to_server:
+ thread_local_data.client_to_server_bytes += len(data)
+ else:
+ thread_local_data.server_to_client_bytes += len(data)
+ # Forwarding done. Print the number of bytes transferred in the direction.
+ if thread_local_data.client_to_server_bytes > 0:
+ print(f"client-to-server: {thread_local_data.client_to_server_bytes}")
+ elif thread_local_data.server_to_client_bytes > 0:
+ print(f"server-to-client: {thread_local_data.server_to_client_bytes}")
+
+
+def start_bidirectional_forwarding(client_socket, forwarding_port):
+ """Start forwarding traffic between client and server.
+
+ :param client_socket: socket connected to the client.
+ :param forwarding_port: server port to forward to.
+ """
+ CLIENT_TO_SERVER = True
+ SERVER_TO_CLIENT = False
+ with client_socket, socket.socket(socket.AF_INET, socket.SOCK_STREAM) as
server_socket:
+ client_socket.settimeout(TIMEOUT)
+ server_socket.settimeout(TIMEOUT)
+ server_socket.connect((LOCAL_HOST, forwarding_port))
+ # Spawn a thread to forward traffic from client to server.
+ client_to_server = threading.Thread(target=forward,
args=(client_socket, server_socket, CLIENT_TO_SERVER))
+ client_to_server.start()
+
+ # Forward traffic from server to client in the current thread.
+ forward(server_socket, client_socket, SERVER_TO_CLIENT)
+ client_to_server.join()
+
+
+def main() -> int:
+ """Run the proxy."""
+ print(f"Starting proxy...")
+ args = parse_args()
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listen_socket:
+ listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ listen_socket.bind((LOCAL_HOST, args.listening_port))
+ listen_socket.listen()
+ print(f"Proxy listening on {LOCAL_HOST}:{args.listening_port}")
+ try:
+ while True:
+ client_sock, client_addr = listen_socket.accept()
+ print(f"Accepted connection from {client_addr}")
+ # Handle each client connection in a new thread.
+ client_thread =
threading.Thread(target=start_bidirectional_forwarding, args=(client_sock,
args.forwarding_port))
+ client_thread.start()
+ except Exception:
+ # Catching all exceptions.
+ pass
+ except KeyboardInterrupt:
+ print("Caught KeyboardInterrupt, terminating the program")
+ return 0
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/gold_tests/tunnel/tunnel_transform.test.py
b/tests/gold_tests/tunnel/tunnel_transform.test.py
index 5da81a97bb..50e53c9a02 100644
--- a/tests/gold_tests/tunnel/tunnel_transform.test.py
+++ b/tests/gold_tests/tunnel/tunnel_transform.test.py
@@ -18,6 +18,10 @@
import os
import subprocess
+import sys
+from ports import get_port
+import re
+
Test.Summary = '''
Test the reported type of HTTP transactions and tunnels
'''
@@ -62,17 +66,32 @@ ts.Disk.sni_yaml.AddLines([
" tunnel_route: localhost:{0}".format(server.Variables.SSL_Port),
])
+# Set up simple forwarding proxy to keep track of TLS bytes for both
+# directions
+tr = Test.AddTestRun("Run dumb proxy and send tunnel request.")
+tr.Setup.CopyAs('dumb_proxy.py', tr.RunDirectory)
+dumb_proxy = tr.Processes.Process(
+ f'dumb-proxy')
+proxy_port = get_port(dumb_proxy, "listening_port")
+dumb_proxy.Command = f'{sys.executable} dumb_proxy.py --listening_port
{proxy_port} --forwarding_port {ts.Variables.ssl_port}'
+dumb_proxy.StartBefore(Test.Processes.ts)
+dumb_proxy.Ready = When.PortOpenv4(proxy_port)
+dumb_proxy.ReturnCode = 0
+# Record the log file path for later verification.
+proxy_output = dumb_proxy.Streams.stdout.AbsPath
+
# Add connection close to ensure that the client connection closes promptly
after completing the transaction
cmd_tunnel = 'curl -k --http1.1 -H "Connection: close" -vs --resolve
"tunnel-test:{0}:127.0.0.1" https://tunnel-test:{0}/'.format(
- ts.Variables.ssl_port)
+ proxy_port)
# Send the tunnel request
-tr = Test.AddTestRun("send tunnel request")
tr.Processes.Default.Env = ts.Env
tr.Processes.Default.Command = cmd_tunnel
tr.Processes.Default.ReturnCode = 0
+tr.TimeOut = 5
tr.Processes.Default.StartBefore(server,
ready=When.PortOpen(server.Variables.SSL_Port))
tr.Processes.Default.StartBefore(Test.Processes.ts)
+tr.Processes.Default.StartBefore(dumb_proxy)
tr.StillRunningAfter = ts
tr.StillRunningAfter = server
@@ -116,30 +135,32 @@ tr.Processes.Default.Streams.All =
Testers.ContainsExpression(
tr.StillRunningAfter = ts
tr.StillRunningAfter = server
-check_input_range = '''
-val=`traffic_ctl metric get tunnel_transform.ua.bytes_sent | cut -d ' ' -f 2;
test $val -gt 700
-'''
+
+def get_expected_bytes(path_to_proxy_output, key):
+ # Construct the regex pattern.
+ pattern = re.compile(rf'{re.escape(key)}:\s+(\d+)')
+ with open(path_to_proxy_output, 'r') as file:
+ log_content = file.read()
+ bytes_transferred = 0
+ match = pattern.search(log_content)
+ if match:
+ bytes_transferred = int(match.group(1))
+ return bytes_transferred
-def check_range(path, lo, hi):
- f = open(path, 'r')
+def check_byte_count(plugin_metric_path, proxy_output_path, key):
+ expected_bytes = get_expected_bytes(proxy_output_path, key)
+ f = open(plugin_metric_path, 'r')
content = f.read()
values = content.split()
f.close()
if len(values) == 2:
val = int(values[1])
- return val > lo and val < hi, "Check range", "Out of range"
+ return val == expected_bytes, "Check byte count", "Byte count does not
match the expected value"
else:
- return false, "Check range", "Out of range"
+ return False, "Check byte count", "Unexpected metrics output format"
-# Ideally, I'd like to cross check the number of TLS bytes received from UA and
-# received from OS to the curl command. The debug output lists the number of
-# non record data bytes, but it does not enumerate the number of bytes sent in
the records
-# as far as I can tell. The size of the data record will be different from
that plain text
-# size due to padding etc. Leaving the test with the hard coded values for
now. Hopefully,
-# some bright soul can come along later and make this a better test.
-# Perhaps adding a netcat based test case could do that.
tr = Test.AddTestRun("Fetch bytes sent")
tr.Processes.Default.Command = "traffic_ctl metric get
tunnel_transform.ua.bytes_sent"
tr.Processes.Default.ReturnCode = 0
@@ -155,7 +176,9 @@ tr2.Processes.Default.ReturnCode = 0
tr2.Processes.Default.Env = ts.Env
tr2.StillRunningAfter = ts
tr2.StillRunningAfter = server
-tr2.Processes.Default.Streams.stdout = Testers.Lambda(lambda info, tester:
check_range(path1, 700, 800))
+tr2.Processes.Default.Streams.stdout = Testers.Lambda(
+ lambda info, tester: check_byte_count(
+ path1, proxy_output, 'client-to-server'))
path2 = tr2.Processes.Default.Streams.stdout.AbsPath
@@ -165,4 +188,4 @@ tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Env = ts.Env
tr.StillRunningAfter = ts
tr.StillRunningAfter = server
-tr.Processes.Default.Streams.stdout = Testers.Lambda(lambda info, tester:
check_range(path2, 1990, 2100))
+tr.Processes.Default.Streams.stdout = Testers.Lambda(lambda info, tester:
check_byte_count(path2, proxy_output, 'server-to-client'))