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 c5430e6a85 Support gRPC traffic (#9987)
c5430e6a85 is described below
commit c5430e6a85e14db59d4d7ccc579036b3f0792c44
Author: Brian Neradt <[email protected]>
AuthorDate: Wed Jan 24 10:15:02 2024 -0600
Support gRPC traffic (#9987)
Now that we support HTTP/2 to origin, we can now support gRPC traffic end
to end. This patch includes the following:
* A new gRPC autest.
* Updates from @keesspoelstra to support tunneling HTTP/2 trailers.
* An update to preserve the "te: trailers" header which the server requires
in order to transmit gRPC traffic.
* A rename of Http2Stream::send_request to send_headers.
---
include/proxy/http/HttpSM.h | 1 +
include/proxy/http2/Http2Stream.h | 10 ++-
include/tscore/ink_string++.h | 1 +
src/proxy/hdrs/HdrToken.cc | 3 -
src/proxy/http/HttpSM.cc | 26 +++++-
src/proxy/http/HttpTransactHeaders.cc | 18 +++-
src/proxy/http2/Http2ConnectionState.cc | 17 ++--
src/proxy/http2/Http2Stream.cc | 81 ++++++++++++------
tests/Pipfile | 4 +
tests/gold_tests/h2/grpc/grpc.test.py | 143 ++++++++++++++++++++++++++++++++
tests/gold_tests/h2/grpc/grpc_client.py | 74 +++++++++++++++++
tests/gold_tests/h2/grpc/grpc_server.py | 81 ++++++++++++++++++
tests/gold_tests/h2/grpc/simple.proto | 38 +++++++++
13 files changed, 451 insertions(+), 46 deletions(-)
diff --git a/include/proxy/http/HttpSM.h b/include/proxy/http/HttpSM.h
index ae14d4d11d..6f28523d0b 100644
--- a/include/proxy/http/HttpSM.h
+++ b/include/proxy/http/HttpSM.h
@@ -446,6 +446,7 @@ private:
void perform_cache_write_action();
void perform_transform_cache_write_action();
void setup_blind_tunnel(bool send_response_hdr, IOBufferReader *initial =
nullptr);
+ void setup_tunnel_handler_trailer(HttpTunnelProducer *p);
HttpTunnelProducer *setup_server_transfer_to_transform();
HttpTunnelProducer *setup_transfer_from_transform();
HttpTunnelProducer *setup_cache_transfer_to_transform();
diff --git a/include/proxy/http2/Http2Stream.h
b/include/proxy/http2/Http2Stream.h
index ffb811cd71..72515f1fb6 100644
--- a/include/proxy/http2/Http2Stream.h
+++ b/include/proxy/http2/Http2Stream.h
@@ -82,7 +82,7 @@ public:
void set_expect_receive_trailer() override;
Http2ErrorCode decode_header_blocks(HpackHandle &hpack_handle, uint32_t
maximum_table_size);
- void send_request(Http2ConnectionState &cstate);
+ void send_headers(Http2ConnectionState &cstate);
void initiating_close();
bool is_outbound_connection() const;
bool is_tunneling() const;
@@ -217,6 +217,12 @@ private:
History<HISTORY_DEFAULT_SIZE> _history;
Milestones<Http2StreamMilestone,
static_cast<size_t>(Http2StreamMilestone::LAST_ENTRY)> _milestones;
+ /** Any headers received while this is true are trailing headers.
+ *
+ * This is set to true when processing DATA frames are done. Therefore any
+ * headers seen after that point are trailing headers. The qualification
+ * "possible" is added because the peer may or may not send trailing headers.
+ */
bool _trailing_header_is_possible = false;
bool _expect_send_trailer = false;
bool _expect_receive_trailer = false;
@@ -373,7 +379,7 @@ Http2Stream::reset_send_headers()
this->_send_header.create(HTTP_TYPE_RESPONSE);
}
-// Check entire DATA payload length if content-length: header is exist
+// Check entire DATA payload length if content-length: header exists
inline void
Http2Stream::increment_data_length(uint64_t length)
{
diff --git a/include/tscore/ink_string++.h b/include/tscore/ink_string++.h
index 9d737edcc1..c8e9a1ada9 100644
--- a/include/tscore/ink_string++.h
+++ b/include/tscore/ink_string++.h
@@ -32,6 +32,7 @@
#pragma once
#include <cstdio>
+#include <cstring>
#include <strings.h>
/***********************************************************************
diff --git a/src/proxy/hdrs/HdrToken.cc b/src/proxy/hdrs/HdrToken.cc
index 2b7a30b7fb..104350feab 100644
--- a/src/proxy/hdrs/HdrToken.cc
+++ b/src/proxy/hdrs/HdrToken.cc
@@ -230,9 +230,6 @@ static HdrTokenFieldInfo
_hdrtoken_strs_field_initializers[] = {
{"Strict-Transport-Security", MIME_SLOTID_NONE,
MIME_PRESENCE_NONE, (HTIF_MULTVALS)
},
{"Subject", MIME_SLOTID_NONE,
MIME_PRESENCE_SUBJECT, HTIF_NONE
},
{"Summary", MIME_SLOTID_NONE,
MIME_PRESENCE_SUMMARY, HTIF_NONE
},
- // TODO: In the past we have observed issues with having hop-by-hop in here
- // for gRPC. We plan to work on gRPC in a future. We should experiment with
- // this and verify that it works as expected.
{"TE", MIME_SLOTID_TE,
MIME_PRESENCE_TE, (HTIF_COMMAS | HTIF_MULTVALS |
HTIF_HOPBYHOP)},
{"Transfer-Encoding", MIME_SLOTID_TRANSFER_ENCODING,
MIME_PRESENCE_TRANSFER_ENCODING,
(HTIF_COMMAS | HTIF_MULTVALS | HTIF_HOPBYHOP)
},
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 21d446c4f1..b3b2aa4ca1 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -2780,6 +2780,23 @@ HttpSM::tunnel_handler_post(int event, void *data)
return 0;
}
+void
+HttpSM::setup_tunnel_handler_trailer(HttpTunnelProducer *p)
+{
+ p->read_success = true;
+ t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE;
+ t_state.current.server->abort = HttpTransact::DIDNOT_ABORT;
+
+ SMDbg(dbg_ctl_http, "Wait for the trailing header");
+
+ // Swap out the default hander to set up the new tunnel for the trailer
exchange.
+ HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_trailer);
+ if (_ua.get_txn()) {
+ _ua.get_txn()->set_expect_send_trailer();
+ }
+ tunnel.local_finish_all(p);
+}
+
int
HttpSM::tunnel_handler_trailer(int event, void *data)
{
@@ -3085,6 +3102,10 @@ HttpSM::tunnel_handler_server(int event,
HttpTunnelProducer *p)
tunnel.append_message_to_producer_buffer(p, reason, reason_len);
}
*/
+ if (server_txn->expect_receive_trailer()) {
+ setup_tunnel_handler_trailer(p);
+ return 0;
+ }
tunnel.local_finish_all(p);
}
break;
@@ -3111,10 +3132,7 @@ HttpSM::tunnel_handler_server(int event,
HttpTunnelProducer *p)
}
}
if (server_txn->expect_receive_trailer()) {
- SMDbg(dbg_ctl_http, "wait for that trailing header");
- // Swap out the default hander to set up the new tunnel for the trailer
exchange.
- HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_trailer);
- tunnel.local_finish_all(p);
+ setup_tunnel_handler_trailer(p);
return 0;
}
break;
diff --git a/src/proxy/http/HttpTransactHeaders.cc
b/src/proxy/http/HttpTransactHeaders.cc
index 3384c62863..9080263001 100644
--- a/src/proxy/http/HttpTransactHeaders.cc
+++ b/src/proxy/http/HttpTransactHeaders.cc
@@ -33,6 +33,7 @@
#include "proxy/hdrs/HTTP.h"
#include "proxy/hdrs/HdrUtils.h"
#include "proxy/hdrs/HttpCompat.h"
+#include "proxy/hdrs/MIME.h"
#include "proxy/http/HttpSM.h"
#include "proxy/PoolableSession.h"
@@ -222,7 +223,9 @@ HttpTransactHeaders::copy_header_fields(HTTPHdr *src_hdr,
HTTPHdr *new_hdr, bool
// my opinion error prone and if the client doesn't follow the spec
// we'll have problems with the TE being forwarded to the server
// and us caching the transfer encoded documents and then
- // serving it to a client that can not handle it
+ // serving it to a client that can not handle it. The exception
+ // to this is that we will allow "TE: trailers" to be forwarded
+ // because that is required for gRPC traffic.
// 2) Transfer encoding is copied. If the transfer encoding
// is changed for example by dechunking, the transfer encoding
// should be modified when the decision is made to dechunk it
@@ -235,10 +238,19 @@ HttpTransactHeaders::copy_header_fields(HTTPHdr *src_hdr,
HTTPHdr *new_hdr, bool
int field_flags = hdrtoken_index_to_flags(field.m_wks_idx);
if (field_flags & HTIF_HOPBYHOP) {
+ std::string_view name(field.name_get());
+ std::string_view value(field.value_get());
+ bool const is_te_trailers = name == MIME_FIELD_TE && value == "trailers";
+ if (is_te_trailers) {
+ // te: trailers is used by gRPC, do not delete it.
+ continue;
+ }
+
// Delete header if not in special proxy_auth retention mode
- if ((!retain_proxy_auth_hdrs) || (!(field_flags & HTIF_PROXYAUTH))) {
- new_hdr->field_delete(&field);
+ if (retain_proxy_auth_hdrs && (field_flags & HTIF_PROXYAUTH)) {
+ continue;
}
+ new_hdr->field_delete(&field);
} else if (field.m_wks_idx == MIME_WKSIDX_DATE) {
date_hdr = true;
}
diff --git a/src/proxy/http2/Http2ConnectionState.cc
b/src/proxy/http2/Http2ConnectionState.cc
index 9f716cdc70..0dd8e53d94 100644
--- a/src/proxy/http2/Http2ConnectionState.cc
+++ b/src/proxy/http2/Http2ConnectionState.cc
@@ -475,16 +475,16 @@ Http2ConnectionState::rcv_headers_frame(const Http2Frame
&frame)
stream->mark_milestone(Http2StreamMilestone::START_TXN);
stream->new_transaction(frame.is_from_early_data());
// Send request header to SM
- stream->send_request(*this);
+ stream->send_headers(*this);
} else {
// If this is a trailer, first signal to the SM that the body is done
if (stream->trailing_header_is_possible()) {
stream->set_expect_receive_trailer();
// Propagate the trailer header
- stream->send_request(*this);
+ stream->send_headers(*this);
} else {
// Propagate the response
- stream->send_request(*this);
+ stream->send_headers(*this);
}
}
// Give a chance to send response before reading next frame.
@@ -1061,7 +1061,7 @@ Http2ConnectionState::rcv_continuation_frame(const
Http2Frame &frame)
// "from_early_data" flag from the associated HEADERS frame.
stream->new_transaction(frame.is_from_early_data());
// Send request header to SM
- stream->send_request(*this);
+ stream->send_headers(*this);
// Give a chance to send response before reading next frame.
this->session->interrupt_reading_frames();
} else {
@@ -2132,15 +2132,18 @@ Http2ConnectionState::send_a_data_frame(Http2Stream
*stream, size_t &payload_len
stream->update_sent_count(payload_length);
// Are we at the end?
+ // We have no payload to send but might expect data from either trailer or
body
+ // TODO(KS): does the expect send trailer and empty payload need a flush, or
does it
+ // warrant a separate flow with NO_ERROR?
// If we return here, we never send the END_STREAM in the case of a early
terminating OS.
// OK if there is no body yet. Otherwise continue on to send a DATA frame
and delete the stream
- if (!stream->is_write_vio_done() && payload_length == 0) {
+ if ((!stream->is_write_vio_done() || stream->expect_send_trailer()) &&
payload_length == 0) {
Http2StreamDebug(this->session, stream->get_id(), "No payload");
this->session->flush();
return Http2SendDataFrameResult::NO_PAYLOAD;
}
- if (stream->is_write_vio_done()) {
+ if (stream->is_write_vio_done() &&
!resp_reader->is_read_avail_more_than(payload_length) &&
!stream->expect_send_trailer()) {
Http2StreamDebug(this->session, stream->get_id(), "End of Data Frame");
flags |= HTTP2_FLAGS_DATA_END_STREAM;
}
@@ -2430,7 +2433,7 @@ Http2ConnectionState::send_push_promise_frame(Http2Stream
*stream, URL &url, con
stream->set_receive_headers(hdr);
stream->new_transaction();
stream->receive_end_stream = true; // No more data with the request
- stream->send_request(*this);
+ stream->send_headers(*this);
return true;
}
diff --git a/src/proxy/http2/Http2Stream.cc b/src/proxy/http2/Http2Stream.cc
index fac543b6bf..d958151fc4 100644
--- a/src/proxy/http2/Http2Stream.cc
+++ b/src/proxy/http2/Http2Stream.cc
@@ -29,6 +29,7 @@
#include "proxy/http/HttpDebugNames.h"
#include "proxy/http/HttpSM.h"
#include "tscore/HTTPVersion.h"
+#include "tscore/ink_assert.h"
#include <numeric>
@@ -259,45 +260,47 @@ Http2Stream::decode_header_blocks(HpackHandle
&hpack_handle, uint32_t maximum_ta
}
void
-Http2Stream::send_request(Http2ConnectionState &cstate)
+Http2Stream::send_headers(Http2ConnectionState &cstate)
{
if (closed) {
return;
}
REMEMBER(NO_EVENT, this->reentrancy_count);
- // Convert header to HTTP/1.1 format
- if (http2_convert_header_from_2_to_1_1(&_receive_header) ==
PARSE_RESULT_ERROR) {
- Http2StreamDebug("Error converting HTTP/2 headers to HTTP/1.1.");
- if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
- // There's no way to cause Bad Request directly at this time.
- // Set an invalid method so it causes an error later.
- _receive_header.method_set("\xffVOID", 1);
+ // Convert header to HTTP/1.1 format. Trailing headers need no conversion
+ // because they, by definition, do not contain pseudo headers.
+ if (this->trailing_header_is_possible()) {
+ Http2StreamDebug("trailing header: Skipping send_headers initialization.");
+ } else {
+ if (http2_convert_header_from_2_to_1_1(&_receive_header) ==
PARSE_RESULT_ERROR) {
+ Http2StreamDebug("Error converting HTTP/2 headers to HTTP/1.1.");
+ if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
+ // There's no way to cause Bad Request directly at this time.
+ // Set an invalid method so it causes an error later.
+ _receive_header.method_set("\xffVOID", 1);
+ }
}
- }
- if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
- // Check whether the request uses CONNECT method
- int method_len;
- const char *method = _receive_header.method_get(&method_len);
- if (method_len == HTTP_LEN_CONNECT && strncmp(method, HTTP_METHOD_CONNECT,
HTTP_LEN_CONNECT) == 0) {
- this->_is_tunneling = true;
+ if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
+ // Check whether the request uses CONNECT method
+ int method_len;
+ const char *method = _receive_header.method_get(&method_len);
+ if (method_len == HTTP_LEN_CONNECT && strncmp(method,
HTTP_METHOD_CONNECT, HTTP_LEN_CONNECT) == 0) {
+ this->_is_tunneling = true;
+ }
}
+ ink_release_assert(this->_sm != nullptr);
+ this->_http_sm_id = this->_sm->sm_id;
}
- if (this->expect_send_trailer()) {
- // Send read complete to terminate previous data tunnel
- this->read_vio.nbytes = this->read_vio.ndone;
- this->signal_read_event(VC_EVENT_READ_COMPLETE);
- }
-
- ink_release_assert(this->_sm != nullptr);
- this->_http_sm_id = this->_sm->sm_id;
-
// Write header to a buffer. Borrowing logic from
HttpSM::write_header_into_buffer.
// Seems like a function like this ought to be in HTTPHdr directly
int bufindex;
int dumpoffset = 0;
+ // The name dumpoffset is used here for parity with
+ // HttpSM::write_header_into_buffer, but create an alias for clarity in the
+ // use of this variable below this loop.
+ int &num_header_bytes = dumpoffset;
int done, tmp;
do {
bufindex = 0;
@@ -315,7 +318,7 @@ Http2Stream::send_request(Http2ConnectionState &cstate)
}
} while (!done);
- if (bufindex == 0) {
+ if (num_header_bytes == 0) {
// No data to signal read event
return;
}
@@ -323,11 +326,35 @@ Http2Stream::send_request(Http2ConnectionState &cstate)
// Is the _sm ready to process the header?
if (this->read_vio.nbytes > 0) {
if (this->receive_end_stream) {
- this->read_vio.nbytes = bufindex;
- this->read_vio.ndone = bufindex;
+ // These headers may be standard or trailer headers:
+ //
+ // * If they are standard, then there is no body (note again that the
+ // END_STREAM flag was sent with them), data_length will be 0, and
+ // num_header_bytes will simply be the length of the headers.
+ //
+ // * If they are trailers, then the tunnel behind the SM was set up after
+ // the original headers were sent, and thus nbytes should not include the
+ // size of the original standard headers. Rather, for trailers, nbytes
+ // only needs to include the body length (i.e., DATA frame payload
+ // length), and the length of these current trailer headers calculated in
+ // num_header_bytes.
+ this->read_vio.nbytes = this->data_length + num_header_bytes;
+ Http2StreamDebug("nbytes: %" PRId64 ", ndone: %" PRId64 ",
num_header_bytes: %d, data_length: %" PRId64,
+ this->read_vio.nbytes, this->read_vio.ndone,
num_header_bytes, this->data_length);
if (this->is_outbound_connection()) {
+ // This is a response header.
+ // We don't set ndone because the VC_EVENT_EOS will
+ // first flush the remaining content to consumers,
+ // after which the TUNNEL_EVENT_DONE will be fired
+ // and the header handler will be set up.
+ // The header handler will read the buffer, and not
+ // get its content from the VIO
+ // This can break if the implementation
+ // changes.
this->signal_read_event(VC_EVENT_EOS);
} else {
+ // Request headers.
+ this->read_vio.ndone = this->read_vio.nbytes;
this->signal_read_event(VC_EVENT_READ_COMPLETE);
}
} else {
diff --git a/tests/Pipfile b/tests/Pipfile
index 51f69e8ff2..ebdd37f189 100644
--- a/tests/Pipfile
+++ b/tests/Pipfile
@@ -49,5 +49,9 @@ jsonschema = "*"
python-jose = "*"
pyyaml ="*"
+# For the grpc tests.
+grpcio = "*"
+grpcio-tools = "*"
+
[requires]
python_version = "3"
diff --git a/tests/gold_tests/h2/grpc/grpc.test.py
b/tests/gold_tests/h2/grpc/grpc.test.py
new file mode 100644
index 0000000000..b7fc962aa3
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/grpc.test.py
@@ -0,0 +1,143 @@
+"""Test basic gRPC traffic."""
+
+# 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
+from ports import get_port
+import sys
+
+
+class TestGrpc():
+ """Test basic gRPC traffic."""
+
+ def __init__(self, description: str):
+ """Configure a TestRun for gRPC traffic.
+
+ :param description: The description for the test runs.
+ """
+ self._description = description
+
+ def _configure_dns(self, tr: 'TestRun') -> 'Process':
+ """Configure a locally running MicroDNS server.
+
+ :param tr: The TestRun with which to associate the MicroDNS server.
+ :return: The MicroDNS server process.
+ """
+ self._dns = tr.MakeDNServer("dns", default=['127.0.0.1'])
+ return self._dns
+
+ def _configure_traffic_server(self, tr: 'TestRun', dns_port: int,
server_port: int) -> 'Process':
+ """Configure the traffic server process.
+
+ :param tr: The TestRun with which to associate the traffic server.
+ :param dns_port: The MicroDNS server port that traffic server should
connect to.
+ :param server_port: The gRPC server port that traffic server should
connect to.
+ :return: The traffic server process.
+ """
+ self._ts = tr.MakeATSProcess("ts", enable_tls=True, enable_cache=False)
+
+ self._ts.addDefaultSSLFiles()
+ self._ts.Disk.ssl_multicert_config.AddLine("dest_ip=*
ssl_cert_name=server.pem ssl_key_name=server.key")
+
+ self._ts.Disk.remap_config.AddLine(f"map /
https://example.com:{server_port}/")
+
+ self._ts.Disk.records_config.update(
+ {
+ "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir,
+ "proxy.config.ssl.server.private_key.path":
self._ts.Variables.SSLDir,
+ 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1',
+ 'proxy.config.http.server_session_sharing.pool': 'thread',
+ 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE',
+ 'proxy.config.dns.nameservers': f"127.0.0.1:{dns_port}",
+ 'proxy.config.dns.resolv_conf': "NULL",
+ "proxy.config.diags.debug.enabled": 1,
+ "proxy.config.diags.debug.tags": "http",
+ })
+ return self._ts
+
+ def _configure_grpc_server(self, tr: 'TestRun') -> 'Process':
+ """Start the gRPC server.
+
+ :param tr: The TestRun with which to associate the gRPC server.
+ :return: The gRPC server process.
+ """
+ tr.Setup.Copy('grpc_server.py')
+ self._server = tr.Processes.Process('server')
+
+ server_pem = os.path.join(Test.Variables.AtsTestToolsDir, "ssl",
"server.pem")
+ server_key = os.path.join(Test.Variables.AtsTestToolsDir, "ssl",
"server.key")
+ self._server.Setup.Copy(server_pem)
+ self._server.Setup.Copy(server_key)
+
+ port = get_port(self._server, 'port')
+ command = (f'{sys.executable} {tr.RunDirectory}/grpc_server.py {port} '
+ 'server.pem server.key')
+ self._server.Command = command
+ self._server.ReturnCode = 0
+ return self._server
+
+ def _configure_grpc_client(self, tr: 'TestRun', proxy_port: int) -> None:
+ """Start the gRPC client.
+
+ :param tr: The TestRun with which to associate the gRPC client.
+ :param proxy_port: The proxy_port to which to connect.
+ """
+ tr.Setup.Copy('grpc_client.py')
+ ts_cert = os.path.join(self._ts.Variables.SSLDir, 'server.pem')
+ # The cert is for example.com, so we must use that domain.
+ hostname = 'example.com'
+ command = (f'{sys.executable} {tr.RunDirectory}/grpc_client.py '
+ f'{hostname} {proxy_port} {ts_cert}')
+ tr.Processes.Default.Command = command
+ tr.Processes.Default.ReturnCode = 0
+
+ def _compile_protobuf_files(self) -> None:
+ """Compile the protobuf files."""
+ tr = Test.AddTestRun(f'{self._description}: compile the protobuf
files.')
+ tr.Setup.Copy('simple.proto')
+ command = (
+ f'{sys.executable} -m grpc_tools.protoc -I{tr.RunDirectory} '
+ f'--python_out={tr.RunDirectory}
--grpc_python_out={tr.RunDirectory} simple.proto')
+ tr.Processes.Default.Command = command
+ pb2_file = os.path.join(tr.RunDirectory, 'simple_pb2.py')
+ tr.Disk.File(pb2_file, id='pb2', exists=True)
+
+ pb2_grpc_file = os.path.join(tr.RunDirectory, 'simple_pb2_grpc.py')
+ tr.Disk.File(pb2_grpc_file, id='pb2_grpc', exists=True)
+
+ def _run_test_traffic(self) -> None:
+ """Configure the TestRun for the client and servers."""
+ tr = Test.AddTestRun(f'{self._description}: run the gRPC traffic.')
+
+ dns = self._configure_dns(tr)
+ server = self._configure_grpc_server(tr)
+ ts = self._configure_traffic_server(tr, dns.Variables.Port,
server.Variables.port)
+
+ tr.Processes.Default.StartBefore(dns)
+ tr.Processes.Default.StartBefore(server)
+ tr.Processes.Default.StartBefore(ts)
+
+ self._configure_grpc_client(tr, ts.Variables.ssl_port)
+
+ def run(self) -> None:
+ """Configure the various test runs for the gRPC test."""
+ self._compile_protobuf_files()
+ self._run_test_traffic()
+
+
+test = TestGrpc("Test basic gRPC traffic")
+test.run()
diff --git a/tests/gold_tests/h2/grpc/grpc_client.py
b/tests/gold_tests/h2/grpc/grpc_client.py
new file mode 100644
index 0000000000..db5990bee6
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/grpc_client.py
@@ -0,0 +1,74 @@
+"""A gRPC client."""
+
+# 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 argparse
+import grpc
+import os
+import sys
+
+import simple_pb2
+import simple_pb2_grpc
+
+
+def run_grpc_client(hostname: str, proxy_port: int, proxy_cert: bytes) -> int:
+ """Run the gRPC client.
+
+ :param hostname: The hostname to which to connect.
+ :param proxy_port: The ATS port to which to connect.
+ :param proxy_cert: The public TLS certificate to verify ATS against.
+ :return: The exit code.
+ """
+ credentials = grpc.ssl_channel_credentials(root_certificates=proxy_cert)
+ channel_options = (('grpc.ssl_target_name_override', hostname),)
+ destination_endpoint = f'127.0.0.1:{proxy_port}'
+ channel = grpc.secure_channel(destination_endpoint, credentials,
options=channel_options)
+ print(f'Connecting to: {destination_endpoint}')
+ stub = simple_pb2_grpc.SimpleStub(channel)
+
+ message = simple_pb2.SimpleRequest(message="Client request message")
+ response = stub.SimpleMethod(message)
+ print(f'Response received from server: {response.message}')
+ return 0
+
+
+def parse_args() -> argparse.Namespace:
+ """Parse command line arguments."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('hostname', help='The hostname to which to connect.')
+
+ parser.add_argument('proxy_port', type=int, help='The ATS port to which to
connect.')
+ parser.add_argument('proxy_cert', type=argparse.FileType('rb'), help='The
public TLS certificate to use.')
+ return parser.parse_args()
+
+
+def main() -> int:
+ """Run the main entry point for the gRPC client.
+
+ :return: The exit code.
+ """
+ args = parse_args()
+
+ try:
+ return run_grpc_client(args.hostname, args.proxy_port,
args.proxy_cert.read())
+ except grpc.RpcError as e:
+ print(f'RPC failed with code {e.code()}: {e.details()}')
+ return 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tests/gold_tests/h2/grpc/grpc_server.py
b/tests/gold_tests/h2/grpc/grpc_server.py
new file mode 100644
index 0000000000..109b303cc3
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/grpc_server.py
@@ -0,0 +1,81 @@
+"""A gRPC server."""
+
+# 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 argparse
+from concurrent import futures
+import grpc
+import sys
+import time
+
+import simple_pb2
+import simple_pb2_grpc
+
+
+class SimpleServicer(simple_pb2_grpc.SimpleServicer):
+ """A gRPC servicer."""
+
+ def SimpleMethod(self, request, context):
+ """An example gRPC method."""
+ print(f'Request received from client: {request.message}')
+ response = simple_pb2.SimpleResponse(message=f"Echo:
{request.message}")
+ return response
+
+
+def run_grpc_server(port: int, server_cert: str, server_key: str) -> int:
+ """Run the gRPC server.
+
+ :param port: The port on which to listen.
+ :param server_cert: The public TLS certificate to use.
+ :param server_key: The private TLS key to use.
+ :return: The exit code.
+ """
+ credentials = grpc.ssl_server_credentials([(server_key, server_cert)])
+ server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
+ simple_pb2_grpc.add_SimpleServicer_to_server(SimpleServicer(), server)
+ server_endpoint = f'127.0.0.1:{port}'
+ server.add_secure_port(server_endpoint, credentials)
+ print(f'Listening on: {server_endpoint}')
+ server.start()
+ try:
+ server.wait_for_termination()
+ except KeyboardInterrupt:
+ print("Keyboard interrupt received, exiting.")
+ return 0
+ return 0
+
+
+def parse_args() -> argparse.Namespace:
+ """Parse command line arguments."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('port', type=int, help='The port on which to listen.')
+ parser.add_argument('server_crt', type=argparse.FileType('rb'), help="The
public TLS certificate to use.")
+ parser.add_argument('server_key', type=argparse.FileType('rb'), help="The
private TLS key to use.")
+ return parser.parse_args()
+
+
+def main() -> int:
+ """Run the main entry point for the gRPC server.
+
+ :return: The exit code.
+ """
+ args = parse_args()
+ return run_grpc_server(args.port, args.server_crt.read(),
args.server_key.read())
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tests/gold_tests/h2/grpc/simple.proto
b/tests/gold_tests/h2/grpc/simple.proto
new file mode 100644
index 0000000000..d8f2aa106b
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/simple.proto
@@ -0,0 +1,38 @@
+/** @file
+
+ The gRPC protocol buffer definition for the gRPC autest.
+
+ @section license License
+
+ 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.
+ */
+
+syntax = "proto3";
+
+package simple;
+
+service Simple {
+ rpc SimpleMethod(SimpleRequest) returns (SimpleResponse) {}
+}
+
+message SimpleRequest {
+ string message = 1;
+}
+
+message SimpleResponse {
+ string message = 1;
+}