ingress-nginx使用limit_except的问题

1、错误的返回-503

起因是ingress-nginx有人提了一个issue:https://github.com/kubernetes/ingress-nginx/issues/11742 ,使用limit_except时没有得到预期的403,而是得到了503,首先看下怎么复现

首先得有一个kubernetes环境,启动一个服务节点foo,然后安装ingress-nginx,ingress的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: foo-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: limit_except GET { deny all; }
nginx.ingress.kubernetes.io/server-snippet: |
location =/ {
return 403;
}
spec:
ingressClassName: nginx
rules:
- host:
http:
paths:
- path: /foo
pathType: Prefix
backend:
service:
name: foo-service
port:
number: 8080

这个配置的含义是,只允许get请求请求foo,其余请求一律为403,比如一个POST请求

1
curl -X POST http://172.xx.xx.xx:32080/foo

按照正常思维来说应该返回403,但是返回了503?

1
2
3
4
5
6
7
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx</center>
</body>
</html>

难道是nginx/openresty的问题吗?于是在openresty快速测试一下,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 5678;
server_name ZJfans.com;

location / {
limit_except GET {
deny all;
}
return 200;
}

location = / {
return 403;
}

location = /foo {
limit_except GET {
deny all;
}
return 200;
}
}

测试的结果,post确实返回了403,所以openresty本身没有问题,那么问题就出在ingress-nginx

openresty测试limit_except

2、ingress的nginx.conf配置

现在来看下ingress生成的nginx.conf有什么特殊的,只看重点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
location = /foo {                                    

set $namespace "default";
set $ingress_name "foo-ingress";
set $service_name "foo-service";
set $service_port "8080";
set $location_path "/foo";
set $global_rate_limit_exceeding n;

rewrite_by_lua_block {
lua_ingress.rewrite({
force_ssl_redirect = false,
ssl_redirect = true,
force_no_ssl_redirect = false,
preserve_trailing_slash = false,
use_port_in_redirects = false,
global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
})
balancer.rewrite()
plugins.run()
}

set $proxy_upstream_name "default-foo-service-8080";

# ...............................................
# ..................忽略海量配置.............................
# ...............................................

limit_except GET { deny all; }


proxy_pass http://upstream_balancer;

proxy_redirect off;

}

测试了一下,我发现balancer.rewrite()是问题的关键,这其实是balancer初始化的一个实例,那么我们来看看这块代码

3、balancer.lua

可以看到这里确实返回了503,那么为什么get_balancer()会失败呢?

1
2
3
4
5
6
7
function _M.rewrite()
local balancer = get_balancer()
if not balancer then
ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE
return ngx.exit(ngx.status)
end
end

现在来看看get_balancer的实现,其实就是,没有获取到balancer返回了nil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local function get_balancer()
if ngx.ctx.balancer then
return ngx.ctx.balancer
end

local backend_name = ngx.var.proxy_upstream_name

local balancer = balancers[backend_name]
if not balancer then
return nil
end

-- ......................省略............................

return balancer
end

现在来看为什么没有获取到balancer,首先使用ngx.say输出一下backend_name,我发现是一个 ”-“,这其实很奇怪,因为nginx.conf是这么设置的

1
set $proxy_upstream_name "default-foo-service-8080";

照理ngx.var.proxy_upstream_name可以取到的值是default-foo-service-8080,那为什么会变成 - ?这又到了openresty的问题了,这时我们需要研究一下limit_except的源码

4、limit_except代码

简单研究一下,直接来看关键的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
ngx_http_update_location_config(ngx_http_request_t *r)
{
ngx_http_core_loc_conf_t *clcf;

clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

if (r->method & clcf->limit_except) {
r->loc_conf = clcf->limit_except_loc_conf;
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
}
// ..................................省略....................................

}

关键在于r->loc_conf = clcf->limit_except_loc_conf;这里切换了r的loc,配置进行了更新,在 Nginx 中,r->loc_conf = clcf->limit_except_loc_conf; 这行代码用于处理特定请求方法时的配置切换。具体来说,当客户端请求的 HTTP 方法不在 limit_except 指定的允许范围内时,Nginx 会将该请求的 loc_conf(location 配置)切换到为该方法配置的 limit_except_loc_conf

举个具体的例子:

假设有如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server {
listen 5678;
server_name ZJfans.com;

location / {
limit_except GET {
deny all;
}
return 200;
}

location = / {
return 403;
}

location /foo {
set $proxy_upstream_name "default-foo-service-8080";

rewrite_by_lua_block {
local upstream_name = ngx.var.proxy_upstream_name
ngx.log(ngx.INFO, "Proxy upstream name: ", upstream_name)
}

limit_except GET {
deny all;
}
}
}

在这个配置中:

  1. 配置结构

    • clcf->limit_except 保存了限制条件,即允许的 HTTP 方法,这里是 GETPOST
    • clcf->limit_except_loc_conf 是一个指向 limit_except 条件下的特定 location 配置结构的指针。
  2. 请求处理

    • 当一个请求到达 /foo 路径时,Nginx 首先会根据请求的方法来检查 clcf->limit_except 中的配置。
    • 如果请求方法是 GET ,则继续使用原始的 loc_conf 处理请求,执行 rewrite_by_lua_block 等其他配置。
    • 如果请求方法是其他方法(如 DELETEPOST),代码中的 if (r->method & clcf->limit_except) 条件会成立,Nginx 会将 r->loc_conf 切换到 clcf->limit_except_loc_conf,即对应限制方法的特定配置。这时,Nginx 可能会直接拒绝请求或应用不同的配置。
  3. 具体行为

    • 当请求方法是 GET 或时,r->loc_conf 保持为原始的配置,执行 rewrite_by_lua_block,可以打印 proxy_upstream_name
    • 当请求方法是 POSTr->loc_conf 切换为 limit_except_loc_conf,此时不再执行 rewrite_by_lua_block,直接应用 deny all,返回 403 错误。

所以如果请求方法是 POST,ngx.var.proxy_upstream_name根本就获取不到值,因为此时配置更新为limit_except的loc,在locatioon设置的变量都无法访问到,nginx会报错using uninitialized,此时问题已经定位到了,这是nginx的限制

获取location设置的变量失败

再深入一下,如果把set $proxy_upstream_name “default-foo-service-8080”;设置为server级别呢,是否可以获取到这个变量?

答案是可以的,因为现在只是替换了location级别的配置,server级别的配置并没有更新,可以正常获取

5、验证–调试balancer.lua

5.1、”-“ 从哪里来的

来看nginx.conf的配置,server里面设置了默认值,所以 - 就是从这里来的

proxy_upstream_name设置初始值为"-"

5.2、balancers结构

回到balancer.lua,我对balancers的结构体挺好奇的,于是打印一下,看下具体的配置

1、查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ocal function get_balancer()                                      
if ngx.ctx.balancer then
return ngx.ctx.balancer
end

local backend_name = ngx.var.proxy_upstream_name

---------------------------------------begin---------------------------------
backend_name = "default-foo-service-8080" --设置正确的值

local balancer = balancers[backend_name]

if balancer then --返回数据结构
local balancer_json = cjson.encode(balancer)
ngx.header["Content-Type"] = "application/json"
ngx.status = ngx.HTTP_OK
ngx.say(balancer_json)
end
---------------------------------------end-----------------------------------
if not balancer then
return nil
end

请求/响应

1
2
3
curl -X POST http://172.xx.xx.xx:32080/foo

{"traffic_shaping_policy":{"weight":0,"cookie":"","headerPattern":"","headerValue":"","header":"","weightTotal":0},"instance":{"nodes":{"10.233.xx.xx:5678":1},"gcd":1,"only_key":"10.233.xx.xx:5678","cw":1,"last_id":"10.233.xx.xx:5678","max_weight":1}}

所以如果正常能取到proxy_upstream_name的值,也是能取到balancer的

6、如何修复

按照我的初步想法,简单粗暴,在proxy_upstream_name没有被重新赋值时,直接返回403。因为使用了limit_except 后,location设置的proxy_upstream_name将无法被获取,肯定为 - ,所以可以认为只要是 -,就是使用了limit_except?返回403也比较合理。

唯一的问题是,会不会有其他指令,也会导致这个情况,但是他需要返回其他的状态码,这个暂时还没想到有,nginx的指令很多,需要慢慢确认

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local function get_balancer()
if ngx.ctx.balancer then
return ngx.ctx.balancer
end

local backend_name = ngx.var.proxy_upstream_name
---------------------------------------begin---------------------------------
if backend_name == '-' then
ngx.status = ngx.HTTP_FORBIDDEN
return ngx.exit(ngx.status)
end
---------------------------------------end-----------------------------------
local balancer = balancers[backend_name]
if not balancer then
return nil
end
-- ......................省略............................
return balancer
end

已经向社区反馈该问题的进展,结果后续会更新


ingress-nginx使用limit_except的问题
https://zjfans.github.io/2024/08/25/Problems with ingress-nginx using limit_except/
作者
张三疯
发布于
2024年8月25日
许可协议