orange网关传统集群部署模式

1、在orange.conf的 plugins中加入node,表示开启node插件(容器集群节点管理插件

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
    "plugins": [
"stat",
"headers",
"monitor",
"redirect",
"rewrite",
"rate_limiting",
"property_rate_limiting",
"basic_auth",
"key_auth",
"jwt_auth",
"hmac_auth",
"signature_auth",
"waf",
"divide",
"kvstore",
"node"
],
...
"api": {
"auth_enable": true,
"credentials": [
{
"username":"api_username",
"password":"api_password"
}
]
}

2、部署多个orange节点,同时在每个节点的dashboard启用node插件
3、每个节点dashboard,集群管理中先注册节点添加自己,然后添加节点依次加入其它节点,需要输入其他节点的 ip 端口(7777) 用户名 和 密码

4、A节点的dashboard修改完配置, 在其他节点的dashboard上点击同步配置进行同步

orange多节点主要是同步各节点的数据,这样的话只要在一个节点做配置,其他节点都能同步到

传统模式在k8s的环境下面临几个问题

  1. 在无状态的部署下存在一些问题,如下(下图部署两个副本进行测试,注意图中的Address,访问后台时默认处于轮询状态)
  1. Orange多节点之间以ip进行通信,但orange的dashboard也对添加/修改集群节点的输入框做了限制(只能写ip,所以这里还需要对源码以及数据库字段长度进行调整)
  1. Orange的默认使用lua_shared_dict缓存数据(其中包括插件的配置数据),所以有以下几个问题

    • dashboard配置完插件后只在当前节点具有数据可见性,其他节点必须点击同步配置,才可生效新配置。这就导致网关服务默认只能以有状态方式部署
    • OpenResty/Nginx 的共享内存区是消耗物理内存的:意味着它存储的数据并不是保存在文件系统中的,而是直接存储在操作系统的内存中。因此,共享内存中的数据是暂时性的,一旦Nginx进程退出或重启,共享内存中的数据就会被清空。所以在容器无法挂载这部分数据,也无法持久化这部分数据。参考:https://blog.openresty.com.cn/cn/how-nginx-shm-consume-ram/
  2. 关于共享内存这里,计划全改成Redis,但是也遇到了几个问题

    • 有些缓存操作是放在init_by_lualog_by_lua中的,但是这类模块不允许操作redis。如果操作对应api会报错: API disabled in the context of xx_by_lua。只有在 content_by_lua 上下文中才能使用 resty.redis 模块,因为这是在响应体生成之前执行的代码。

      解决办法:如果一定要在这当中执行redis操作,可以在调用Redis模块之前,使用ngx.timer.at(delay or 0, function() ... end)将代码延迟到下一个请求周期中执行,这样就可以避免在*_by_lua上下文中使用Redis模块了。

      但设置被允许的running timers(正在执行回调函数的计时器)的最大数量默认为256,如果超过这个数量,就会抛出**lua failed to run timer with function defined at ... N lua_max_running_timers are not enough**,其中N是变量,指的是当前正在运行的running timers的最大数量。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      -- 在log_by_lua*上下文中使用ngx.timer.at延迟执行Redis操作
      -- * delay=0时,表示立即执行任务,并且只执行一次
      -- * delay不为0时,表示每隔delay秒执行一次任务
      ngx.timer.at(delay or 0, function()
      local redis = require "resty.redis"
      local red = redis:new()
      local ok, err = red:connect("127.0.0.1", 6379)
      if not ok then
      ngx.log(ngx.ERR, "failed to connect to Redis: ", err)
      return
      end
      -- 执行Redis操作
      local res, err = red:get("mykey")
      if not res then
      ngx.log(ngx.ERR, "failed to get Redis key: ", err)
      return
      end
      ngx.log(ngx.INFO, "Redis key value: ", res)
      red:set_keepalive(10000, 100)
      end)
    • orange的全局统计插件中的部分数据如活跃连接数,这里是取自当前网关节点的ngx.var.connections_active,这是个动态值。与此相似的还有其他数据。这类监控数据不适合定性存储

    • 涉及到lua_shared_dict orange_data的代码地方较多。但orange_data仅供缓存插件配置,可以不做持久化。

      数据加载流程:网关服务启动 -> 从数据库加载插件数据 -> 缓存至orange_data

开发/改造

针对0.8.1分支进行改造

代码见: GitHub - behappy-other/orange at v0.8.1-k8s

luarocks依赖版本调整

  • orange-master-0.rockspec

lua-resty-kafka -> 0.09-0

luafilesystem -> 1.8.0-1

数据库字段长度调整

1
2
3
4
5
6
7
8
alter table cluster_node
modify name varchar(255) default '' not null;

alter table cluster_node
modify ip varchar(255) default '' not null comment 'ip';

alter table persist_log
modify ip varchar(255) default '' not null comment 'ip';

注册节点IP逻辑调整

  • 去掉注册节点时的ip限制
  • nginx.conf.example中添加env ORANGE_SERVICE;(默认值为orange-headless),后续将使用socket.dns.gethostname() .. "." .. os.getenv("ORANGE_SERVICE")做为注册ip(k8s中,结合statefulset + headless service可将流量打在指定pod)
  • orange.conf.example中添加node插件

配置分流,及DNS解析

  • 配置nginx.conf.example,添加kube-dns,使k8s环境下可以解析对应service name
1
2
3
4
5
http {
# 修改kube-dns, 使k8s环境下可以解析对应service name
# resolver 114.114.114.114; # replace it with your favorite config
resolver kube-dns.kube-system.svc.cluster.local;
...
  • 添加server,按**Header:oranger-versiondashboard**进行分流

通常情况下,Kubernetes集群中的DNS服务器(kube-dns或CoreDNS)会自动为服务名称添加完整的域名(例如service.{namespace}.svc.cluster.local)。但是,如果在Nginx配置文件中指定了其他DNS服务器,则需要自行添加完整的服务名称。

例如,如果在Nginx配置文件中指定了 第三方 DNS服务器(8.8.8.8),则需要在proxy_pass指令中使用完整的服务名称,例如http://service.{namespace}.svc.cluster.local:port

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# orange dashboard fronted
server {
listen 8888;
access_log off;
error_log off;

location / {
# 设置默认路由地址.orange-0为pod名称,同时也是该pod的hostname,格式为:orange-{n}
set $backend "http://orange-0.orange-headless.default.svc.cluster.local:9999";
# 检查标头
if ($http_orange_version) {
set $backend "http://${http_orange_version}.orange-headless.default.svc.cluster.local:9999";
}
# 路由到dashboard的后端服务
proxy_pass $backend;
}
}
  • 获取真实remote ip
1
2
3
4
5
6
7
8
9
10
11
http {
......
# update remote-addr/real-ip
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

map $http_upgrade $connection_upgrade {
websocket upgrade;
default $http_connection;
}
  • 解决426 upgrade required问题:nginx反向代理默认走的http 1.0版本
1
2
3
4
http {
......
# 解决426问题:nginx反向代理默认走的http 1.0版本
proxy_http_version 1.1;

调整使用到lua_shared_dict的插件,将其改成Redis

  • rate_limiting
  • monitor
  • waf
  • stat
  • property_rate_limiting

调整Makefile,改良服务的启动速度

将下载依赖的步骤单独提出来,之后只要rockspec文件内容不变,就不需要额外下载依赖

  • 去除make dev操作中的luarocks install ...指令
  • 新增make dependency指令
1
2
3
4
5
6
7
8
### dependency:          install the dependencies from .rockspec
.PHONY: dependency
dependency:
ifneq ($(LUAROCKS_VER),'luarocks 3')
luarocks install rockspec/orange-master-0.rockspec --server=https://luarocks.cn --tree=deps --only-deps --local
else
luarocks install --server=https://luarocks.cn --lua-dir=/usr/local/openresty/luajit rockspec/orange-master-0.rockspec --tree=deps --only-deps --local
endif

property_rate_limiting插件 - 防刷功能添加封禁时长

  • 未配置封禁时间,则按429处理 -> 限流
  • 配置了封禁时间,则按403处理 -> 指定时间内不可访问

Dockerfile

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
FROM centos:7
WORKDIR /opt/orange
EXPOSE 80 7777 8888 9999
RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& yum update -y \
&& yum install -y wget \
# install epel, `luarocks` need it.
&& wget http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
&& rpm -ivh epel-release-latest-7.noarch.rpm \
# install some compilation tools
&& yum install -y yum-utils git libuuid-devel pcre-devel openssl-devel gcc gcc-c++ make perl-Digest-MD5 lua-devel cmake3 curl libtool autoconf automake openresty-resty readline-devel unzip gettext kde-l10n-Chinese which net-tools \
&& yum -y reinstall glibc-common \
&& ln -s /usr/bin/cmake3 /usr/bin/cmake \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.utf8
ENV LC_ALL zh_CN.utf8
RUN cd /usr/local/src \
&& git clone https://gitee.com/xiaowu_wang/lor.git \
# install lor
&& cd lor \
&& make install
RUN cd /usr/local/src \
# install luarocks
&& git clone https://gitee.com/xiaowu_wang/luarocks.git \
&& cd luarocks \
&& git checkout v3.9.2 \
&& ./configure --prefix=/usr/local/luarocks --with-lua=/usr --with-lua-include=/usr/include \
&& make \
&& make install \
&& ln -s /usr/local/luarocks/bin/luarocks /usr/local/bin/luarocks
RUN cd /usr/local/src \
# install openresty,sticky and compile
&& wget https://openresty.org/download/openresty-1.21.4.1.tar.gz \
&& wget https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/get/08a395c66e42.zip -O ./nginx-goodies-nginx-sticky-module-ng-08a395c66e42.zip \
&& tar -xzvf openresty-1.21.4.1.tar.gz \
&& unzip -D nginx-goodies-nginx-sticky-module-ng-08a395c66e42.zip \
&& mv nginx-goodies-nginx-sticky-module-ng-08a395c66e42 openresty-1.21.4.1/nginx-sticky-module-ng \
&& cd openresty-1.21.4.1 \
&& ./configure --prefix=/usr/local/openresty --with-http_stub_status_module --with-http_v2_module --with-http_ssl_module --with-http_realip_module --add-module=./nginx-sticky-module-ng \
&& make \
&& make install \
&& ln -s /usr/local/openresty/nginx/sbin/nginx /usr/bin/nginx \
&& ln -s /usr/local/openresty/bin/openresty /usr/bin/openresty \
&& ln -s /usr/local/openresty/bin/resty /usr/bin/resty \
&& ln -s /usr/local/openresty/bin/opm /usr/bin/opm \
&& openresty
# 提前构建
COPY rockspec ./rockspec/
COPY Makefile ./Makefile
RUN make dependency
COPY . .
# 这里需要sleep几秒,不然会在init_by_lua阶段,执行初始化操作时报错连接不上mysql
CMD make dev && make install && sleep 10 && resty bin/orange start && tail -f /opt/orange/logs/access.log

Statefulset.yaml

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
apiVersion: v1
kind: Service
metadata:
name: orange-headless
spec:
publishNotReadyAddresses: false
clusterIP: None
selector:
app: orange
---
apiVersion: v1
kind: Service
metadata:
name: orange
spec:
selector:
app: orange
ports:
- port: 80
name: http-web
targetPort: 80
nodePort: 35080
appProtocol: HTTP
- port: 7777
name: http-api
targetPort: 7777
nodePort: 35077
appProtocol: HTTP
- port: 8888
name: http-fronted
targetPort: 8888
nodePort: 35088
appProtocol: HTTP
- port: 9999
name: http-admin
targetPort: 9999
appProtocol: HTTP
type: NodePort
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: orange
labels:
app: orange
spec:
serviceName: orange-headless
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
replicas: 2
selector:
matchLabels:
app: orange
template:
metadata:
labels:
app: orange
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: "kubernetes.io/hostname"
namespaces:
- default
labelSelector:
matchLabels:
app: orange
weight: 100
containers:
- name: orange
image: $REGISTRY_ADDRESS/${NODE_ENV}/${CI_PROJECT_NAME}:v${CI_PIPELINE_ID}
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "while [ $(netstat -plunt | grep tcp | wc -l | xargs) -ne 0 ]; do sleep 1; done"
livenessProbe:
tcpSocket:
port: 80
readinessProbe:
tcpSocket:
port: 80
env:
- name: ORANGE_SERVICE
value: orange-headless
ports:
- containerPort: 80
- containerPort: 7777
- containerPort: 8888
- containerPort: 9999
dnsPolicy: ClusterFirst
restartPolicy: Always

测试

多副本

扩容测试

新副本可以在10秒内启动成功

访问测试

多次访问,呈轮询效果

控制台

访问8888端口,根据自定义header:orange-version,将流量打入指定pod上

节点注册

  • 添加自定义header
  • 查看全局统计(此处如果传的header:orange-versionorange-1,则Address呈现为orange-1
  • 分别注册orange-0orange-1节点

配置同步

  • orange-0节点配置防刷插件
  • 回到orange-1,进行同步
  • 同步成功

防刷测试

  • 配置dashboard,限制ip :一小时10次,封禁120秒

Jmeter压测

测试从100线程开始

观察2xx4xx,以及conn active(活跃连接数)

活跃连接数维持在100上下

在单节点抗压测试下,orange是可以有效防止因压力过大导致崩溃的问题的

使用专业工具LOIC进行DDos攻击

测试从100线程开始,等待结果响应再发送下一次。看idle空闲线程始终有剩余,说明没跑满

观察2xx4xx,以及conn active(活跃连接数)

虽然是100线程,但LOIC的工作原理是向目标服务器发送大量 TCPUDPHTTP 数据包以中断服务,所以看到的活跃连接数要远高于100

产生的问题

redis压力

随着压力再加大,redis连接开始逐渐崩掉。所以需要考虑压力更大的情况下,单机redis是否能撑得住。如果和业务模块共用一个redis,会不会拖垮整个业务系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2023/06/27 18:15:45 [error] 20172#20172: *154221 lua tcp socket connect timed out, when connecting to 192.168.56.100:6379, context: ngx.timer, client: 192.168.56.1, server: 0.0.0.0:80
2023/06/27 18:15:45 [error] 20172#20172: *154221 lua entry thread aborted: runtime error: /opt/orange/orange//orange/utils/redis.lua:102: API disabled in the context of ngx.timer
stack traceback:
coroutine 0:
[C]: in function 'say'
/opt/orange/orange//orange/utils/redis.lua:102: in function 'connect_mod'
/opt/orange/orange//orange/utils/redis.lua:195: in function 'get'
/opt/orange/orange//orange/plugins/base_redis.lua:32: in function 'get_string'
/opt/orange/orange//orange/plugins/stat/stat.lua:25: in function 'init'
/opt/orange/orange//orange/plugins/stat/stat.lua:55: in function </opt/orange/orange//orange/plugins/stat/stat.lua:53>, context: ngx.timer, client: 192.168.56.1, server: 0.0.0.0:80
2023/06/27 18:15:45 [error] 20172#20172: *154228 lua tcp socket connect timed out, when connecting to 192.168.56.100:6379, context: ngx.timer, client: 192.168.56.1, server: 0.0.0.0:80
2023/06/27 18:15:45 [error] 20172#20172: *154228 lua entry thread aborted: runtime error: /opt/orange/orange//orange/utils/redis.lua:102: API disabled in the context of ngx.timer
stack traceback:
coroutine 0:
[C]: in function 'say'
/opt/orange/orange//orange/utils/redis.lua:102: in function 'connect_mod'
/opt/orange/orange//orange/utils/redis.lua:195: in function 'get'
/opt/orange/orange//orange/plugins/base_redis.lua:32: in function 'get_string'
/opt/orange/orange//orange/plugins/stat/stat.lua:25: in function 'init'
/opt/orange/orange//orange/plugins/stat/stat.lua:55: in function </opt/orange/orange//orange/plugins/stat/stat.lua:53>, context: ngx.timer, client: 192.168.56.1, server: 0.0.0.0:80
2023/06/27 18:15:45 [error] 20172#20172: *154233 lua tcp socket connect timed out, when connecting to 192.168.56.100:6379, context: ngx.timer, client: 192.168.56.1, server: 0.0.0.0:80
2023/06/27 18:15:45 [error] 20172#20172: *154233 lua entry thread aborted: runtime error: /opt/orange/orange//orange/utils/redis.lua:102: API disabled in the context of ngx.timer

work_connections are not enough

nginx默认worker_connections(单个工作进程可以允许同时建立外部连接的数量) 为 4096,数字越大,能同时处理的连接越多,对应需要的资源也越高

这里修改默认值为512,测试300线程,再进行DDos攻击

活跃连接数已经来到1000

日志开始出现大量512 worker_connections are not enough错误

1
2
3
4
5
2023/06/28 09:47:21 [alert] 7829#7829: *51849 512 worker_connections are not enough, client: 192.168.56.1, server: , request: "GET / HTTP/1.0"
2023/06/28 09:47:21 [alert] 7829#7829: *51849 512 worker_connections are not enough, client: 192.168.56.1, server: , request: "GET / HTTP/1.0"
2023/06/28 09:47:21 [alert] 7829#7829: *51849 512 worker_connections are not enough, client: 192.168.56.1, server: , request: "GET / HTTP/1.0"
2023/06/28 09:47:21 [alert] 7829#7829: *51849 512 worker_connections are not enough while connecting to upstream, client: 192.168.56.1, server: , request: "GET / HTTP/1.0", upstream: "http://[::1]:8001/"
2023/06/28 09:47:21 [alert] 7829#7829: *51859 512 worker_connections are not enough, context: ngx.timer, client: 192.168.56.1, server: 0.0.0.0:80

TODO

其他插件的DDos模拟测试

结论

Orange可以抵挡高并发场景下的冲击

但依靠Orange抵挡专业的DDos攻击,还需要其他的办法

插件开发补充

ngx_lua_waf --- Nginx防火墙

ngx_lua_waf 是一个高性能的轻量级 web 应用防火墙,基于 lua-nginx-module。

它具有以下功能:

  • 防止sql注入,本地包含,部分溢出,fuzzing测试,xss,SSRF等web攻击
  • 防止svn/备份之类文件泄漏
  • 防止ApacheBench之类压力测试工具的攻击
  • 屏蔽常见的扫描黑客工具,扫描器
  • 屏蔽异常的网络请求
  • 屏蔽图片附件类目录php执行权限
  • 防止webshell上传

经过 unixhot 的修改和重构,拥有了以下功能:

  • 支持IP白名单和黑名单功能,直接将黑名单的IP访问拒绝
  • 支持URL白名单,将不需要过滤的URL进行定义
  • 支持User-Agent的过滤,匹配自定义规则中的条目,然后进行处理(返回403)
  • 支持CC攻击防护,单个URL指定时间的访问次数,超过设定值,直接返回403
  • 支持Cookie过滤,匹配自定义规则中的条目,然后进行处理(返回403)
  • 支持URL过滤,匹配自定义规则中的条目,如果用户请求的URL包含这些,返回403
  • 支持URL参数过滤,原理同上
  • 支持日志记录,将所有拒绝的操作,记录到日志中去
  • 日志记录为JSON格式,便于日志分析,例如使用ELKStack进行攻击日志收集、存储、搜索和展示

新增插件 - orange/plugins/bot_detection

详见orange/plugins/bot_detection/README.md

新增插件 - orange/plugins/sql_injections

详见orange/plugins/sql_injections/README.md

新增插件 - orange/plugins/xss_code

详见orange/plugins/xss_code/README.md