This is an automated email from the ASF dual-hosted git repository.

nic-6443 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 4278bc9ae5 fix(redis): send the TLS SNI and add redis_server_name 
(#13938)
4278bc9ae5 is described below

commit 4278bc9ae5e6322a0ef222e047e8395f05829b62
Author: Mohammad Izzraff Janius 
<[email protected]>
AuthorDate: Tue Sep 15 16:15:42 2026 +0900

    fix(redis): send the TLS SNI and add redis_server_name (#13938)
---
 apisix/plugins/limit-count/util.lua           |  14 +-
 apisix/utils/redis-schema.lua                 |   3 +
 apisix/utils/redis.lua                        |  14 +-
 docs/en/latest/plugins/ai-cache.md            |   1 +
 docs/en/latest/plugins/ai-rate-limiting.md    |   1 +
 docs/en/latest/plugins/graphql-limit-count.md |   1 +
 docs/en/latest/plugins/limit-conn.md          |   1 +
 docs/en/latest/plugins/limit-count.md         |   1 +
 docs/en/latest/plugins/limit-req.md           |   1 +
 docs/zh/latest/plugins/ai-cache.md            |   1 +
 docs/zh/latest/plugins/graphql-limit-count.md |   1 +
 docs/zh/latest/plugins/limit-conn.md          |   1 +
 docs/zh/latest/plugins/limit-count.md         |   1 +
 docs/zh/latest/plugins/limit-req.md           |   1 +
 t/utils/redis-schema.t                        |  41 +++
 t/utils/redis-sni.t                           | 478 ++++++++++++++++++++++++++
 16 files changed, 559 insertions(+), 2 deletions(-)

diff --git a/apisix/plugins/limit-count/util.lua 
b/apisix/plugins/limit-count/util.lua
index 19c1751b2d..7b66c89e3b 100644
--- a/apisix/plugins/limit-count/util.lua
+++ b/apisix/plugins/limit-count/util.lua
@@ -45,7 +45,7 @@ function _M.redis_cli(conf)
     -- so connections with different databases, credentials or TLS settings
     -- must not share the default host:port keepalive pool, otherwise a
     -- reused connection may be bound to an unexpected database or user, or
-    -- skip the expected certificate verification
+    -- skip the expected certificate verification / present the wrong SNI
     local scheme = "redis"
     if conf.redis_ssl then
         scheme = conf.redis_ssl_verify and "rediss-verify" or "rediss"
@@ -56,11 +56,23 @@ function _M.redis_cli(conf)
         -- digest instead of the plaintext credentials in the pool name
         pool = pool .. "#" .. crc32((conf.redis_username or "") .. ":" .. 
conf.redis_password)
     end
+    if conf.redis_ssl and conf.redis_server_name then
+        pool = pool .. "#" .. conf.redis_server_name
+    end
+
+    local server_name
+    if conf.redis_ssl then
+        server_name = conf.redis_server_name or conf.redis_host
+        if core.utils.parse_ipv4(server_name) or 
core.utils.parse_ipv6(server_name) then
+            server_name = nil
+        end
+    end
 
     local sock_opts = {
         ssl = conf.redis_ssl,
         ssl_verify = conf.redis_ssl_verify,
         pool = pool,
+        server_name = server_name,
     }
 
     local ok, err = red:connect(conf.redis_host, conf.redis_port or 6379, 
sock_opts)
diff --git a/apisix/utils/redis-schema.lua b/apisix/utils/redis-schema.lua
index 991919a2a9..b722d5b8f6 100644
--- a/apisix/utils/redis-schema.lua
+++ b/apisix/utils/redis-schema.lua
@@ -44,6 +44,9 @@ local policy_to_additional_properties = {
             redis_ssl_verify = {
                 type = "boolean", default = false,
             },
+            redis_server_name = {
+                type = "string", minLength = 1,
+            },
             redis_keepalive_timeout = {
                 type = "integer", minimum = 1000, default = 10000
             },
diff --git a/apisix/utils/redis.lua b/apisix/utils/redis.lua
index 5634185925..8af17db692 100644
--- a/apisix/utils/redis.lua
+++ b/apisix/utils/redis.lua
@@ -31,7 +31,7 @@ local function redis_cli(conf)
     -- so connections with different databases, credentials or TLS settings
     -- must not share the default host:port keepalive pool, otherwise a
     -- reused connection may be bound to an unexpected database or user, or
-    -- skip the expected certificate verification
+    -- skip the expected certificate verification / present the wrong SNI
     local scheme = "redis"
     if conf.redis_ssl then
         scheme = conf.redis_ssl_verify and "rediss-verify" or "rediss"
@@ -42,11 +42,23 @@ local function redis_cli(conf)
         -- digest instead of the plaintext credentials in the pool name
         pool = pool .. "#" .. crc32((conf.redis_username or "") .. ":" .. 
conf.redis_password)
     end
+    if conf.redis_ssl and conf.redis_server_name then
+        pool = pool .. "#" .. conf.redis_server_name
+    end
+
+    local server_name
+    if conf.redis_ssl then
+        server_name = conf.redis_server_name or conf.redis_host
+        if core.utils.parse_ipv4(server_name) or 
core.utils.parse_ipv6(server_name) then
+            server_name = nil
+        end
+    end
 
     local sock_opts = {
         ssl = conf.redis_ssl,
         ssl_verify = conf.redis_ssl_verify,
         pool = pool,
+        server_name = server_name,
     }
 
     local ok, err = red:connect(conf.redis_host, conf.redis_port or 6379, 
sock_opts)
diff --git a/docs/en/latest/plugins/ai-cache.md 
b/docs/en/latest/plugins/ai-cache.md
index 3443f86b84..6018ddf1e7 100644
--- a/docs/en/latest/plugins/ai-cache.md
+++ b/docs/en/latest/plugins/ai-cache.md
@@ -89,6 +89,7 @@ For the `passthrough` protocol, `ai-proxy` forwards the 
client's request method,
 | redis_timeout | integer | False | 1000 | >= 1 | Redis timeout value in 
milliseconds. |
 | redis_ssl | boolean | False | false | | If true, use SSL to connect to 
Redis. |
 | redis_ssl_verify | boolean | False | false | | If true, verify the Redis 
server SSL certificate. |
+| redis_server_name | string | False | | | TLS SNI when `redis_ssl` is true. 
Defaults to `redis_host`. When `redis_ssl_verify` is true the certificate must 
also match this name, so set it when `redis_host` is an alias the certificate 
does not cover. |
 | redis_keepalive_timeout | integer | False | 10000 | >= 1000 | Keepalive 
timeout, in milliseconds, for the Redis connection pool. |
 | redis_keepalive_pool | integer | False | 100 | >= 1 | Maximum number of 
connections in the Redis keepalive pool. |
 
diff --git a/docs/en/latest/plugins/ai-rate-limiting.md 
b/docs/en/latest/plugins/ai-rate-limiting.md
index acf1367a9b..4d8757fae9 100644
--- a/docs/en/latest/plugins/ai-rate-limiting.md
+++ b/docs/en/latest/plugins/ai-rate-limiting.md
@@ -65,6 +65,7 @@ The `ai-rate-limiting` Plugin enforces token-based rate 
limiting for requests se
 | redis_timeout | integer | False | 1000 | [1,...] | The Redis timeout value 
in milliseconds when `policy` is `redis` or `redis-cluster`. |
 | redis_ssl | boolean | False | false | | If true, use SSL to connect to Redis 
when `policy` is `redis`. |
 | redis_ssl_verify | boolean | False | false | | If true, verify the server 
SSL certificate when `policy` is `redis`. |
+| redis_server_name | string | False | | | TLS SNI when `policy` is `redis` 
and `redis_ssl` is true. Defaults to `redis_host`. When `redis_ssl_verify` is 
true the certificate must also match this name, so set it when `redis_host` is 
an alias the certificate does not cover. |
 | redis_cluster_nodes | array[string] | False | | | The list of Redis cluster 
nodes with at least one address. Required when `policy` is `redis-cluster`. |
 | redis_cluster_name | string | False | | | The name of the Redis cluster. 
Required when `policy` is `redis-cluster`. |
 | redis_cluster_ssl | boolean | False | false | | If true, use SSL to connect 
to Redis when `policy` is `redis-cluster`. |
diff --git a/docs/en/latest/plugins/graphql-limit-count.md 
b/docs/en/latest/plugins/graphql-limit-count.md
index f5c1f4979b..e74e2fc743 100644
--- a/docs/en/latest/plugins/graphql-limit-count.md
+++ b/docs/en/latest/plugins/graphql-limit-count.md
@@ -75,6 +75,7 @@ This Plugin shares the same schema as the 
[limit-count](./limit-count.md) Plugin
 | redis_password | string | False | | | Password of the Redis node. Used when 
`policy` is `redis` or `redis-cluster`. |
 | redis_ssl | boolean | False | false | | When true, use SSL to connect to 
Redis. Used when `policy` is `redis`. |
 | redis_ssl_verify | boolean | False | false | | When true, verify the Redis 
server SSL certificate. Used when `policy` is `redis`. |
+| redis_server_name | string | False | | | TLS SNI when `policy` is `redis` 
and `redis_ssl` is true. Defaults to `redis_host`. When `redis_ssl_verify` is 
true the certificate must also match this name, so set it when `redis_host` is 
an alias the certificate does not cover. |
 | redis_database | integer | False | 0 | >= 0 | The Redis database number. 
Used when `policy` is `redis`. |
 | redis_timeout | integer | False | 1000 | [1,...] | Redis timeout in 
milliseconds. Used when `policy` is `redis` or `redis-cluster`. |
 | redis_cluster_nodes | array[string] | False | | | List of Redis cluster node 
addresses. Required when `policy` is `redis-cluster`. |
diff --git a/docs/en/latest/plugins/limit-conn.md 
b/docs/en/latest/plugins/limit-conn.md
index a92ecf2db3..56902f13f1 100644
--- a/docs/en/latest/plugins/limit-conn.md
+++ b/docs/en/latest/plugins/limit-conn.md
@@ -57,6 +57,7 @@ The `limit-conn` Plugin limits the rate of requests by the 
number of concurrent
 | redis_password | string | False | | | The password of the Redis node when 
`policy` is `redis` or `redis-cluster`. |
 | redis_ssl | boolean | False | false | | If true, use SSL to connect to Redis 
when `policy` is `redis`. |
 | redis_ssl_verify | boolean | False | false | | If true, verify the server 
SSL certificate when `policy` is `redis`. |
+| redis_server_name | string | False | | | TLS SNI when `policy` is `redis` 
and `redis_ssl` is true. Defaults to `redis_host`. When `redis_ssl_verify` is 
true the certificate must also match this name, so set it when `redis_host` is 
an alias the certificate does not cover. |
 | redis_database | integer | False | 0 | >= 0 | The database number in Redis 
when `policy` is `redis`. |
 | redis_timeout | integer | False | 1000 | >= 1 | The Redis timeout value in 
milliseconds when `policy` is `redis` or `redis-cluster`. |
 | redis_keepalive_timeout | integer | False | 10000 | >= 1000 | Keepalive 
timeout in milliseconds for Redis when `policy` is `redis` or `redis-cluster`. |
diff --git a/docs/en/latest/plugins/limit-count.md 
b/docs/en/latest/plugins/limit-count.md
index 19fc6a6ac4..c1b938c8e9 100644
--- a/docs/en/latest/plugins/limit-count.md
+++ b/docs/en/latest/plugins/limit-count.md
@@ -81,6 +81,7 @@ Redis Sentinel, sliding windows, and delayed Redis 
synchronization are supported
 | redis_password          | string            | False                          
           |               |                            | The password of the 
Redis node when `policy` is `redis`, `redis-cluster`, or `redis-sentinel`. |
 | redis_ssl               | boolean           | False                          
           | false         |                            | If true, use SSL to 
connect to Redis when `policy` is `redis`. |
 | redis_ssl_verify        | boolean           | False                          
           | false         |                            | If true, verify the 
server SSL certificate when `policy` is `redis`. |
+| redis_server_name       | string            | False                          
           |               |                            | TLS SNI when `policy` 
is `redis` and `redis_ssl` is true. Defaults to `redis_host`. When 
`redis_ssl_verify` is true the certificate must also match this name, so set it 
when `redis_host` is an alias the certificate does not cover. |
 | redis_database          | integer           | False                          
           | 0             | >= 0                       | The database number 
in Redis when `policy` is `redis` or `redis-sentinel`. |
 | redis_timeout           | integer           | False                          
           | 1000          | [1,...]                    | The Redis timeout 
value in milliseconds when `policy` is `redis` or `redis-cluster`. |
 | redis_keepalive_timeout | integer           | False                          
           | 10000 (`redis` and `redis-cluster`); 60000 (`redis-sentinel`) | >= 
1000 (`redis` and `redis-cluster`); >= 1 (`redis-sentinel`) | Keepalive timeout 
in milliseconds for Redis connections. |
diff --git a/docs/en/latest/plugins/limit-req.md 
b/docs/en/latest/plugins/limit-req.md
index 59c65d8e54..eaf771ea3a 100644
--- a/docs/en/latest/plugins/limit-req.md
+++ b/docs/en/latest/plugins/limit-req.md
@@ -64,6 +64,7 @@ The `limit-req` Plugin supports two modes of rate limiting:
 | redis_password | string | False | | | The password of the Redis node when 
`policy` is `redis` or `redis-cluster`. |
 | redis_ssl | boolean | False | false | | If true, use SSL to connect to Redis 
when `policy` is `redis`. |
 | redis_ssl_verify | boolean | False | false | | If true, verify the server 
SSL certificate when `policy` is `redis`. |
+| redis_server_name | string | False | | | TLS SNI when `policy` is `redis` 
and `redis_ssl` is true. Defaults to `redis_host`. When `redis_ssl_verify` is 
true the certificate must also match this name, so set it when `redis_host` is 
an alias the certificate does not cover. |
 | redis_database | integer | False | 0 | >= 0 | The database number in Redis 
when `policy` is `redis`. |
 | redis_timeout | integer | False | 1000 | [1,...] | The Redis timeout value 
in milliseconds when `policy` is `redis` or `redis-cluster`. |
 | redis_keepalive_timeout | integer | False | 10000 | ≥ 1000 | Keepalive 
timeout in milliseconds for redis when `policy` is `redis` or `redis-cluster`. |
diff --git a/docs/zh/latest/plugins/ai-cache.md 
b/docs/zh/latest/plugins/ai-cache.md
index db4d50fc2b..794313ff4a 100644
--- a/docs/zh/latest/plugins/ai-cache.md
+++ b/docs/zh/latest/plugins/ai-cache.md
@@ -89,6 +89,7 @@ import TabItem from '@theme/TabItem';
 | redis_timeout | integer | 否 | 1000 | >= 1 | Redis 超时时间,单位为毫秒。 |
 | redis_ssl | boolean | 否 | false | | 如果为 true,则使用 SSL 连接 Redis。 |
 | redis_ssl_verify | boolean | 否 | false | | 如果为 true,则校验 Redis 服务器的 SSL 证书。 |
+| redis_server_name | string | 否 | | | 当 `redis_ssl` 为 true 时使用的 TLS SNI。默认使用 
`redis_host`。当 `redis_ssl_verify` 为 true 时,证书还必须与该名称匹配,因此当 `redis_host` 
是证书未覆盖的别名时请设置此项。 |
 | redis_keepalive_timeout | integer | 否 | 10000 | >= 1000 | Redis 
连接池的保活超时时间,单位为毫秒。 |
 | redis_keepalive_pool | integer | 否 | 100 | >= 1 | Redis 保活连接池中的最大连接数。 |
 
diff --git a/docs/zh/latest/plugins/graphql-limit-count.md 
b/docs/zh/latest/plugins/graphql-limit-count.md
index 96c2b8347c..5bfc13aa33 100644
--- a/docs/zh/latest/plugins/graphql-limit-count.md
+++ b/docs/zh/latest/plugins/graphql-limit-count.md
@@ -70,6 +70,7 @@ description: graphql-limit-count 插件使用固定窗口算法,基于 GraphQL
 | redis_password | string | 否 | | | Redis 节点密码。`policy` 为 `redis` 或 
`redis-cluster` 时使用。 |
 | redis_ssl | boolean | 否 | false | | 为 true 时使用 SSL 连接 Redis。`policy` 为 
`redis` 时使用。 |
 | redis_ssl_verify | boolean | 否 | false | | 为 true 时验证 Redis 服务端 SSL 
证书。`policy` 为 `redis` 时使用。 |
+| redis_server_name | string | 否 | | | 当 `policy` 为 `redis` 且 `redis_ssl` 为 
true 时使用的 TLS SNI。默认使用 `redis_host`。当 `redis_ssl_verify` 为 true 
时,证书还必须与该名称匹配,因此当 `redis_host` 是证书未覆盖的别名时请设置此项。 |
 | redis_database | integer | 否 | 0 | >= 0 | Redis 数据库编号。`policy` 为 `redis` 
时使用。 |
 | redis_timeout | integer | 否 | 1000 | [1,...] | Redis 超时时间(毫秒)。`policy` 为 
`redis` 或 `redis-cluster` 时使用。 |
 | redis_cluster_nodes | array[string] | 否 | | | Redis 集群节点地址列表。`policy` 为 
`redis-cluster` 时必填。 |
diff --git a/docs/zh/latest/plugins/limit-conn.md 
b/docs/zh/latest/plugins/limit-conn.md
index 85d75a6f19..f5cba87668 100644
--- a/docs/zh/latest/plugins/limit-conn.md
+++ b/docs/zh/latest/plugins/limit-conn.md
@@ -57,6 +57,7 @@ import TabItem from '@theme/TabItem';
 | redis_password | string | 否 | | | 当 `policy` 为 `redis` 或 `redis-cluster` 
时,Redis 节点的密码。|
 | redis_ssl | boolean | 否 | false | | 如果为 true,则在 `policy` 为 `redis` 时使用 SSL 
连接到 Redis。|
 | redis_ssl_verify | boolean | 否 | false | | 如果为 true,则在 `policy` 为 `redis` 
时验证服务器 SSL 证书。|
+| redis_server_name | string | 否 | | | 当 `policy` 为 `redis` 且 `redis_ssl` 为 
true 时使用的 TLS SNI。默认使用 `redis_host`。当 `redis_ssl_verify` 为 true 
时,证书还必须与该名称匹配,因此当 `redis_host` 是证书未覆盖的别名时请设置此项。|
 | redis_database | integer | 否 | 0 | >= 0 | 当 `policy` 为 `redis` 时,Redis 
中的数据库编号。|
 | redis_timeout | integer | 否 | 1000 | >= 1 | 当 `policy` 为 `redis` 或 
`redis-cluster` 时,Redis 超时值(以毫秒为单位)。|
 | redis_keepalive_timeout | integer | 否 | 10000 | >= 1000 | 当 `policy` 为 
`redis` 或 `redis-cluster` 时,Redis 的空闲连接超时时间(以毫秒为单位)。|
diff --git a/docs/zh/latest/plugins/limit-count.md 
b/docs/zh/latest/plugins/limit-count.md
index 69202ff51c..2029f3e29b 100644
--- a/docs/zh/latest/plugins/limit-count.md
+++ b/docs/zh/latest/plugins/limit-count.md
@@ -82,6 +82,7 @@ APISIX 3.18.0 及后续版本支持 Redis Sentinel、滑动窗口和 Redis 延
 | redis_password | string | 否 | | | 当 `policy` 为 `redis`、`redis-cluster` 或 
`redis-sentinel` 时,Redis 节点的密码。 |
 | redis_ssl | boolean | 否 | false | | 如果为 true,则在 `policy` 为 `redis` 时使用 SSL 
连接 Redis。 |
 | redis_ssl_verify | boolean | 否 | false | | 如果为 true,则在 `policy` 为 `redis` 
时验证服务器 SSL 证书。 |
+| redis_server_name | string | 否 | | | 当 `policy` 为 `redis` 且 `redis_ssl` 为 
true 时使用的 TLS SNI。默认使用 `redis_host`。当 `redis_ssl_verify` 为 true 
时,证书还必须与该名称匹配,因此当 `redis_host` 是证书未覆盖的别名时请设置此项。 |
 | redis_database | integer | 否 | 0 | >= 0 | 当 `policy` 为 `redis` 或 
`redis-sentinel` 时,Redis 中的数据库编号。 |
 | redis_timeout | integer | 否 | 1000 | [1,...] | 当 `policy` 为 `redis` 或 
`redis-cluster` 时,Redis 超时值(以毫秒为单位)。 |
 | redis_keepalive_timeout | integer | 否 | `redis` 和 `redis-cluster` 为 
10000;`redis-sentinel` 为 60000 | `redis` 和 `redis-cluster` >= 
1000;`redis-sentinel` >= 1 | Redis 空闲连接超时时间,单位为毫秒。 |
diff --git a/docs/zh/latest/plugins/limit-req.md 
b/docs/zh/latest/plugins/limit-req.md
index 91d92b8f68..40a3e8aaaf 100644
--- a/docs/zh/latest/plugins/limit-req.md
+++ b/docs/zh/latest/plugins/limit-req.md
@@ -64,6 +64,7 @@ import TabItem from '@theme/TabItem';
 | redis_password | string | 否 | | | 当 `policy` 为 `redis` 或 `redis-cluster` 
时,Redis 节点的密码。|
 | redis_ssl | boolean | 否 | false | | 如果为 true,则在 `policy` 为 `redis` 时使用 SSL 
连接到 Redis。|
 | redis_ssl_verify | boolean | 否 | false | | 如果为 true,则在 `policy` 为 `redis` 
时验证服务器 SSL 证书。|
+| redis_server_name | string | 否 | | | 当 `policy` 为 `redis` 且 `redis_ssl` 为 
true 时使用的 TLS SNI。默认使用 `redis_host`。当 `redis_ssl_verify` 为 true 
时,证书还必须与该名称匹配,因此当 `redis_host` 是证书未覆盖的别名时请设置此项。|
 | redis_database | integer | 否 | 0 | >= 0 | 当 `policy` 为 `redis` 时,Redis 
中的数据库编号。|
 | redis_timeout | integer | 否 | 1000 | [1,...] | 当 `policy` 为 `redis` 或 
`redis-cluster` 时,Redis 超时值(以毫秒为单位)。|
 | redis_keepalive_timeout | integer | 否 | 10000 | ≥ 1000 | 当 `policy` 为 
`redis` 或 `redis-cluster` 时,与 Redis 的空闲连接超时时间,单位为毫秒。|
diff --git a/t/utils/redis-schema.t b/t/utils/redis-schema.t
index 493aedcb94..7557efee7c 100644
--- a/t/utils/redis-schema.t
+++ b/t/utils/redis-schema.t
@@ -152,3 +152,44 @@ done
 --- response_body
 3600
 60
+
+
+
+=== TEST 3: redis_server_name is accepted for redis policy and rejected when 
empty
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.limit-count")
+
+            local ok, err = plugin.check_schema({
+                count = 2,
+                time_window = 60,
+                policy = "redis",
+                redis_host = "127.0.0.1",
+                redis_ssl = true,
+                redis_server_name = "redis.example.com",
+            })
+            if not ok then
+                ngx.say(err)
+                return
+            end
+            ngx.say("passed")
+
+            ok, err = plugin.check_schema({
+                count = 2,
+                time_window = 60,
+                policy = "redis",
+                redis_host = "127.0.0.1",
+                redis_ssl = true,
+                redis_server_name = "",
+            })
+            if ok then
+                ngx.say("empty redis_server_name should fail")
+                return
+            end
+            ngx.say("rejected")
+        }
+    }
+--- response_body
+passed
+rejected
diff --git a/t/utils/redis-sni.t b/t/utils/redis-sni.t
new file mode 100644
index 0000000000..5ad1942bfc
--- /dev/null
+++ b/t/utils/redis-sni.t
@@ -0,0 +1,478 @@
+#
+# 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';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    # TLS fronts in front of the plain redis on 6379, like a name-routed cloud
+    # endpoint: 6395 only reaches redis when the SNI is "test.com", any other
+    # SNI (or none) lands on the plain HTTP test server; 6396 reaches redis
+    # regardless of SNI. test.com resolves to 127.0.0.1 via /etc/hosts.
+    # Only the request blocks enable it: stream_enable replaces location /t.
+    $block->set_value("extra_stream_config", <<_EOC_);
+    server {
+        listen 6395 ssl;
+        server_name test.com;
+        ssl_certificate ../../certs/apisix.crt;
+        ssl_certificate_key ../../certs/apisix.key;
+        proxy_pass 127.0.0.1:6379;
+    }
+    server {
+        listen 6395 ssl default_server;
+        server_name _;
+        ssl_certificate ../../certs/apisix.crt;
+        ssl_certificate_key ../../certs/apisix.key;
+        proxy_pass 127.0.0.1:1980;
+    }
+    server {
+        listen 6396 ssl;
+        ssl_certificate ../../certs/apisix.crt;
+        ssl_certificate_key ../../certs/apisix.key;
+        proxy_pass 127.0.0.1:6379;
+    }
+_EOC_
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    if (!$block->error_log && !$block->no_error_log) {
+        $block->set_value("no_error_log", "[error]\n[alert]");
+    }
+
+    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // "";
+    $extra_init_worker_by_lua .= <<_EOC_;
+        require("lib.test_redis").flush_all()
+_EOC_
+    $block->set_value("extra_init_worker_by_lua", $extra_init_worker_by_lua);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: limit-count: redis behind a TLS front that routes by SNI
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "remote_addr",
+                            "policy": "redis",
+                            "redis_host": "test.com",
+                            "redis_port": 6395,
+                            "redis_ssl": true,
+                            "redis_ssl_verify": false
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 2: the SNI reaches redis, the counter works
+--- stream_enable
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 503]
+
+
+
+=== TEST 3: limit-count: redis_ssl_verify also checks the certificate against 
redis_host
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "remote_addr",
+                            "policy": "redis",
+                            "redis_host": "test.com",
+                            "redis_port": 6395,
+                            "redis_ssl": true,
+                            "redis_ssl_verify": true
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 4: verified connection, the counter works
+--- stream_enable
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 503]
+
+
+
+=== TEST 5: limit-count: an IP literal redis_host sends no SNI and skips the 
host check
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "remote_addr",
+                            "policy": "redis",
+                            "redis_host": "127.0.0.1",
+                            "redis_port": 6396,
+                            "redis_ssl": true,
+                            "redis_ssl_verify": true
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 6: the counter works although the certificate is for test.com
+--- stream_enable
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 503]
+
+
+
+=== TEST 7: limit-req (shared redis util): redis behind the SNI-routed front
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "plugins": {
+                        "limit-req": {
+                            "rate": 4,
+                            "burst": 1,
+                            "rejected_code": 503,
+                            "key": "remote_addr",
+                            "policy": "redis",
+                            "redis_host": "test.com",
+                            "redis_port": 6395,
+                            "redis_ssl": true,
+                            "redis_ssl_verify": true
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 8: the SNI reaches redis, the limiter works
+--- stream_enable
+--- pipelined_requests eval
+["GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200]
+
+
+
+=== TEST 9: limit-count: redis_server_name overrides the SNI when redis_host 
is an IP
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "remote_addr",
+                            "policy": "redis",
+                            "redis_host": "127.0.0.1",
+                            "redis_port": 6395,
+                            "redis_ssl": true,
+                            "redis_ssl_verify": true,
+                            "redis_server_name": "test.com"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 10: the SNI-routed front reaches redis and the certificate matches 
the SNI
+--- stream_enable
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 503]
+
+
+
+=== TEST 11: two routes with different redis_server_name must not share a 
keepalive pool
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            for i, sni in ipairs({"test.com", "other.com"}) do
+                local code, body = t('/apisix/admin/routes/' .. i,
+                    ngx.HTTP_PUT,
+                    [[{
+                        "uri": "/hello]] .. (i == 1 and "" or "1") .. [[",
+                        "plugins": {
+                            "limit-count": {
+                                "count": 100,
+                                "time_window": 60,
+                                "key": "remote_addr",
+                                "policy": "redis",
+                                "redis_host": "127.0.0.1",
+                                "redis_port": 6395,
+                                "redis_ssl": true,
+                                "redis_server_name": "]] .. sni .. [["
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        }
+                    }]]
+                    )
+                if code >= 300 then
+                    ngx.status = code
+                    ngx.say(body)
+                    return
+                end
+            end
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 12: the route whose SNI lands on the HTTP server never borrows the 
other route's redis connection
+--- stream_enable
+--- pipelined_requests eval
+["GET /hello", "GET /hello1", "GET /hello", "GET /hello1"]
+--- error_code eval
+[200, 500, 200, 500]
+--- no_error_log
+[alert]
+
+
+
+=== TEST 13: limit-count: a DNS redis_host not covered by the certificate 
fails the host check
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "remote_addr",
+                            "policy": "redis",
+                            "redis_host": "admin.apisix.dev",
+                            "redis_port": 6396,
+                            "redis_ssl": true,
+                            "redis_ssl_verify": true
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 14: the certificate is for test.com, so the verified connection is 
refused
+--- stream_enable
+--- request
+GET /hello
+--- error_code: 500
+--- error_log
+certificate host mismatch
+
+
+
+=== TEST 15: limit-count: redis_server_name names the certificate host for 
that DNS alias
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "plugins": {
+                        "limit-count": {
+                            "count": 2,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "remote_addr",
+                            "policy": "redis",
+                            "redis_host": "admin.apisix.dev",
+                            "redis_port": 6396,
+                            "redis_ssl": true,
+                            "redis_ssl_verify": true,
+                            "redis_server_name": "test.com"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 16: the override matches the certificate, the counter works
+--- stream_enable
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 503]

Reply via email to