diff --git a/apisix/plugins/real-ip.lua b/apisix/plugins/real-ip.lua new file mode 100644 index 000000000000..f0da0bb9a8f9 --- /dev/null +++ b/apisix/plugins/real-ip.lua @@ -0,0 +1,100 @@ +-- +-- 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. +-- +local core = require("apisix.core") +local is_apisix_or, client = pcall(require, "resty.apisix.client") +local str_byte = string.byte +local str_sub = string.sub + + +local schema = { + type = "object", + properties = { + source = { + type = "string", + minLength = 1 + } + }, + required = {"source"}, +} + + +local plugin_name = "real-ip" + + +local _M = { + version = 0.1, + priority = 23000, + name = plugin_name, + schema = schema, +} + + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + + +local function get_addr(conf, ctx) + return ctx.var[conf.source] +end + + +function _M.rewrite(conf, ctx) + if not is_apisix_or then + core.log.error("need to build APISIX-OpenResty to support setting real ip") + return 501 + end + + local addr = get_addr(conf, ctx) + if not addr then + core.log.warn("missing real address") + return + end + + local ip, port = core.utils.parse_addr(addr) + if not ip or (not core.utils.parse_ipv4(ip) and not core.utils.parse_ipv6(ip)) then + core.log.warn("bad address: ", addr) + return + end + + if str_byte(ip, 1, 1) == str_byte("[") then + -- For IPv6, the `set_real_ip` accepts '::1' but not '[::1]' + ip = str_sub(ip, 2, #ip - 1) + end + + if port ~= nil and (port < 1 or port > 65535) then + core.log.warn("bad port: ", port) + return + end + + core.log.info("set real ip: ", ip, ", port: ", port) + + local ok, err = client.set_real_ip(ip, port) + if not ok then + core.log.error("failed to set real ip: ", err) + return + end + + -- flush cached vars in APISIX + ctx.var.remote_addr = nil + ctx.var.remote_port = nil + ctx.var.realip_remote_addr = nil + ctx.var.realip_remote_port = nil +end + + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index c1f38b0e895b..0455a53d47aa 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -271,6 +271,7 @@ graphql: #cmd: ["ls", "-l"] plugins: # plugin list (sorted by priority) + - real-ip # priority: 23000 - client-control # priority: 22000 - ext-plugin-pre-req # priority: 12000 - zipkin # priority: 11011 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 1e3094cd08a7..a7135c210212 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -39,6 +39,7 @@ "plugins/redirect", "plugins/echo", "plugins/gzip", + "plugins/real-ip", "plugins/server-info", "plugins/ext-plugin-pre-req", "plugins/ext-plugin-post-req" diff --git a/docs/en/latest/plugins/client-control.md b/docs/en/latest/plugins/client-control.md index f1913236619d..80e5d045d326 100644 --- a/docs/en/latest/plugins/client-control.md +++ b/docs/en/latest/plugins/client-control.md @@ -34,7 +34,7 @@ title: client-control The `client-control` plugin dynamically controls the behavior of Nginx to handle the client request. -This plugin requires APISIX to run on [APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix). +**This plugin requires APISIX to run on [APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix).** ## Attributes diff --git a/docs/en/latest/plugins/gzip.md b/docs/en/latest/plugins/gzip.md index d94721571d96..a7a6af91614e 100644 --- a/docs/en/latest/plugins/gzip.md +++ b/docs/en/latest/plugins/gzip.md @@ -33,7 +33,7 @@ title: gzip The `gzip` plugin dynamically set the gzip behavior of Nginx. -This plugin requires APISIX to run on [APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix). +**This plugin requires APISIX to run on [APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix).** ## Attributes diff --git a/docs/en/latest/plugins/real-ip.md b/docs/en/latest/plugins/real-ip.md new file mode 100644 index 000000000000..673d973c153c --- /dev/null +++ b/docs/en/latest/plugins/real-ip.md @@ -0,0 +1,106 @@ +--- +title: real-ip +--- + + + +## Summary + +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + +## Name + +The `real-ip` plugin dynamically changes the client's IP and port seen by APISIX. + +It works like Nginx's `ngx_http_realip_module`, but is more flexible. + +**This plugin requires APISIX to run on [APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix).** + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| --------- | ------------- | ----------- | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| source | string | required | | Any Nginx variable like `arg_realip` or `http_x_forwarded_for`| dynamically set the client's IP and port in APISIX's view, according to the value of variable. If the value doesn't contain a port, the client's port won't be changed. | + +If the remote address comes from `source` is missing or invalid, this plugin will just let it go and don't change the client address. + +## How To Enable + +Here's an example, enable this plugin on the specified route: + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "real-ip": { + "source": "arg_realip" + }, + "response-rewrite": { + "headers": { + "remote_addr": "$remote_addr", + "remote_port": "$remote_port" + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +## Test Plugin + +Use curl to access: + +```shell +curl 'http://127.0.0.1:9080/index.html?realip=1.2.3.4:9080' -I +... +remote-addr: 1.2.3.4 +remote-port: 9080 +``` + +## Disable Plugin + +When you want to disable this plugin, it is very simple, +you can delete the corresponding JSON configuration in the plugin configuration, +no need to restart the service, it will take effect immediately: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +This plugin has been disabled now. It works for other plugins. diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 67b8c0a01a79..705ce285c70d 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -40,7 +40,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/ +qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/ --- no_error_log [error] diff --git a/t/core/config.t b/t/core/config.t index 2ffd45cf8e77..b87fe1224e0c 100644 --- a/t/core/config.t +++ b/t/core/config.t @@ -38,7 +38,7 @@ __DATA__ GET /t --- response_body etcd host: http://127.0.0.1:2379 -first plugin: "client-control" +first plugin: "real-ip" diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index ea847281a5a1..f13e68c83937 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -44,6 +44,7 @@ GET /t --- response_body done --- error_log +loaded plugin and sort by priority: 23000 name: real-ip loaded plugin and sort by priority: 22000 name: client-control loaded plugin and sort by priority: 12000 name: ext-plugin-pre-req loaded plugin and sort by priority: 11011 name: zipkin diff --git a/t/plugin/real-ip.t b/t/plugin/real-ip.t new file mode 100644 index 000000000000..23badde91184 --- /dev/null +++ b/t/plugin/real-ip.t @@ -0,0 +1,267 @@ +# +# 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; + +my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx'; +my $version = eval { `$nginx_binary -V 2>&1` }; + +if ($version !~ m/\/apisix-nginx-module/) { + plan(skip_all => "apisix-nginx-module not installed"); +} else { + plan('no_plan'); +} + +repeat_each(1); +log_level('info'); +no_root_location(); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: schema 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", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "real-ip": { + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } +} +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin real-ip err: property \"source\" is required"} + + + +=== TEST 2: sanity +--- 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", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "real-ip": { + "source": "http_xff" + }, + "ip-restriction": { + "whitelist": ["1.1.1.1"] + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } +} +--- response_body +passed + + + +=== TEST 3: hit +--- request +GET /hello +--- more_headers +XFF: 1.1.1.1 + + + +=== TEST 4: with port +--- request +GET /hello +--- more_headers +XFF: 1.1.1.1:80 + + + +=== TEST 5: miss address +--- request +GET /hello +--- error_code: 403 + + + +=== TEST 6: bad address +--- request +GET /hello +--- more_headers +XFF: 1.1.1.1.1 +--- error_code: 403 + + + +=== TEST 7: bad port +--- request +GET /hello +--- more_headers +XFF: 1.1.1.1:65536 +--- error_code: 403 + + + +=== TEST 8: ipv6 +--- 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", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "real-ip": { + "source": "http_xff" + }, + "ip-restriction": { + "whitelist": ["::2"] + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } +} +--- response_body +passed + + + +=== TEST 9: hit +--- request +GET /hello +--- more_headers +XFF: ::2 + + + +=== TEST 10: with port +--- request +GET /hello +--- more_headers +XFF: [::2]:80 + + + +=== TEST 11: with bracket +--- request +GET /hello +--- more_headers +XFF: [::2] + + + +=== TEST 12: check port +--- 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", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "real-ip": { + "source": "http_xff" + }, + "response-rewrite": { + "headers": { + "remote_port": "$remote_port" + } + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } +} +--- response_body +passed + + + +=== TEST 13: hit +--- request +GET /hello +--- more_headers +XFF: 1.1.1.1:7090 +--- response_headers +remote_port: 7090