This is an automated email from the ASF dual-hosted git repository. maskit 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 335054e739 Check SNI in h3 (#10184) 335054e739 is described below commit 335054e739cb2402b65211342c9a5d1ef7aa3330 Author: Zhengxi Li <lzx404...@hotmail.com> AuthorDate: Mon Aug 14 18:18:22 2023 -0400 Check SNI in h3 (#10184) --- proxy/http3/Http3SessionAccept.cc | 7 +- tests/gold_tests/h3/h3_sni_check.test.py | 129 +++++++++++++++++++++++++ tests/gold_tests/h3/replays/h3_sni.replay.yaml | 72 ++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) diff --git a/proxy/http3/Http3SessionAccept.cc b/proxy/http3/Http3SessionAccept.cc index ab00d0aa20..be3f2b90d2 100644 --- a/proxy/http3/Http3SessionAccept.cc +++ b/proxy/http3/Http3SessionAccept.cc @@ -48,6 +48,12 @@ Http3SessionAccept::accept(NetVConnection *netvc, MIOBuffer *iobuf, IOBufferRead Warning("QUIC client '%s' prohibited by ip-allow policy", ats_ip_ntop(client_ip, ipb, sizeof(ipb))); return false; } + // RFC9114, section 3.2-2: Client must send the SNI extension. + if (auto sni = netvc->get_service<TLSSNISupport>(); !sni || sni->get_sni_server_name()[0] == '\0') { + ip_port_text_buffer ipb; + Debug("http3", "SNI not found in connection from %s.", ats_ip_nptop(client_ip, ipb, sizeof(ipb))); + return false; + } netvc->attributes = this->options.transport_type; @@ -59,7 +65,6 @@ Http3SessionAccept::accept(NetVConnection *netvc, MIOBuffer *iobuf, IOBufferRead Debug("http3", "[%s] accepted connection from %s transport type = %d", qvc->cids().data(), ats_ip_nptop(client_ip, ipb, sizeof(ipb)), netvc->attributes); } - std::string_view alpn = qvc->negotiated_application_name(); if (IP_PROTO_TAG_HTTP_QUIC.compare(alpn) == 0 || IP_PROTO_TAG_HTTP_QUIC_D29.compare(alpn) == 0) { diff --git a/tests/gold_tests/h3/h3_sni_check.test.py b/tests/gold_tests/h3/h3_sni_check.test.py new file mode 100644 index 0000000000..7dea1a8ba7 --- /dev/null +++ b/tests/gold_tests/h3/h3_sni_check.test.py @@ -0,0 +1,129 @@ +''' +Verify h3 SNI checking 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. + +Test.Summary = ''' +Verify h3 SNI checking behavior. +''' + +Test.SkipUnless( + Condition.HasATSFeature('TS_HAS_QUICHE'), + Condition.HasCurlFeature('http3') +) + +Test.ContinueOnFail = True + + +class Test_sni_check: + """Configure a test to verify SNI checking behavior for h3 connections.""" + + replay_file = "replays/h3_sni.replay.yaml" + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__(self, name: str, gold_file="", replay_keys="", expect_request_rejected=False): + """Initialize the test. + + :param name: The name of the test. + :param gold_file: (Optional) Gold file to be checked. + :param replay_keys: (Optional) Keys to be used by pv. + :param expect_request_rejected: (Optional) Whether or not the client request is expected to be rejected. + """ + self.name = name + self.gold_file = gold_file + self.replay_keys = replay_keys + self.expect_request_rejected = expect_request_rejected + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = tr.AddVerifierServerProcess( + f"server_{Test_sni_check.server_counter}", + self.replay_file) + Test_sni_check.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f"ts-{Test_sni_check.ts_counter}", enable_quic=True, enable_tls=True) + + Test_sni_check.ts_counter += 1 + self._ts = ts + # Configure TLS for Traffic Server. + 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.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.quic.no_activity_timeout_in': 0, + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + def run(self): + """Run the test.""" + tr = Test.AddTestRun(self.name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess( + f'client-{Test_sni_check.client_counter}', + self.replay_file, + http3_ports=[self._ts.Variables.ssl_port], + keys=self.replay_keys) + Test_sni_check.client_counter += 1 + + if self.expect_request_rejected: + # The client request should time out because ATS rejects it and does + # not send a response. + tr.Processes.Default.ReturnCode = 1 + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "SNI not found", "ATS should detect the missing SNI.") + else: + # Verify the client request is successful. + tr.Processes.Default.ReturnCode = 0 + self._ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "SNI not found", "ATS should see the SNI presented by client.") + + if self.gold_file: + tr.Processes.Default.Streams.all = self.gold_file + + +# TEST 1: Client request with SNI. +test0 = Test_sni_check( + "", replay_keys="has_sni", expect_request_rejected=False) +test0.run() + +# TEST 2: Client request without SNI. +test1 = Test_sni_check( + "", replay_keys="no_sni", expect_request_rejected=True) +test1.run() diff --git a/tests/gold_tests/h3/replays/h3_sni.replay.yaml b/tests/gold_tests/h3/replays/h3_sni.replay.yaml new file mode 100644 index 0000000000..135d375dfc --- /dev/null +++ b/tests/gold_tests/h3/replays/h3_sni.replay.yaml @@ -0,0 +1,72 @@ +# 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" + +sessions: +- protocol: + - name: http + version: 3 + - name: tls + sni: test_sni + transactions: + + - client-request: + version: "3" + headers: + fields: + - [ Content-Length, 0 ] + - [:method, GET] + - [:scheme, https] + - [:authority, example.com] + - [:path, /path/test1] + - [ uuid, has_sni ] + server-response: + status: 200 + reason: "OK" + headers: + fields: + - [ Content-Length, 20 ] + + proxy-response: + status: 200 + +- protocol: + - name: http + version: 3 + # Note that the SNI is not specified for this connection. + - name: tls + transactions: + - client-request: + version: "3" + headers: + fields: + - [ Content-Length, 0 ] + - [:method, GET] + - [:scheme, https] + - [:authority, example.com] + - [:path, /path/test1] + - [ uuid, no_sni ] + server-response: + status: 200 + reason: "OK" + headers: + fields: + - [ Content-Length, 20 ] + + proxy-response: + status: 200