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'))

Reply via email to