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