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