From 5e479627f1274f15b89dfff297f6eb4179e9a8fc Mon Sep 17 00:00:00 2001 From: Arjen10 Date: Mon, 14 Sep 2026 12:37:41 +0800 Subject: [PATCH 1/4] fix(redis): add redis_server_name configuration --- apisix/plugins/limit-count/util.lua | 6 +- apisix/utils/redis-schema.lua | 3 + apisix/utils/redis.lua | 6 +- 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.t | 225 ++++++++++++++++++ 16 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 t/utils/redis.t diff --git a/apisix/plugins/limit-count/util.lua b/apisix/plugins/limit-count/util.lua index 19c1751b2df8..06f334f94785 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,15 @@ 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 sock_opts = { ssl = conf.redis_ssl, ssl_verify = conf.redis_ssl_verify, pool = pool, + server_name = conf.redis_ssl and (conf.redis_server_name or conf.redis_host) or nil, } 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 991919a2a9d5..b722d5b8f649 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 5634185925d2..7457e1d3901b 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,15 @@ 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 sock_opts = { ssl = conf.redis_ssl, ssl_verify = conf.redis_ssl_verify, pool = pool, + server_name = conf.redis_ssl and (conf.redis_server_name or conf.redis_host) or nil, } 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 3443f86b84af..b4fe864b78c7 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`. | | 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 acf1367a9bb1..3f50c07a179d 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`. | | 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 f5c1f4979b17..07b6160aa234 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`. | | 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 a92ecf2db36e..08a831690cc4 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`. | | 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 19fc6a6ac47c..b958820516ba 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`. | | 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 59c65d8e547f..5a7c5f090d49 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`. | | 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 db4d50fc2bbf..6eed3d4f767c 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_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 96c2b8347c86..07312e6e2b75 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_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 85d75a6f1988..4efa02a65ee3 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_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 69202ff51c97..89fe15a60c7a 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_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 91d92b8f68e1..23a723c8a5e7 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_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 493aedcb9446..7557efee7cc6 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.t b/t/utils/redis.t new file mode 100644 index 000000000000..569f09b72c39 --- /dev/null +++ b/t/utils/redis.t @@ -0,0 +1,225 @@ +# +# 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_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: TLS SNI is redis_server_name or redis_host when redis_ssl is enabled +--- config + location /t { + content_by_lua_block { + local last_opts + local orig_redis = package.loaded["resty.redis"] + package.loaded["resty.redis"] = { + new = function() + return { + set_timeouts = function() end, + connect = function(_, host, port, opts) + last_opts = opts + return true + end, + get_reused_times = function() + return 1 + end, + } + end, + } + + local function reload(name) + package.loaded[name] = nil + return require(name) + end + + local cases = { + { + conf = {redis_host = "127.0.0.1"}, + expect = "nil", + }, + { + conf = {redis_host = "redis.example.com", redis_ssl = true}, + expect = "redis.example.com", + }, + { + conf = { + redis_host = "10.0.0.1", + redis_ssl = true, + redis_server_name = "redis.example.com", + }, + expect = "redis.example.com", + }, + { + conf = { + redis_host = "redis.example.com", + redis_ssl = false, + redis_server_name = "ignored.example.com", + }, + expect = "nil", + }, + } + + local function run(label, connect) + for _, case in ipairs(cases) do + last_opts = nil + connect(case.conf) + local got = last_opts and last_opts.server_name or "nil" + if got ~= case.expect then + ngx.say(label, " want ", case.expect, " got ", got) + return false + end + ngx.say(label, ": ", got) + end + return true + end + + local redis = reload("apisix.utils.redis") + if not run("utils", function(conf) redis.new(conf) end) then + return + end + + local util = reload("apisix.plugins.limit-count.util") + run("util", function(conf) util.redis_cli(conf) end) + + package.loaded["resty.redis"] = orig_redis + package.loaded["apisix.utils.redis"] = nil + package.loaded["apisix.plugins.limit-count.util"] = nil + } + } +--- response_body +utils: nil +utils: redis.example.com +utils: redis.example.com +utils: nil +util: nil +util: redis.example.com +util: redis.example.com +util: nil + + + +=== TEST 2: TLS SNI is part of the keepalive pool name +--- config + location /t { + content_by_lua_block { + local last_opts + local orig_redis = package.loaded["resty.redis"] + package.loaded["resty.redis"] = { + new = function() + return { + set_timeouts = function() end, + connect = function(_, host, port, opts) + last_opts = opts + return true + end, + get_reused_times = function() + return 1 + end, + } + end, + } + + local function reload(name) + package.loaded[name] = nil + return require(name) + end + + local cases = { + { + conf = {redis_host = "127.0.0.1"}, + expect = "redis#127.0.0.1#6379#0", + }, + { + conf = {redis_host = "redis.example.com", redis_ssl = true}, + expect = "rediss#redis.example.com#6379#0", + }, + { + conf = { + redis_host = "10.0.0.1", + redis_ssl = true, + redis_server_name = "redis.example.com", + }, + expect = "rediss#10.0.0.1#6379#0#redis.example.com", + }, + { + conf = { + redis_host = "10.0.0.1", + redis_ssl = true, + redis_server_name = "other.example.com", + }, + expect = "rediss#10.0.0.1#6379#0#other.example.com", + }, + { + conf = { + redis_host = "redis.example.com", + redis_ssl = false, + redis_server_name = "ignored.example.com", + }, + expect = "redis#redis.example.com#6379#0", + }, + } + + local function run(label, connect) + for _, case in ipairs(cases) do + last_opts = nil + connect(case.conf) + local got = last_opts and last_opts.pool or "nil" + if got ~= case.expect then + ngx.say(label, " want ", case.expect, " got ", got) + return false + end + ngx.say(label, ": ", got) + end + return true + end + + local redis = reload("apisix.utils.redis") + if not run("utils", function(conf) redis.new(conf) end) then + return + end + + local util = reload("apisix.plugins.limit-count.util") + run("util", function(conf) util.redis_cli(conf) end) + + package.loaded["resty.redis"] = orig_redis + package.loaded["apisix.utils.redis"] = nil + package.loaded["apisix.plugins.limit-count.util"] = nil + } + } +--- response_body +utils: redis#127.0.0.1#6379#0 +utils: rediss#redis.example.com#6379#0 +utils: rediss#10.0.0.1#6379#0#redis.example.com +utils: rediss#10.0.0.1#6379#0#other.example.com +utils: redis#redis.example.com#6379#0 +util: redis#127.0.0.1#6379#0 +util: rediss#redis.example.com#6379#0 +util: rediss#10.0.0.1#6379#0#redis.example.com +util: rediss#10.0.0.1#6379#0#other.example.com +util: redis#redis.example.com#6379#0 From a0f89476514dc686e06583b6d6bd1248013163ae Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Mon, 14 Sep 2026 12:59:59 +0800 Subject: [PATCH 2/4] fix(redis): skip the SNI for IP literal hosts and test it end to end --- apisix/plugins/limit-count/util.lua | 10 +- apisix/utils/redis.lua | 10 +- t/utils/redis-sni.t | 376 ++++++++++++++++++++++++++++ t/utils/redis.t | 225 ----------------- 4 files changed, 394 insertions(+), 227 deletions(-) create mode 100644 t/utils/redis-sni.t delete mode 100644 t/utils/redis.t diff --git a/apisix/plugins/limit-count/util.lua b/apisix/plugins/limit-count/util.lua index 06f334f94785..7b66c89e3b79 100644 --- a/apisix/plugins/limit-count/util.lua +++ b/apisix/plugins/limit-count/util.lua @@ -60,11 +60,19 @@ function _M.redis_cli(conf) 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 = conf.redis_ssl and (conf.redis_server_name or conf.redis_host) or nil, + 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.lua b/apisix/utils/redis.lua index 7457e1d3901b..8af17db69228 100644 --- a/apisix/utils/redis.lua +++ b/apisix/utils/redis.lua @@ -46,11 +46,19 @@ local function redis_cli(conf) 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 = conf.redis_ssl and (conf.redis_server_name or conf.redis_host) or nil, + server_name = server_name, } local ok, err = red:connect(conf.redis_host, conf.redis_port or 6379, sock_opts) diff --git a/t/utils/redis-sni.t b/t/utils/redis-sni.t new file mode 100644 index 000000000000..322490880047 --- /dev/null +++ b/t/utils/redis-sni.t @@ -0,0 +1,376 @@ +# +# 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] diff --git a/t/utils/redis.t b/t/utils/redis.t deleted file mode 100644 index 569f09b72c39..000000000000 --- a/t/utils/redis.t +++ /dev/null @@ -1,225 +0,0 @@ -# -# 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_root_location(); - -add_block_preprocessor(sub { - my ($block) = @_; - - if (!defined $block->request) { - $block->set_value("request", "GET /t"); - } -}); - -run_tests(); - -__DATA__ - -=== TEST 1: TLS SNI is redis_server_name or redis_host when redis_ssl is enabled ---- config - location /t { - content_by_lua_block { - local last_opts - local orig_redis = package.loaded["resty.redis"] - package.loaded["resty.redis"] = { - new = function() - return { - set_timeouts = function() end, - connect = function(_, host, port, opts) - last_opts = opts - return true - end, - get_reused_times = function() - return 1 - end, - } - end, - } - - local function reload(name) - package.loaded[name] = nil - return require(name) - end - - local cases = { - { - conf = {redis_host = "127.0.0.1"}, - expect = "nil", - }, - { - conf = {redis_host = "redis.example.com", redis_ssl = true}, - expect = "redis.example.com", - }, - { - conf = { - redis_host = "10.0.0.1", - redis_ssl = true, - redis_server_name = "redis.example.com", - }, - expect = "redis.example.com", - }, - { - conf = { - redis_host = "redis.example.com", - redis_ssl = false, - redis_server_name = "ignored.example.com", - }, - expect = "nil", - }, - } - - local function run(label, connect) - for _, case in ipairs(cases) do - last_opts = nil - connect(case.conf) - local got = last_opts and last_opts.server_name or "nil" - if got ~= case.expect then - ngx.say(label, " want ", case.expect, " got ", got) - return false - end - ngx.say(label, ": ", got) - end - return true - end - - local redis = reload("apisix.utils.redis") - if not run("utils", function(conf) redis.new(conf) end) then - return - end - - local util = reload("apisix.plugins.limit-count.util") - run("util", function(conf) util.redis_cli(conf) end) - - package.loaded["resty.redis"] = orig_redis - package.loaded["apisix.utils.redis"] = nil - package.loaded["apisix.plugins.limit-count.util"] = nil - } - } ---- response_body -utils: nil -utils: redis.example.com -utils: redis.example.com -utils: nil -util: nil -util: redis.example.com -util: redis.example.com -util: nil - - - -=== TEST 2: TLS SNI is part of the keepalive pool name ---- config - location /t { - content_by_lua_block { - local last_opts - local orig_redis = package.loaded["resty.redis"] - package.loaded["resty.redis"] = { - new = function() - return { - set_timeouts = function() end, - connect = function(_, host, port, opts) - last_opts = opts - return true - end, - get_reused_times = function() - return 1 - end, - } - end, - } - - local function reload(name) - package.loaded[name] = nil - return require(name) - end - - local cases = { - { - conf = {redis_host = "127.0.0.1"}, - expect = "redis#127.0.0.1#6379#0", - }, - { - conf = {redis_host = "redis.example.com", redis_ssl = true}, - expect = "rediss#redis.example.com#6379#0", - }, - { - conf = { - redis_host = "10.0.0.1", - redis_ssl = true, - redis_server_name = "redis.example.com", - }, - expect = "rediss#10.0.0.1#6379#0#redis.example.com", - }, - { - conf = { - redis_host = "10.0.0.1", - redis_ssl = true, - redis_server_name = "other.example.com", - }, - expect = "rediss#10.0.0.1#6379#0#other.example.com", - }, - { - conf = { - redis_host = "redis.example.com", - redis_ssl = false, - redis_server_name = "ignored.example.com", - }, - expect = "redis#redis.example.com#6379#0", - }, - } - - local function run(label, connect) - for _, case in ipairs(cases) do - last_opts = nil - connect(case.conf) - local got = last_opts and last_opts.pool or "nil" - if got ~= case.expect then - ngx.say(label, " want ", case.expect, " got ", got) - return false - end - ngx.say(label, ": ", got) - end - return true - end - - local redis = reload("apisix.utils.redis") - if not run("utils", function(conf) redis.new(conf) end) then - return - end - - local util = reload("apisix.plugins.limit-count.util") - run("util", function(conf) util.redis_cli(conf) end) - - package.loaded["resty.redis"] = orig_redis - package.loaded["apisix.utils.redis"] = nil - package.loaded["apisix.plugins.limit-count.util"] = nil - } - } ---- response_body -utils: redis#127.0.0.1#6379#0 -utils: rediss#redis.example.com#6379#0 -utils: rediss#10.0.0.1#6379#0#redis.example.com -utils: rediss#10.0.0.1#6379#0#other.example.com -utils: redis#redis.example.com#6379#0 -util: redis#127.0.0.1#6379#0 -util: rediss#redis.example.com#6379#0 -util: rediss#10.0.0.1#6379#0#redis.example.com -util: rediss#10.0.0.1#6379#0#other.example.com -util: redis#redis.example.com#6379#0 From 540e78dc7c26b27e2263f231741b918811268c46 Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Tue, 15 Sep 2026 12:14:25 +0800 Subject: [PATCH 3/4] test(redis): cover the hostname check for a DNS alias and document redis_server_name --- docs/en/latest/plugins/ai-cache.md | 2 +- docs/en/latest/plugins/ai-rate-limiting.md | 2 +- docs/en/latest/plugins/graphql-limit-count.md | 2 +- docs/en/latest/plugins/limit-conn.md | 2 +- docs/en/latest/plugins/limit-count.md | 2 +- docs/en/latest/plugins/limit-req.md | 2 +- docs/zh/latest/plugins/ai-cache.md | 2 +- docs/zh/latest/plugins/graphql-limit-count.md | 2 +- docs/zh/latest/plugins/limit-conn.md | 2 +- docs/zh/latest/plugins/limit-count.md | 2 +- docs/zh/latest/plugins/limit-req.md | 2 +- t/utils/redis-sni.t | 102 ++++++++++++++++++ 12 files changed, 113 insertions(+), 11 deletions(-) diff --git a/docs/en/latest/plugins/ai-cache.md b/docs/en/latest/plugins/ai-cache.md index b4fe864b78c7..6018ddf1e75f 100644 --- a/docs/en/latest/plugins/ai-cache.md +++ b/docs/en/latest/plugins/ai-cache.md @@ -89,7 +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`. | +| 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 3f50c07a179d..4d8757fae9c4 100644 --- a/docs/en/latest/plugins/ai-rate-limiting.md +++ b/docs/en/latest/plugins/ai-rate-limiting.md @@ -65,7 +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`. | +| 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 07b6160aa234..e74e2fc74338 100644 --- a/docs/en/latest/plugins/graphql-limit-count.md +++ b/docs/en/latest/plugins/graphql-limit-count.md @@ -75,7 +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`. | +| 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 08a831690cc4..56902f13f187 100644 --- a/docs/en/latest/plugins/limit-conn.md +++ b/docs/en/latest/plugins/limit-conn.md @@ -57,7 +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`. | +| 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 b958820516ba..c1b938c8e95a 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -81,7 +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`. | +| 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 5a7c5f090d49..eaf771ea3a1d 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -64,7 +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`. | +| 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 6eed3d4f767c..52f5c6d41e2e 100644 --- a/docs/zh/latest/plugins/ai-cache.md +++ b/docs/zh/latest/plugins/ai-cache.md @@ -89,7 +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_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 07312e6e2b75..117bc92eba75 100644 --- a/docs/zh/latest/plugins/graphql-limit-count.md +++ b/docs/zh/latest/plugins/graphql-limit-count.md @@ -70,7 +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_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 4efa02a65ee3..bf3488594522 100644 --- a/docs/zh/latest/plugins/limit-conn.md +++ b/docs/zh/latest/plugins/limit-conn.md @@ -57,7 +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_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 89fe15a60c7a..a498bf38e337 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -82,7 +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_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 23a723c8a5e7..28e6303e1428 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -64,7 +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_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-sni.t b/t/utils/redis-sni.t index 322490880047..5ad1942bfcd5 100644 --- a/t/utils/redis-sni.t +++ b/t/utils/redis-sni.t @@ -374,3 +374,105 @@ passed [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] From dfd06807d63aee091654562459bf4fd7f0f4f3ca Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Tue, 15 Sep 2026 12:44:35 +0800 Subject: [PATCH 4/4] docs: drop the space before the Chinese sentence for autocorrect --- docs/zh/latest/plugins/ai-cache.md | 2 +- docs/zh/latest/plugins/graphql-limit-count.md | 2 +- docs/zh/latest/plugins/limit-conn.md | 2 +- docs/zh/latest/plugins/limit-count.md | 2 +- docs/zh/latest/plugins/limit-req.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/zh/latest/plugins/ai-cache.md b/docs/zh/latest/plugins/ai-cache.md index 52f5c6d41e2e..794313ff4a0f 100644 --- a/docs/zh/latest/plugins/ai-cache.md +++ b/docs/zh/latest/plugins/ai-cache.md @@ -89,7 +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_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 117bc92eba75..5bfc13aa33c2 100644 --- a/docs/zh/latest/plugins/graphql-limit-count.md +++ b/docs/zh/latest/plugins/graphql-limit-count.md @@ -70,7 +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_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 bf3488594522..f5cba8766886 100644 --- a/docs/zh/latest/plugins/limit-conn.md +++ b/docs/zh/latest/plugins/limit-conn.md @@ -57,7 +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_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 a498bf38e337..2029f3e29b6f 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -82,7 +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_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 28e6303e1428..40a3e8aaaffe 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -64,7 +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_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 的空闲连接超时时间,单位为毫秒。|