diff --git a/apisix/plugins/limit-count/util.lua b/apisix/plugins/limit-count/util.lua index 19c1751b2df8..7b66c89e3b79 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 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..8af17db69228 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 3443f86b84af..6018ddf1e75f 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 acf1367a9bb1..4d8757fae9c4 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 f5c1f4979b17..e74e2fc74338 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 a92ecf2db36e..56902f13f187 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 19fc6a6ac47c..c1b938c8e95a 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 59c65d8e547f..eaf771ea3a1d 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 db4d50fc2bbf..794313ff4a0f 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 96c2b8347c86..5bfc13aa33c2 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 85d75a6f1988..f5cba8766886 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 69202ff51c97..2029f3e29b6f 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 91d92b8f68e1..40a3e8aaaffe 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 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-sni.t b/t/utils/redis-sni.t new file mode 100644 index 000000000000..5ad1942bfcd5 --- /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]