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 89fdedb504 Raise HTTP/2 SETTINGS limits (#13444)
89fdedb504 is described below

commit 89fdedb504e13c7e2a9e8eb59d6e76627ed2c68c
Author: Brian Neradt <[email protected]>
AuthorDate: Fri Jul 31 10:55:25 2026 -0500

    Raise HTTP/2 SETTINGS limits (#13444)
    
    HTTP/2 extensions can add enough SETTINGS parameters to exceed the
    defaults, causing Traffic Server to close otherwise valid connections
    before a request. This occurs with Meta forward proxy traffic that
    advertises WebTransport-over-HTTP/2 settings.
    
    This raises the per-frame limit to 16 and the per-minute limit to
    32, retaining abuse protections while allowing protocol growth. This
    also adds an AuTest that sends an extension-rich SETTINGS frame and
    verifies that the subsequent request succeeds.
    
    Fixes: #13443
---
 doc/admin-guide/files/records.yaml.en.rst          |   4 +-
 src/proxy/http2/HTTP2.cc                           |   4 +-
 src/records/RecordsConfig.cc                       |   4 +-
 .../gold_tests/h2/clients/h2_extension_settings.py | 156 +++++++++++++++++++++
 tests/gold_tests/h2/http2.test.py                  |   9 ++
 tests/gold_tests/records/gold/full_records.yaml    |   4 +-
 .../records/legacy_config/full_records.config      |   4 +-
 7 files changed, 175 insertions(+), 10 deletions(-)

diff --git a/doc/admin-guide/files/records.yaml.en.rst 
b/doc/admin-guide/files/records.yaml.en.rst
index 47d28e4a46..043ed594b6 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -5089,7 +5089,7 @@ HTTP/2 Configuration
 
    This is the threshold of sampling stream number to start checking the 
stream error rate.
 
-.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_frame INT 7
+.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_frame INT 16
    :reloadable:
 
    Specifies how many settings in an HTTP/2 SETTINGS frame |TS| accepts.
@@ -5097,7 +5097,7 @@ HTTP/2 Configuration
    code of ENHANCE_YOUR_CALM.
    Any negative value configures no limit to the number of settings received.
 
-.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_minute INT 14
+.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_minute INT 32
    :reloadable:
 
    Specifies how many settings in HTTP/2 SETTINGS frames |TS| accept for a 
minute.
diff --git a/src/proxy/http2/HTTP2.cc b/src/proxy/http2/HTTP2.cc
index e8ee95662c..e0e0d62d09 100644
--- a/src/proxy/http2/HTTP2.cc
+++ b/src/proxy/http2/HTTP2.cc
@@ -488,8 +488,8 @@ uint32_t               Http2::no_activity_timeout_out    = 
120;
 
 float    Http2::stream_error_rate_threshold        = 0.1;
 uint32_t Http2::stream_error_sampling_threshold    = 10;
-int32_t  Http2::max_settings_per_frame             = 7;
-int32_t  Http2::max_settings_per_minute            = 14;
+int32_t  Http2::max_settings_per_frame             = 16;
+int32_t  Http2::max_settings_per_minute            = 32;
 int32_t  Http2::max_settings_frames_per_minute     = 14;
 int32_t  Http2::max_ping_frames_per_minute         = 60;
 int32_t  Http2::max_priority_frames_per_minute     = 120;
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index 003e494380..6cca71851d 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -1372,9 +1372,9 @@ static constexpr RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.http2.stream_error_sampling_threshold", 
RECD_INT, "10", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
   ,
-  {RECT_CONFIG, "proxy.config.http2.max_settings_per_frame", RECD_INT, "7", 
RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
+  {RECT_CONFIG, "proxy.config.http2.max_settings_per_frame", RECD_INT, "16", 
RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
   ,
-  {RECT_CONFIG, "proxy.config.http2.max_settings_per_minute", RECD_INT, "14", 
RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
+  {RECT_CONFIG, "proxy.config.http2.max_settings_per_minute", RECD_INT, "32", 
RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.http2.max_settings_frames_per_minute", RECD_INT, 
"14", RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
   ,
diff --git a/tests/gold_tests/h2/clients/h2_extension_settings.py 
b/tests/gold_tests/h2/clients/h2_extension_settings.py
new file mode 100644
index 0000000000..af8ea58c67
--- /dev/null
+++ b/tests/gold_tests/h2/clients/h2_extension_settings.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+
+#  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.
+"""Send an HTTP/2 request after an extension-rich SETTINGS frame."""
+
+import argparse
+import socket
+import ssl
+import sys
+from typing import Tuple
+
+import hpack
+
+H2_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
+
+TYPE_DATA = 0x00
+TYPE_HEADERS = 0x01
+TYPE_RST_STREAM = 0x03
+TYPE_SETTINGS = 0x04
+TYPE_GOAWAY = 0x07
+
+FLAG_ACK = 0x01
+FLAG_END_STREAM = 0x01
+FLAG_END_HEADERS = 0x04
+
+
+def make_frame(frame_type: int, flags: int = 0, stream_id: int = 0, payload: 
bytes = b"") -> bytes:
+    return len(payload).to_bytes(3, "big") + bytes([frame_type, flags]) + 
(stream_id & 0x7FFFFFFF).to_bytes(4, "big") + payload
+
+
+def make_setting(setting_id: int, value: int) -> bytes:
+    return setting_id.to_bytes(2, "big") + value.to_bytes(4, "big")
+
+
+def make_socket(port: int) -> ssl.SSLSocket:
+    socket.setdefaulttimeout(5)
+
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    ctx.set_alpn_protocols(["h2"])
+
+    raw_socket = socket.create_connection(("127.0.0.1", port))
+    tls_socket = ctx.wrap_socket(raw_socket, server_hostname="localhost")
+    if tls_socket.selected_alpn_protocol() != "h2":
+        raise RuntimeError(f"failed to negotiate h2, got 
{tls_socket.selected_alpn_protocol()!r}")
+    return tls_socket
+
+
+def recv_exact(sock: ssl.SSLSocket, size: int) -> bytes:
+    data = bytearray()
+    while len(data) < size:
+        chunk = sock.recv(size - len(data))
+        if not chunk:
+            raise EOFError("socket closed")
+        data.extend(chunk)
+    return bytes(data)
+
+
+def read_frame(sock: ssl.SSLSocket) -> Tuple[int, int, int, bytes]:
+    header = recv_exact(sock, 9)
+    length = int.from_bytes(header[0:3], "big")
+    frame_type = header[3]
+    flags = header[4]
+    stream_id = int.from_bytes(header[5:9], "big") & 0x7FFFFFFF
+    return frame_type, flags, stream_id, recv_exact(sock, length)
+
+
+def make_settings_payload() -> bytes:
+    settings = [
+        (0x01, 4096),
+        (0x02, 0),
+        (0x05, 16384),
+        (0x06, 131072),
+        (0x03, 100),
+        (0x04, 65535),
+        (0x2B61, 65535),
+        (0x2B62, 65535),
+        (0x2B63, 65535),
+        (0x2B64, 10),
+        (0x2B65, 10),
+    ]
+    return b"".join(make_setting(setting_id, value) for setting_id, value in 
settings)
+
+
+def run(port: int) -> int:
+    encoder = hpack.Encoder()
+    decoder = hpack.Decoder()
+    request_headers = encoder.encode(
+        [
+            (":method", "GET"),
+            (":scheme", "https"),
+            (":authority", "www.example.com"),
+            (":path", "/"),
+        ])
+
+    with make_socket(port) as sock:
+        sock.sendall(H2_PREFACE)
+        sock.sendall(make_frame(TYPE_SETTINGS, 
payload=make_settings_payload()))
+        sock.sendall(make_frame(TYPE_HEADERS, FLAG_END_HEADERS | 
FLAG_END_STREAM, 1, request_headers))
+
+        response_status = None
+        try:
+            while True:
+                frame_type, flags, stream_id, payload = read_frame(sock)
+                if frame_type == TYPE_SETTINGS and not (flags & FLAG_ACK):
+                    sock.sendall(make_frame(TYPE_SETTINGS, FLAG_ACK))
+                    continue
+                if frame_type == TYPE_GOAWAY:
+                    error_code = int.from_bytes(payload[4:8], "big")
+                    print(f"Received GOAWAY with error code {error_code}", 
file=sys.stderr)
+                    return 1
+                if frame_type == TYPE_RST_STREAM and stream_id == 1:
+                    error_code = int.from_bytes(payload[0:4], "big")
+                    print(f"Received RST_STREAM with error code {error_code}", 
file=sys.stderr)
+                    return 1
+                if frame_type == TYPE_HEADERS and stream_id == 1:
+                    if not (flags & FLAG_END_HEADERS):
+                        print("Received an unexpected CONTINUATION sequence", 
file=sys.stderr)
+                        return 1
+                    response_headers = decoder.decode(payload)
+                    response_status = dict(response_headers).get(":status")
+                if stream_id == 1 and frame_type in (TYPE_HEADERS, TYPE_DATA) 
and (flags & FLAG_END_STREAM):
+                    if response_status == "200":
+                        print("Received 200 response")
+                        return 0
+                    print(f"Received response status {response_status!r}", 
file=sys.stderr)
+                    return 1
+        except (EOFError, socket.timeout) as exc:
+            print(f"Connection ended before the response completed: {exc}", 
file=sys.stderr)
+            return 1
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("port", type=int, help="ATS TLS port")
+    args = parser.parse_args()
+    return run(args.port)
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/tests/gold_tests/h2/http2.test.py 
b/tests/gold_tests/h2/http2.test.py
index 7907344cbd..65cfbe8107 100644
--- a/tests/gold_tests/h2/http2.test.py
+++ b/tests/gold_tests/h2/http2.test.py
@@ -158,6 +158,7 @@ ts.Disk.records_config.update(
 
 ts.Setup.CopyAs('h2client.py', Test.RunDirectory)
 ts.Setup.CopyAs('h2active_timeout.py', Test.RunDirectory)
+ts.Setup.CopyAs('clients/h2_extension_settings.py', Test.RunDirectory)
 
 settings_limit_ts = Test.MakeATSProcess("ts_settings_limit", enable_tls=True, 
enable_cache=False)
 settings_limit_ts.addDefaultSSLFiles()
@@ -275,3 +276,11 @@ tr.Processes.Default.ReturnCode = 0
 tr.Processes.Default.StartBefore(settings_limit_ts)
 tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
     "Received GOAWAY with error code 11", "Received ENHANCE_YOUR_CALM GOAWAY.")
+
+# Test Case 11: Extension settings fit within the default SETTINGS limits.
+tr = Test.AddTestRun("HTTP/2 extension settings")
+tr.Processes.Default.Command = f'{sys.executable} h2_extension_settings.py 
{ts.Variables.ssl_port}'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(
+    "Received 200 response", "The request following the extension settings 
should succeed.")
+tr.StillRunningAfter = server
diff --git a/tests/gold_tests/records/gold/full_records.yaml 
b/tests/gold_tests/records/gold/full_records.yaml
index fc02158c5b..1123e2cbb1 100644
--- a/tests/gold_tests/records/gold/full_records.yaml
+++ b/tests/gold_tests/records/gold/full_records.yaml
@@ -303,8 +303,8 @@ records:
     max_ping_frames_per_minute: 60
     max_priority_frames_per_minute: 120
     max_settings_frames_per_minute: 14
-    max_settings_per_frame: 7
-    max_settings_per_minute: 14
+    max_settings_per_frame: 16
+    max_settings_per_minute: 32
     min_avg_window_update: 2560.0
     min_concurrent_streams_in: 10
     no_activity_timeout_in: 120
diff --git a/tests/gold_tests/records/legacy_config/full_records.config 
b/tests/gold_tests/records/legacy_config/full_records.config
index 8b1d13c822..d7b640dae3 100644
--- a/tests/gold_tests/records/legacy_config/full_records.config
+++ b/tests/gold_tests/records/legacy_config/full_records.config
@@ -413,8 +413,8 @@ CONFIG proxy.config.http2.push_diary_size INT 256
 CONFIG proxy.config.http2.zombie_debug_timeout_in INT 0
 CONFIG proxy.config.http2.stream_error_rate_threshold FLOAT 0.1
 CONFIG proxy.config.http2.stream_error_sampling_threshold INT 10
-CONFIG proxy.config.http2.max_settings_per_frame INT 7
-CONFIG proxy.config.http2.max_settings_per_minute INT 14
+CONFIG proxy.config.http2.max_settings_per_frame INT 16
+CONFIG proxy.config.http2.max_settings_per_minute INT 32
 CONFIG proxy.config.http2.max_settings_frames_per_minute INT 14
 CONFIG proxy.config.http2.max_ping_frames_per_minute INT 60
 CONFIG proxy.config.http2.max_priority_frames_per_minute INT 120

Reply via email to