You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bn...@apache.org on 2022/10/04 14:30:26 UTC
[trafficserver] branch master updated: Fix HTTP/2 session receive window handling for small sizes (#9117)
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 4fcb9b47d Fix HTTP/2 session receive window handling for small sizes (#9117)
4fcb9b47d is described below
commit 4fcb9b47dbe549901141ebeedd09f389f55be6e1
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Tue Oct 4 09:30:17 2022 -0500
Fix HTTP/2 session receive window handling for small sizes (#9117)
This fixes the way we handle session window sizes if the user configures
proxy.config.http2.initial_window_size_in to be smaller than the HTTP/2
protocol's default value of 65,535. For streams, we can communicate to
the client a smaller stream size via a SETTINGS frame, but not so for
smaller session window sizes. The way HTTP/2 hosts are expected to
reduce their receive session window size is by receiving DATA frames
without replying with WINDOW_UPDATE frames until the window falls to the
desired value. ATS already handled this latter behavior correctly
(letting the window size fall without sending WINDOW_UPDATE frames for
the session until it reached the desired smaller value). However, it
incorrectly reported an error and sent a GOAWAY frame when the user sent
DATA content above the reduced window size before ATS gave the client a
chance to reduce it via DATA frames. This fixes the receive session
window check to allow for a shrinking session window.
Fixes #9115
---
proxy/http2/Http2ConnectionState.cc | 17 +-
proxy/http2/Http2ConnectionState.h | 12 ++
tests/gold_tests/h2/http2_flow_control.replay.yaml | 237 +++++++++++++++++++++
tests/gold_tests/h2/http2_flow_control.test.py | 211 ++++++++++++++++++
4 files changed, 474 insertions(+), 3 deletions(-)
diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc
index 07f325daf..95be51dea 100644
--- a/proxy/http2/Http2ConnectionState.cc
+++ b/proxy/http2/Http2ConnectionState.cc
@@ -156,7 +156,7 @@ Http2ConnectionState::rcv_data_frame(const Http2Frame &frame)
}
// Check whether Window Size is acceptable
- if (this->server_rwnd() < payload_length) {
+ if (!this->_server_rwnd_is_shrinking && this->server_rwnd() < payload_length) {
return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_FLOW_CONTROL_ERROR,
"recv data cstate.server_rwnd < payload_length");
}
@@ -1039,8 +1039,18 @@ Http2ConnectionState::Http2ConnectionState() : stream_list()
void
Http2ConnectionState::init(Http2CommonSession *ssn)
{
- session = ssn;
- this->_server_rwnd = Http2::initial_window_size;
+ session = ssn;
+
+ if (Http2::initial_window_size < HTTP2_INITIAL_WINDOW_SIZE) {
+ // There is no HTTP/2 specified way to shrink the connection window size
+ // other than to receive data and not send WINDOW_UPDATE frames for a
+ // while.
+ this->_server_rwnd = HTTP2_INITIAL_WINDOW_SIZE;
+ this->_server_rwnd_is_shrinking = true;
+ } else {
+ this->_server_rwnd = Http2::initial_window_size;
+ this->_server_rwnd_is_shrinking = false;
+ }
local_hpack_handle = new HpackHandle(HTTP2_HEADER_TABLE_SIZE);
remote_hpack_handle = new HpackHandle(HTTP2_HEADER_TABLE_SIZE);
@@ -1424,6 +1434,7 @@ Http2ConnectionState::restart_receiving(Http2Stream *stream)
if (this->server_rwnd() < min_rwnd) {
Http2WindowSize diff_size = initial_rwnd - this->server_rwnd();
this->increment_server_rwnd(diff_size);
+ this->_server_rwnd_is_shrinking = false;
this->send_window_update_frame(0, diff_size);
}
diff --git a/proxy/http2/Http2ConnectionState.h b/proxy/http2/Http2ConnectionState.h
index 9fc8e7665..1be7c5045 100644
--- a/proxy/http2/Http2ConnectionState.h
+++ b/proxy/http2/Http2ConnectionState.h
@@ -218,6 +218,18 @@ private:
ssize_t _client_rwnd = HTTP2_INITIAL_WINDOW_SIZE;
ssize_t _server_rwnd = 0;
+ /** Whether the session window is in a shrinking state before we send the
+ * first WINDOW_UPDATE frame.
+ *
+ * Unlike HTTP/2 streams, the HTTP/2 session window has no way to initialize
+ * it to a value lower than 65,535. If the initial value is lower than
+ * 65,535, the session window will have to shrink while we receive DATA
+ * frames without incrementing the window via WINDOW_UPDATE frames. Once the
+ * window gets to the desired size, we start maintaining the window via
+ * WINDOW_UPDATE frames.
+ */
+ bool _server_rwnd_is_shrinking = false;
+
std::vector<size_t> _recent_rwnd_increment = {SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX};
int _recent_rwnd_increment_index = 0;
diff --git a/tests/gold_tests/h2/http2_flow_control.replay.yaml b/tests/gold_tests/h2/http2_flow_control.replay.yaml
new file mode 100644
index 000000000..f8eca55d5
--- /dev/null
+++ b/tests/gold_tests/h2/http2_flow_control.replay.yaml
@@ -0,0 +1,237 @@
+# 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.
+
+meta:
+ version: "1.0"
+
+# This replay file generates an HTTP/2 session with three streams in order to
+# verify that ATS generates the expected SETTINGS and WINDOW_UPDATE frames.
+
+sessions:
+
+- protocol:
+ - name: http
+ version: 2
+ - name: tls
+ sni: www.example.com
+ - name: tcp
+ - name: ip
+
+ transactions:
+
+ - client-request:
+ headers:
+ fields:
+ - [ :method, GET ]
+ - [ :scheme, https ]
+ - [ :authority, www.example.com ]
+ - [ :path, /zero-request ]
+ - [ uuid, zero-request ]
+ - [ X-Request, zero-request ]
+ - [ Content-Length, 0 ]
+
+ proxy-request:
+ headers:
+ fields:
+ - [ X-Request, { value: 'zero-request', as: equal } ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ X-Response, zero-response ]
+ - [ Content-Length, 28 ]
+ content:
+ size: 28
+
+ proxy-response:
+ headers:
+ fields:
+ - [ X-Response, { value: 'zero-response', as: equal } ]
+
+ - client-request:
+ delay: 500ms
+
+ headers:
+ fields:
+ - [ :method, POST ]
+ - [ :scheme, https ]
+ - [ :authority, www.example.com ]
+ - [ :path, /first-request ]
+ - [ uuid, first-request ]
+ - [ X-Request, first-request ]
+ content:
+ size: 1200
+
+ proxy-request:
+ headers:
+ fields:
+ - [ X-Request, { value: 'first-request', as: equal } ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ X-Response, first-response ]
+ - [ Content-Length, 28 ]
+ content:
+ size: 28
+
+ proxy-response:
+ headers:
+ fields:
+ - [ X-Response, { value: 'first-response', as: equal } ]
+
+ - client-request:
+ headers:
+ fields:
+ - [ :method, POST ]
+ - [ :scheme, https ]
+ - [ :authority, www.example.com ]
+ - [ :path, /second-request ]
+ - [ uuid, second-request ]
+ - [ X-Request, second-request ]
+ content:
+ size: 1200
+
+ proxy-request:
+ headers:
+ fields:
+ - [ X-Request, {value: 'second-request', as: equal } ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ X-Response, second-response ]
+ - [ Content-Length, 28 ]
+ content:
+ size: 28
+
+ proxy-response:
+ headers:
+ fields:
+ - [ X-Response, {value: 'second-response', as: equal } ]
+
+ - client-request:
+ headers:
+ fields:
+ - [ :method, POST ]
+ - [ :scheme, https ]
+ - [ :authority, www.example.com ]
+ - [ :path, /third-request ]
+ - [ uuid, third-request ]
+ - [ X-Request, third-request ]
+ content:
+ size: 1200
+
+ proxy-request:
+ headers:
+ fields:
+ - [ X-Request, {value: 'third-request', as: equal } ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ X-Response, third-response ]
+ - [ Content-Length, 28 ]
+ content:
+ size: 28
+
+ proxy-response:
+ headers:
+ fields:
+ - [ X-Response, {value: 'third-response', as: equal } ]
+
+ - client-request:
+ # Intentionally test a stream after the three other parallel POST
+ # requests.
+ delay: 500ms
+
+ headers:
+ fields:
+ - [ :method, POST ]
+ - [ :scheme, https ]
+ - [ :authority, www.example.com ]
+ - [ :path, /fourth-request ]
+ - [ uuid, fourth-request ]
+ - [ X-Request, fourth-request ]
+ content:
+ # Send a very large DATA frame so that we exceed the 65,535 window
+ # size of most of the test runs.
+ size: 120000
+
+ proxy-request:
+ headers:
+ fields:
+ - [ X-Request, {value: 'fourth-request', as: equal } ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ X-Response, fourth-response ]
+ - [ Content-Length, 28 ]
+ content:
+ size: 28
+
+ proxy-response:
+ headers:
+ fields:
+ - [ X-Response, {value: 'fourth-response', as: equal } ]
+
+ - client-request:
+ # Give the above request time to process and give us an opportunity to
+ # receive any other WINDOW_UPDATE frames.
+ delay: 500ms
+
+ headers:
+ fields:
+ - [ :method, POST ]
+ - [ :scheme, https ]
+ - [ :authority, www.example.com ]
+ - [ :path, /fifth-request ]
+ - [ uuid, fifth-request ]
+ - [ X-Request, fifth-request ]
+ content:
+ size: 10000
+
+ proxy-request:
+ headers:
+ fields:
+ - [ X-Request, {value: 'fifth-request', as: equal } ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ X-Response, fifth-response ]
+ - [ Content-Length, 28 ]
+ content:
+ size: 28
+
+ proxy-response:
+ headers:
+ fields:
+ - [ X-Response, {value: 'fifth-response', as: equal } ]
+
diff --git a/tests/gold_tests/h2/http2_flow_control.test.py b/tests/gold_tests/h2/http2_flow_control.test.py
new file mode 100644
index 000000000..78b087fe2
--- /dev/null
+++ b/tests/gold_tests/h2/http2_flow_control.test.py
@@ -0,0 +1,211 @@
+"""Verify HTTP/2 flow control behavior."""
+
+# 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 os
+import re
+from typing import List, Optional
+
+
+Test.Summary = __doc__
+
+
+class Http2FlowControlTest:
+ """Define an object to test HTTP/2 flow control behavior."""
+
+ _replay_file: str = 'http2_flow_control.replay.yaml'
+ _valid_policy_values: List[int] = list(range(0, 2))
+
+ _default_initial_window_size: int = 65535
+ _default_max_concurrent_streams_in: int = 100
+
+ _dns_counter: int = 0
+ _server_counter: int = 0
+ _ts_counter: int = 0
+ _client_counter: int = 0
+
+ def __init__(
+ self,
+ description: str,
+ initial_window_size: Optional[int] = None,
+ max_concurrent_streams_in: Optional[int] = None,
+ verify_window_update_frames: bool = False):
+ """Declare the various test Processes.
+
+ :param description: A description of the test.
+
+ :param initial_window_size: The value with which to configure the
+ proxy.config.http2.initial_window_size_in ATS parameter in the
+ records.config file. If the paramenter is None, then no window size
+ will be explicitly set and ATS will use the default value.
+
+ :param max_concurrent_streams_in: The value with which to configure the
+ proxy.config.http2.max_concurrent_streams_in ATS parameter in the
+ records.config file. If the paramenter is None, then no window size
+ will be explicitly set and ATS will use the default value.
+
+ :param verify_window_update_frames: If True, then the test will verify
+ that it sees HTTP/2 WINDOW_UPDATE frames as data is sent.
+ """
+ self._description = description
+
+ self._initial_window_size = initial_window_size
+ self._expected_initial_window_size = (
+ initial_window_size if initial_window_size is not None
+ else self._default_initial_window_size)
+
+ self._max_concurrent_streams_in = max_concurrent_streams_in
+ self._expected_max_concurrent_streams_in = (
+ max_concurrent_streams_in if max_concurrent_streams_in is not None
+ else self._default_max_concurrent_streams_in)
+
+ self._verify_window_update_frames = verify_window_update_frames
+
+ self._dns = self._configure_dns()
+ self._server = self._configure_server()
+ self._ts = self._configure_trafficserver()
+
+ def _configure_dns(self):
+ """Configure the DNS."""
+ dns = Test.MakeDNServer(f'dns-{Http2FlowControlTest._dns_counter}')
+ Http2FlowControlTest._dns_counter += 1
+ return dns
+
+ def _configure_server(self):
+ """Configure the test server."""
+ server = Test.MakeVerifierServerProcess(
+ f'server-{Http2FlowControlTest._server_counter}',
+ self._replay_file)
+ Http2FlowControlTest._server_counter += 1
+ return server
+
+ def _configure_trafficserver(self):
+ """Configure a Traffic Server process."""
+ ts = Test.MakeATSProcess(
+ f'ts-{Http2FlowControlTest._ts_counter}',
+ enable_tls=True,
+ enable_cache=False)
+ Http2FlowControlTest._ts_counter += 1
+
+ ts.addDefaultSSLFiles()
+ ts.Disk.records_config.update({
+ 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE',
+ 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(self._dns.Variables.Port),
+ 'proxy.config.ssl.keylog_file': os.path.join(Test.RunDirectory, 'tls_session_keys.txt'),
+
+ 'proxy.config.diags.debug.enabled': 3,
+ 'proxy.config.diags.debug.tags': 'http',
+ })
+
+ if self._initial_window_size is not None:
+ ts.Disk.records_config.update({
+ 'proxy.config.http2.initial_window_size_in': self._initial_window_size,
+ })
+
+ if self._max_concurrent_streams_in is not None:
+ ts.Disk.records_config.update({
+ 'proxy.config.http2.max_concurrent_streams_in': self._max_concurrent_streams_in,
+ })
+
+ ts.Disk.ssl_multicert_config.AddLine(
+ 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+ )
+
+ ts.Disk.remap_config.AddLine(
+ f'map / http://127.0.0.1:{self._server.Variables.http_port}'
+ )
+
+ return ts
+
+ def _configure_client(self, tr):
+ """Configure a client process.
+
+ :param tr: The TestRun to associate the client with.
+ """
+ tr.AddVerifierClientProcess(
+ f'client-{Http2FlowControlTest._client_counter}',
+ self._replay_file,
+ https_ports=[self._ts.Variables.ssl_port])
+ Http2FlowControlTest._client_counter += 1
+
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ f'MAX_CONCURRENT_STREAMS:{self._expected_max_concurrent_streams_in}',
+ "Client should receive a MAX_CONCURRENT_STREAMS setting.")
+
+ if self._initial_window_size is not None:
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ f'INITIAL_WINDOW_SIZE:{self._expected_initial_window_size}',
+ "Client should receive an INITIAL_WINDOW_SIZE setting.")
+
+ if self._verify_window_update_frames:
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ f'WINDOW_UPDATE.*id 0: {self._expected_initial_window_size}',
+ "Client should receive a session WINDOW_UPDATE.")
+
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ f'WINDOW_UPDATE.*id 3: {self._expected_initial_window_size}',
+ "Client should receive a stream WINDOW_UPDATE.")
+
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ f'WINDOW_UPDATE.*id 5: {self._expected_initial_window_size}',
+ "Client should receive a stream WINDOW_UPDATE.")
+
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ f'WINDOW_UPDATE.*id 7: {self._expected_initial_window_size}',
+ "Client should receive a stream WINDOW_UPDATE.")
+
+ def run(self):
+ """Configure the TestRun."""
+ tr = Test.AddTestRun(self._description)
+ self._configure_client(tr)
+ tr.Processes.Default.StartBefore(self._dns)
+ tr.Processes.Default.StartBefore(self._server)
+ tr.StillRunningAfter = self._ts
+ tr.Processes.Default.StartBefore(self._ts)
+
+
+#
+# Default configuration.
+#
+test = Http2FlowControlTest("Default Configurations")
+test.run()
+
+#
+# Configuring max_concurrent_streams_in.
+#
+test = Http2FlowControlTest(
+ description="Configure max_concurrent_streams_in",
+ max_concurrent_streams_in=53)
+test.run()
+
+#
+# Configuring initial_window_size.
+#
+test = Http2FlowControlTest(
+ description="Configure a large initial_window_size_in",
+ initial_window_size=100123)
+test.run()
+
+
+test = Http2FlowControlTest(
+ description="Configure a small initial_window_size_in",
+ max_concurrent_streams_in=10,
+ initial_window_size=10,
+ verify_window_update_frames=True)
+test.run()