开发一个 kong 网关插件禁止通过 ip 访问服务器

背景

服务统一使用了 kong 网关进行管理,并且不希望客户端通过服务器 ip 进行访问,必须要通过域名。

这个功需求点如果是在 nginx 处理起来会很简单,修改下配置文件里的 server_name 就行了,但是 kong 如何处理我没有在网上找到一个很好的解决方案,那只能自己写插件来处理了,也顺便借此学习下 kong 插件的开发,毕竟用 kong 哪有不写自定义插件的时候呢。

Kong 运行环境搭建

使用 docker 快速搭建 db-less 环境:

  1. 创建 docker-compose.yml 内容如下:

    version: "3.8"
    
    services:
      kong:
        image: kong:2.5.1
        volumes:
          - ./kong.yml:/kong.yml
        environment:
          - KONG_DATABASE=off
          - KONG_DECLARATIVE_CONFIG=/kong.yml
        ports:
          - 8000:8000
    
  2. 创建 kong.yml 内容如下:

    这里从 public-apis 里随便找一个免费无须认证的 api 接口做测试

    _format_version: "2.1"
    
    services:
      - name: test
        url: http://dog-facts-api.herokuapp.com/api/v1/resources/dogs
        routes:
          - name: search
            paths:
              - /get
            strip_path: true
    
  3. docker-compose up 运行服务

  4. curl http://localhost:8000/get?number=2 测试是否正常返回结果

Kong 插件开发环境搭建

目录结构

kong 插件的开发目录结构需要使用下面的格式,否则会报错 ... <plugin-name> plugin is enabled but not installed :

<plugin-name>
├── kong
│   └── plugins
│       └── <plugin-name>
│           ├── handler.lua
│           └── schema.lua
└── <plugin-name>-<version>.rockspec

最后,我们本地目录长这个样子:

kong-plugin-dev
├── docker-compose.yml
├── kong.yml
└── plugins
    └── disable-access-ip
        ├── disable-access-ip-0.0.1-1.rockspec
        └── kong
            └── plugins
                └── disable-access-ip
                    ├── handler.lua
                    └── schema.lua

disable-access-ip-0.0.1-1.rockspec 写入如下内容:

package = "disable-access-ip"
version = "0.0.1-1"
build = {
   type = "builtin",
   modules = {
      ["kong.plugins.disable-access-ip.handler"] = "kong/plugins/disable-access-ip/handler.lua",
      ["kong.plugins.disable-access-ip.schema"] = "kong/plugins/disable-access-ip/schema.lua"
   }
}

schema.lua 写入如下内容:

local schema = {
    name="disable-access-ip",
    fields = {}
}

return schema

handler.lua 写入如下内容:

local handler = {
    VERSION  = "0.0.1-1",
    PRIORITY = 10,
}

function handler:access(config)
    kong.log("access")
end

return handler

除了上述俩个 lua 模块文件,其他的模块文件及其作用可见 官网文档

安装并加载插件

官网推荐使用 luarocks 来安装插件,根据 官网文档 所述,有三种方式:

本文使用法三,既不需要本地环境下安装 luarocks,也不会污染 kong 运行环境里的相关包,毕竟这个插件仅是服务于 kong 的:

  1. 先更新下 docker-compose.yml

    version: "3.8"
    
    services:
      kong:
        image: kong:2.5.1
        volumes:
          - ./kong.yml:/kong.yml
          - ./plugins:/plugins # <--------- new
        environment:
          - KONG_DATABASE=off
          - KONG_DECLARATIVE_CONFIG=/kong.yml
                
          # ↓↓↓↓↓↓↓ new ↓↓↓↓↓↓↓
          - KONG_LUA_PACKAGE_PATH=/plugins/disable-access-ip/?.lua;;
          - KONG_PLUGINS=disable-access-ip
          - KONG_LOG_LEVEL=debug
          # ↑↑↑↑↑↑↑ new ↑↑↑↑↑↑↑
        ports:
          - 8000:8000
    
  2. kong.yml 里启用这个插件:

    services:
        # ...
    
    # ↓↓↓↓↓↓↓ new ↓↓↓↓↓↓↓
    plugins:
      - name: disable-access-ip
    
  3. 重新运行 docker-compose up

如果日志打印 "... [debug] 1#0: [lua] plugins.lua:245: load_plugin(): Loading plugin: disable-access-ip" 即表明插件加载成功

编写具体逻辑

现在插件开发的基本环境已经 OK 了,下面开始具体功能的开发。

handler.lua 是我们主要用到的模块文件,kong 会将里面的逻辑注入到对应的执行阶段,共有7个阶段,不同阶段分别做什么可参见 官网文档

该插件则关注的是 access 阶段,该阶段会在客户端的请求转发到上游服务之前被执行, 我们通过调用全局变量 kong 的相关属性和方法来实现相关逻辑,其具体属性和函数可参见 官网PDK文档

另外,除了调用 PDK,我们也是可以直接调用 lua-nginx-module 里的 API 哦,毕竟 kong 是基于 Openresty 开发的,但对于修改类的操作,请尽可能使用 PDK 提供的修改函数,避免一些非预期情况的发生。

下面是完整的 handler.lua 内容:

local handler = {
    VERSION  = "0.0.1-1",
    PRIORITY = 10,
}

-- 该函数用来判断输入内容是否是ipv4,ipv6字符串
-- copied from https://stackoverflow.com/questions/10975935/lua-function-check-if-ipv4-or-ipv6-or-string
local R = {ERROR = 0, IPV4 = 1, IPV6 = 2, STRING = 3}
local function getIPType(ip)
    if type(ip) ~= "string" then return R.ERROR end

    local chunks = {ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")}
    if #chunks == 4 then
        for _,v in pairs(chunks) do
            if tonumber(v) > 255 then return R.STRING end
        end
        return R.IPV4
    end

    local chunks = {ip:match("^"..(("([a-fA-F0-9]*):"):rep(8):gsub(":$","$")))}
    if #chunks == 8
            or #chunks < 8 and ip:match('::') and not ip:gsub("::","",1):match('::') then
        for _,v in pairs(chunks) do
            if #v > 0 and tonumber(v, 16) > 65535 then return R.STRING end
        end
        return R.IPV6
    end

    return R.STRING
end

function handler:access(config)
    local host = kong.request.get_host()
    local hostType = getIPType(host)
		-- 如果是ip地址,则返回444
    if hostType == R.IPV4 or hostType == R.IPV6 then
        return kong.response.error(403)
    end
end

return handler

然后我们重新运行 docker-compose up

接着开始验证插件是否正常运行:

先通过域名访问:curl http://localhost:8000/get?number=2 正常,

然后通过 ip 访问:curl http://127.0.0.1:8000/get?number=2 会发现 kong 日志的状态码返回的是403,即我们的插件正常运行了。

结束

除了使用 lua 语言开发插件,kong 也支持使用 go,js,python 进行插件开发, 不过开发这种精简小插件就甭费神折腾其他语言的开发环境了,Language is just a tool, choose the one which is faster for delivery.