This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 32cdc62277 feat(stream): match a stream route by several SNIs (#13911)
32cdc62277 is described below
commit 32cdc622777d2b5889671ea971b98b9dc8ed79c5
Author: AlinsRan <[email protected]>
AuthorDate: Wed Sep 16 15:19:12 2026 +0800
feat(stream): match a stream route by several SNIs (#13911)
---
apisix/router.lua | 10 +
apisix/schema_def.lua | 15 +
apisix/stream/router/ip_port.lua | 72 +++--
docs/en/latest/stream-proxy.md | 22 ++
docs/zh/latest/stream-proxy.md | 20 ++
t/stream-node/stream-route-snis.t | 565 ++++++++++++++++++++++++++++++++++++++
6 files changed, 679 insertions(+), 25 deletions(-)
diff --git a/apisix/router.lua b/apisix/router.lua
index 6cdd071755..f098f78a89 100644
--- a/apisix/router.lua
+++ b/apisix/router.lua
@@ -43,6 +43,16 @@ local function filter(route)
end
end
+ -- the same filter runs for stream routes, whose SNIs are matched against
+ -- apisix/ssl.lua's server_name(), which always returns them lowercased
+ if route.value.sni then
+ route.value.sni = str_lower(route.value.sni)
+ elseif route.value.snis then
+ for i, v in ipairs(route.value.snis) do
+ route.value.snis[i] = str_lower(v)
+ end
+ end
+
apisix_upstream.filter_upstream(route.value.upstream, route)
end
diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua
index 85dd409052..959cc941b0 100644
--- a/apisix/schema_def.lua
+++ b/apisix/schema_def.lua
@@ -1050,6 +1050,16 @@ _M.stream_route = {
type = "string",
pattern = host_def_pat,
},
+ snis = {
+ description = "server name indications, matched as alternatives",
+ type = "array",
+ items = {
+ type = "string",
+ pattern = host_def_pat,
+ },
+ minItems = 1,
+ uniqueItems = true,
+ },
tls_passthrough = {
description = "forward the TLS stream to the upstream untouched
instead of "
.. "terminating it here; only consulted on a mixed
listen, one "
@@ -1063,6 +1073,11 @@ _M.stream_route = {
plugins = plugins_schema,
protocol = xrpc_protocol_schema,
},
+ -- `snis` is the plural form of `sni`, not an addition to it. Carrying both
+ -- would leave the precedence between them to guesswork.
+ ["not"] = {
+ required = {"sni", "snis"},
+ },
additionalProperties = false,
}
diff --git a/apisix/stream/router/ip_port.lua b/apisix/stream/router/ip_port.lua
index ce0a819108..ecc1b344df 100644
--- a/apisix/stream/router/ip_port.lua
+++ b/apisix/stream/router/ip_port.lua
@@ -61,6 +61,26 @@ local function match_addrs(route, vars)
end
+-- Returns the SNIs a stream route is matched by, or nil when it puts no
+-- restriction on the SNI. `sni` and `snis` are the singular and plural form of
+-- the same thing; the schema forbids carrying both.
+local function get_snis(route)
+ local snis = route.snis
+ if not snis then
+ if not route.sni then
+ return nil
+ end
+ snis = {route.sni}
+ end
+
+ if #snis == 0 then
+ return nil
+ end
+
+ return snis
+end
+
+
local create_router
do
local sni_to_items = {}
@@ -91,38 +111,40 @@ do
if item.value.server_addr then
item.value.server_addr_matcher =
core_ip.create_ip_matcher({item.value.server_addr})
end
- if not route.sni then
+ local snis = get_snis(route)
+ if not snis then
other_routes[other_routes_idx] = item
other_routes_idx = other_routes_idx + 1
goto CONTINUE
end
- local sni_rev = route.sni:reverse()
- local stored = sni_to_items[sni_rev]
- if stored then
- core.table.insert(stored, item)
- goto CONTINUE
- end
-
- sni_to_items[sni_rev] = {item}
- tls_routes[tls_routes_idx] = {
- paths = sni_rev,
- filter_fun = function (vars, opts, ctx)
- local items = sni_to_items[sni_rev]
- for _, route in ipairs(items) do
- local hit = match_addrs(route, vars)
- if hit then
- ctx.matched_route = route
- return true
+ for _, sni in ipairs(snis) do
+ local sni_rev = sni:reverse()
+ local stored = sni_to_items[sni_rev]
+ if stored then
+ core.table.insert(stored, item)
+ else
+ sni_to_items[sni_rev] = {item}
+ tls_routes[tls_routes_idx] = {
+ paths = sni_rev,
+ filter_fun = function (vars, opts, ctx)
+ local items = sni_to_items[sni_rev]
+ for _, route in ipairs(items) do
+ local hit = match_addrs(route, vars)
+ if hit then
+ ctx.matched_route = route
+ return true
+ end
+ end
+ return false
+ end,
+ handler = function (ctx, sni_rev)
+ -- done in the filter_fun
end
- end
- return false
- end,
- handler = function (ctx, sni_rev)
- -- done in the filter_fun
+ }
+ tls_routes_idx = tls_routes_idx + 1
end
- }
- tls_routes_idx = tls_routes_idx + 1
+ end
::CONTINUE::
end
diff --git a/docs/en/latest/stream-proxy.md b/docs/en/latest/stream-proxy.md
index e7152058de..7b1a2c9175 100644
--- a/docs/en/latest/stream-proxy.md
+++ b/docs/en/latest/stream-proxy.md
@@ -221,6 +221,28 @@ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H
"X-API-KEY: $admin_ke
In this case, a connection handshaked with SNI `a.test.com` will be proxied to
`127.0.0.1:5991`.
+To serve several names from one route, use `snis`, the plural form of `sni`:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H "X-API-KEY:
$admin_key" -X PUT -d '
+{
+ "snis": ["a.test.com", "b.test.com"],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:5991": 1
+ },
+ "type": "roundrobin"
+ }
+}'
+```
+
+Here a connection handshaked with SNI `a.test.com` or `b.test.com` will be
proxied to
+`127.0.0.1:5991`. Wildcards are matched as a suffix, so `*.test.com` also
matches
+`a.b.test.com`, and a single `*` matches any SNI — a connection carrying none
is still left to
+the routes that match on address instead.
+
+A route can carry `sni` or `snis`, not both.
+
## Proxy to TLS over TCP upstream
APISIX also supports proxying to TLS over TCP upstream.
diff --git a/docs/zh/latest/stream-proxy.md b/docs/zh/latest/stream-proxy.md
index bf1cb3ec87..f49723fd0a 100644
--- a/docs/zh/latest/stream-proxy.md
+++ b/docs/zh/latest/stream-proxy.md
@@ -212,6 +212,26 @@ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H
"X-API-KEY: $admin_ke
在这里,握手时发送 SNI `a.test.com` 的连接会被代理到 `127.0.0.1:5991`。
+如果一条路由需要匹配多个名字,使用 `sni` 的复数形式 `snis`:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H "X-API-KEY:
$admin_key" -X PUT -d '
+{
+ "snis": ["a.test.com", "b.test.com"],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:5991": 1
+ },
+ "type": "roundrobin"
+ }
+}'
+```
+
+握手时发送 SNI `a.test.com` 或 `b.test.com` 的连接都会被代理到 `127.0.0.1:5991`。通配符按后缀匹配,
+因此 `*.test.com` 同样匹配 `a.b.test.com`;单独的 `*` 匹配任意 SNI,而不携带 SNI
的连接仍然交给按地址匹配的路由处理。
+
+一条路由只能携带 `sni` 或 `snis` 其中之一。
+
## 代理到基于 TCP 的 TLS 上游
APISIX 还支持代理到基于 TCP 的 TLS 上游。
diff --git a/t/stream-node/stream-route-snis.t
b/t/stream-node/stream-route-snis.t
new file mode 100644
index 0000000000..f1d74dd191
--- /dev/null
+++ b/t/stream-node/stream-route-snis.t
@@ -0,0 +1,565 @@
+#
+# 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.
+#
+use t::APISIX 'no_plan';
+
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ # The TLS requests are sent from inside the same nginx instance that runs
+ # the setup, so that the SNI router is exercised only once the stream
+ # subsystem has synced the configuration from etcd.
+ # An empty response body therefore means that no stream route was matched:
+ # apisix closes the connection without proxying anything.
+ $block->set_value("stream_conf_enable", 1);
+
+ my $config = $block->config // '';
+ $config .= <<_EOC_;
+ location /tls {
+ content_by_lua_block {
+ local sock = ngx.socket.tcp()
+ sock:settimeout(2000)
+ local ok, err = sock:connect("127.0.0.1", 2005)
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ local sess, err = sock:sslhandshake(nil, ngx.var.arg_sni, false)
+ if not sess then
+ sock:close()
+ ngx.say("failed to do SSL handshake: ", err)
+ return
+ end
+
+ local bytes, err = sock:send("mmm")
+ if not bytes then
+ sock:close()
+ ngx.say("send stream request error: ", err)
+ return
+ end
+
+ -- reaching peer closure yields `nil, "closed", partial`; the bytes
+ -- read so far are the response
+ local data, err, partial = sock:receive("*a")
+ sock:close()
+ if not data and err ~= "closed" then
+ ngx.say("receive stream response error: ", err)
+ return
+ end
+
+ ngx.print(data or partial or "")
+ }
+ }
+_EOC_
+
+ $block->set_value("config", $config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: every sni of the route is matched
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["a.test.com", "b.test.com"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=a.test.com", "GET /tls?sni=b.test.com", "GET
/tls?sni=c.test.com"]
+--- response_body eval
+["passed\n", "hello world\n", "hello world\n", ""]
+
+
+
+=== TEST 2: the singular sni still works on its own
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "sni": "a.test.com",
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=a.test.com", "GET /tls?sni=b.test.com"]
+--- response_body eval
+["passed\n", "hello world\n", ""]
+
+
+
+=== TEST 3: sni and snis can not be carried together
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local code, body = t.test('/apisix/admin/stream_routes/1',
ngx.HTTP_PUT,
+ [[{
+ "sni": "a.test.com",
+ "snis": ["b.test.com"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ ngx.say(code)
+ }
+ }
+--- request
+GET /t
+--- response_body
+400
+
+
+
+=== TEST 4: a wildcard sni is a suffix match
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["*.test.com"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=a.test.com", "GET /tls?sni=a.b.test.com", "GET
/tls?sni=test.org"]
+--- response_body eval
+["passed\n", "hello world\n", "hello world\n", ""]
+
+
+
+=== TEST 5: a bare * matches any SNI, but not a connection carrying none
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["*"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=whatever.test.com", "GET
/tls?sni=another.test.org"]
+--- response_body eval
+["passed\n", "hello world\n", "hello world\n"]
+
+
+
+=== TEST 6: the snis are matched case-insensitively
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["Mixed.TEST.com"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=mixed.test.com", "GET /tls?sni=other.test.com"]
+--- response_body eval
+["passed\n", "hello world\n", ""]
+
+
+
+=== TEST 7: the snis are matched on a TLS passthrough listen
+--- stream_server_config
+ listen 2005;
+ ssl_preread on;
+
+ preread_by_lua_block {
+ ngx.sleep(0.1)
+ apisix.stream_preread_phase(true)
+ }
+
+ proxy_pass apisix_backend;
+--- extra_stream_config
+ server {
+ listen 1997 ssl;
+ ssl_certificate cert/apisix.crt;
+ ssl_certificate_key cert/apisix.key;
+ content_by_lua_block {
+ ngx.say("hello from the backend")
+ }
+ }
+--- config
+ location /setup {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ -- the listen carries no `ssl`, so it holds no certificate and a
+ -- completed handshake can only have been terminated by the backend
+ local code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["a.test.com", "b.test.com"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1997": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=a.test.com", "GET /tls?sni=b.test.com", "GET
/tls?sni=c.test.com"]
+--- response_body_like eval
+[qr/^passed$/, qr/^hello from the backend$/, qr/^hello from the backend$/,
+ qr/failed to do SSL handshake/]
+--- no_error_log
+[alert]
+
+
+
+=== TEST 8: the singular sni is matched case-insensitively too
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "sni": "Mixed.SNI.com",
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=mixed.sni.com", "GET /tls?sni=other.sni.com"]
+--- response_body eval
+["passed\n", "hello world\n", ""]
+
+
+
+=== TEST 9: two routes sharing one sni are told apart by their address
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ -- both carry a.test.com, so they share one radixtree entry and are
+ -- separated by match_addrs inside its filter_fun
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["a.test.com"],
+ "server_port": 1985,
+ "upstream": {
+ "nodes": {"127.0.0.1:1": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route 1: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/2', ngx.HTTP_PUT,
+ [[{
+ "snis": ["a.test.com"],
+ "server_port": 2005,
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route 2: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=a.test.com"]
+--- response_body eval
+["passed\n", "hello world\n"]
+
+
+
+=== TEST 10: shrinking the sni list stops matching the removed name
+--- config
+ location /setup {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local code = t.test('/apisix/admin/ssls/1', ngx.HTTP_PUT,
+ core.json.encode({cert = ssl_cert, key = ssl_key, sni = "*"}))
+ if code >= 300 then
+ ngx.say("failed to create ssl: ", code)
+ return
+ end
+
+ code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["a.test.com", "b.test.com"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+
+ location /shrink {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["a.test.com"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1995": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to update stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("shrunk")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=b.test.com", "GET /shrink", "GET
/tls?sni=b.test.com", "GET /tls?sni=a.test.com"]
+--- response_body eval
+["passed\n", "hello world\n", "shrunk\n", "", "hello world\n"]
+
+
+
+=== TEST 11: a bare * on a passthrough listen still needs an SNI
+--- stream_server_config
+ listen 2005;
+ ssl_preread on;
+
+ preread_by_lua_block {
+ ngx.sleep(0.1)
+ apisix.stream_preread_phase(true)
+ }
+
+ proxy_pass apisix_backend;
+--- extra_stream_config
+ server {
+ listen 1997 ssl;
+ ssl_certificate cert/apisix.crt;
+ ssl_certificate_key cert/apisix.key;
+ content_by_lua_block {
+ ngx.say("hello from the backend")
+ }
+ }
+--- config
+ location /setup {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local code = t.test('/apisix/admin/stream_routes/1', ngx.HTTP_PUT,
+ [[{
+ "snis": ["*"],
+ "upstream": {
+ "nodes": {"127.0.0.1:1997": 1},
+ "type": "roundrobin"
+ }
+ }]])
+ if code >= 300 then
+ ngx.say("failed to create stream route: ", code)
+ return
+ end
+
+ ngx.sleep(0.5)
+ ngx.say("passed")
+ }
+ }
+--- pipelined_requests eval
+["GET /setup", "GET /tls?sni=anything.test.com", "GET /tls"]
+--- response_body_like eval
+[qr/^passed$/, qr/^hello from the backend$/, qr/failed to do SSL handshake/]
+--- no_error_log
+[alert]