Skip to content

behappy-other/orange

 
 

Repository files navigation

orange网关传统集群部署模式

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

    "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,所以这里还需要对源码以及数据库字段长度进行调整)

image-20220728102915986

  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

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

      -- 在log_by_lua*上下文中使用ngx.timer.at延迟执行Redis操作
      ngx.timer.at(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

数据库字段长度调整

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
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

# 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
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版本
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指令
### 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处理 -> 指定时间内不可访问

image-20230626172818833

Dockerfile

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 . .
CMD make dev && make install && sleep 5 && resty bin/orange start && tail -f /opt/orange/logs/access.log

Statefulset.yaml

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:
            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秒内启动成功

image-20230627145245207

访问测试

多次访问,呈轮询效果

image-20230627152651336

控制台

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

节点注册

  • 添加自定义header

image-20230627145605151

  • 查看全局统计(此处如果传的header:orange-versionorange-1,则Address呈现为orange-1

image-20230627145522531

  • 分别注册orange-0orange-1节点

image-20230627145426685

image-20230627145731146

配置同步

  • orange-0节点配置防刷插件

image-20230627152004446

  • 回到orange-1,进行同步

image-20230627152113693

  • 同步成功

image-20230627152149382

防刷测试

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

image-20230627180145560

Jmeter压测

测试从100线程开始

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

活跃连接数维持在100上下

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

jmeter

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

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

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

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

429

产生的问题

lua_max_running_timers are not enough

压了一段时间后,网关日志开始出现大量错误

2023/06/27 17:59:58 [alert] 20102#20102: lua failed to run timer with function defined at @/opt/orange/orange//orange/plugins/monitor/stat.lua:18: 256 lua_max_running_timers are not enough
2023/06/27 17:59:58 [alert] 20102#20102: lua failed to run timer with function defined at @/opt/orange/orange//orange/plugins/monitor/stat.lua:18: 256 lua_max_running_timers are not enough
2023/06/27 17:59:58 [alert] 20102#20102: lua failed to run timer with function defined at @/opt/orange/orange//orange/plugins/stat/stat.lua:53: 256 lua_max_running_timers are not enough
2023/06/27 17:59:58 [alert] 20102#20102: lua failed to run timer with function defined at @/opt/orange/orange//orange/plugins/monitor/stat.lua:18: 256 lua_max_running_timers are not enough
2023/06/27 17:59:58 [alert] 20102#20102: lua failed to run timer with function defined at @/opt/orange/orange//orange/plugins/monitor/stat.lua:18: 256 lua_max_running_timers are not enough

这个错误是因为在自定义监控插件中,记录监控数据是在log_by_lua中操作的,而这里不允许操作redis,所以我改写成了ngx.timer.at延迟操作

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

这里需要考虑重写下:TODO

redis压力

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

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

image-20230628095126845

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

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/bot_detection

详见orange/plugins/bot_detection/README.md

Packages

No packages published

Languages

  • Lua 47.1%
  • JavaScript 28.4%
  • HTML 17.2%
  • Raku 4.8%
  • CSS 1.0%
  • Perl 0.5%
  • Other 1.0%