Skip to content
This repository has been archived by the owner on Aug 1, 2023. It is now read-only.

Commit

Permalink
block external traffic when takedown flag is present (#24)
Browse files Browse the repository at this point in the history
* initial implementation

* include lua-resty-ipmatcher in the docker image

* refactor the code

* refactor the code

* fix typo

* fix import

* fix shared dict usage

* dns resolver

* change dns resolver

* log ip

* use server ip env var

* drop resolver

* fix formatting

* block hns

* fix duplicate access_by_lua_block

* add to generic portal check

* refactor imports

* block tus and trustless download

* unused skynet_utils

* drop unnecessary server ip env var requirement

* update docker test image

* update testing image

* revert some changes to test docker image

* add lua-resty-ipmatcher to testing setup

* create exit_public_access_forbidden

* deny registry access too

* increase test coverage

* refactor exit functions

* unused argument message

* no trailing whitespace

* luacov disable untestable function

* rename function and add test coverage

* docs typo
  • Loading branch information
kwypchlo authored Jul 11, 2022
1 parent 784c47f commit efc2913
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 32 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ jobs:

tests:
runs-on: ubuntu-latest
container: openresty/openresty:1.19.9.1-focal
container: openresty/openresty:1.21.4.1-focal
steps:
- uses: actions/checkout@v3

- name: Install Dependencies
run: |
luarocks install lua-resty-http
luarocks install lua-resty-ipmatcher
luarocks install hasher
luarocks install busted
luarocks install luacov
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ WORKDIR /
RUN apt-get update && apt-get --no-install-recommends -y install bc=1.* && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
luarocks install lua-resty-http && \
luarocks install lua-resty-ipmatcher && \
luarocks install hasher

# reload nginx every 6 hours (for reloading certificates)
Expand Down
26 changes: 20 additions & 6 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pcre_jit on;
env PORTAL_DOMAIN;
env SERVER_DOMAIN;
env PORTAL_MODULES;
env DENY_PUBLIC_ACCESS;
env ACCOUNTS_LIMIT_ACCESS;
env SIA_API_PASSWORD;
env ACCOUNTS_REDIRECT_URL;
Expand Down Expand Up @@ -82,12 +83,25 @@ http {
# proxy cache definition
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=skynet:10m max_size=50g min_free=100g inactive=48h use_temp_path=off;

# this runs before forking out nginx worker processes
init_by_lua_block {
require "cjson"
require "resty.http"
require "skynet.skylink"
require "skynet.utils"
# init phase runs before forking out nginx worker processes
# and preloads the libraries once to improve performance
init_by_lua_block {
-- preload external libraries
require("cjson")
require("resty.http")
require("resty.ipmatcher")
-- preload local lua general use libraries
require("basexx")
require("utils")
-- preload local lua skynet libraries
require("skynet.access")
require("skynet.account")
require("skynet.modules")
require("skynet.pinner")
require("skynet.scanner")
require("skynet.skylink")
require("skynet.tracker")
require("skynet.utils")
}

# include skynet-portal-api and skynet-server-api header on every request
Expand Down
19 changes: 13 additions & 6 deletions nginx/conf.d/include/location-skylink
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@ set_by_lua_block $skylink { return require("skynet.skylink").parse(ngx.var.skyli
set $limit_rate 0;

access_by_lua_block {
if require("skynet.account").accounts_enabled() then
local skynet_access = require("skynet.access")
local skynet_account = require("skynet.account")

if skynet_access.should_block_access(ngx.var.remote_addr) then
return skynet_access.exit_public_access_forbidden()
end

if skynet_account.accounts_enabled() then
-- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
if skynet_account.is_access_unauthorized() then
return skynet_account.exit_access_unauthorized()
end

-- check if portal is in subscription only mode
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
if skynet_account.is_access_forbidden() then
return skynet_account.exit_access_forbidden()
end

-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
local limits = skynet_account.get_account_limits()

-- apply download speed limit
ngx.var.limit_rate = limits.download
Expand Down
19 changes: 13 additions & 6 deletions nginx/conf.d/include/location-skynet-registry
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@ proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
proxy_pass http://sia:9980/skynet/registry;

access_by_lua_block {
if require("skynet.account").accounts_enabled() then
local skynet_access = require("skynet.access")
local skynet_account = require("skynet.account")

if skynet_access.should_block_access(ngx.var.remote_addr) then
return skynet_access.exit_public_access_forbidden()
end

if skynet_account.accounts_enabled() then
-- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
if skynet_account.is_access_unauthorized() then
return skynet_account.exit_access_unauthorized()
end

-- check if portal is in subscription only mode
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
if skynet_account.is_access_forbidden() then
return skynet_account.exit_access_forbidden()
end

-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
local limits = skynet_account.get_account_limits()

-- apply registry rate limits (forced delay)
if limits.registry > 0 then
Expand Down
16 changes: 12 additions & 4 deletions nginx/conf.d/include/portal-access-check
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
access_by_lua_block {
local skynet_access = require("skynet.access")
local skynet_account = require("skynet.account")

-- check if portal should be blocking public access to api
if skynet_access.should_block_access(ngx.var.remote_addr) then
return skynet_access.exit_public_access_forbidden()
end

-- check portal access rules and exit if access is restricted
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
if skynet_account.is_access_unauthorized() then
return skynet_account.exit_access_unauthorized()
end

-- check if portal is in subscription only mode
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
if skynet_account.is_access_forbidden() then
return skynet_account.exit_access_forbidden()
end
}
12 changes: 12 additions & 0 deletions nginx/conf.d/server/server.api
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,14 @@ location /skynet/tus {
proxy_pass http://sia:9980;

access_by_lua_block {
local skynet_access = require("skynet.access")
local skynet_account = require("skynet.account")

-- check if portal should be blocking public access to api
if skynet_access.should_block_access(ngx.var.remote_addr) then
return skynet_access.exit_public_access_forbidden()
end

if skynet_account.accounts_enabled() then
-- check if portal is in authenticated only mode
if skynet_account.is_access_unauthorized() then
Expand Down Expand Up @@ -448,8 +454,14 @@ location /skynet/trustless/basesector {
set $limit_rate 0;

access_by_lua_block {
local skynet_access = require("skynet.access")
local skynet_account = require("skynet.account")

-- check if portal should be blocking public access to api
if skynet_access.should_block_access(ngx.var.remote_addr) then
return skynet_access.exit_public_access_forbidden()
end

if skynet_account.accounts_enabled() then
-- check if portal is in authenticated only mode
if skynet_account.is_access_unauthorized() then
Expand Down
36 changes: 36 additions & 0 deletions nginx/libs/skynet/access.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
local _M = {}

-- imports
local utils = require("utils")
local skynet_utils = require("skynet.utils")

function _M.match_allowed_internal_networks(ip_addr)
local ipmatcher = require("resty.ipmatcher")
local ipmatcher_private_network = ipmatcher.new({
"127.0.0.0/8", -- host network
"10.0.0.0/8", -- private network
"172.16.0.0/12", -- private network
"192.168.0.0/16", -- private network
})

return ipmatcher_private_network:match(ip_addr)
end

-- function that decides whether the request should be blocked or not
-- based on portal settings and request properties (ngx.var.remote_addr)
function _M.should_block_access(remote_addr)
-- deny public access has to be explictly set to true to block traffic
if utils.getenv("DENY_PUBLIC_ACCESS", "boolean") ~= true then
return false
end

-- block access only when the request does not come from allowed internal network
return _M.match_allowed_internal_networks(remote_addr) == false
end

-- handle request exit when access to portal should deny public access
function _M.exit_public_access_forbidden()
return skynet_utils.exit(ngx.HTTP_FORBIDDEN, "Server public access denied")
end

return _M
114 changes: 114 additions & 0 deletions nginx/libs/skynet/access.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
local utils = require("utils")
local skynet_access = require("skynet.access")
local skynet_utils = require("skynet.utils")

describe("match_allowed_internal_networks", function()
it("should return true for addresses from 127.0.0.0/8 host network", function()
-- start and end of the range
assert.is_true(skynet_access.match_allowed_internal_networks("127.0.0.0"))
assert.is_true(skynet_access.match_allowed_internal_networks("127.255.255.255"))
-- random addresses from the network
assert.is_true(skynet_access.match_allowed_internal_networks("127.0.0.1"))
assert.is_true(skynet_access.match_allowed_internal_networks("127.10.0.100"))
assert.is_true(skynet_access.match_allowed_internal_networks("127.99.210.3"))
assert.is_true(skynet_access.match_allowed_internal_networks("127.210.20.99"))
end)

it("should return true for addresses from 10.0.0.0/8 private network", function()
-- start and end of the range
assert.is_true(skynet_access.match_allowed_internal_networks("10.0.0.0"))
assert.is_true(skynet_access.match_allowed_internal_networks("10.255.255.255"))
-- random addresses from the network
assert.is_true(skynet_access.match_allowed_internal_networks("10.10.1.0"))
assert.is_true(skynet_access.match_allowed_internal_networks("10.10.10.10"))
assert.is_true(skynet_access.match_allowed_internal_networks("10.87.10.120"))
assert.is_true(skynet_access.match_allowed_internal_networks("10.210.23.255"))
end)

it("should return true for addresses from 172.16.0.0/12 private network", function()
-- start and end of the range
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.0.0"))
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.255.255"))
-- random addresses from the network
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.1.0"))
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.10.10"))
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.10.120"))
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.230.55"))
end)

it("should return true for addresses from 192.168.0.0/16 private network", function()
-- start and end of the range
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.0.0"))
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.255.255"))
-- random addresses from the network
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.1.0"))
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.10.10"))
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.10.120"))
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.230.55"))
end)

it("should return false for addresses from outside of allowed networks", function()
assert.is_false(skynet_access.match_allowed_internal_networks("8.8.8.8"))
assert.is_false(skynet_access.match_allowed_internal_networks("16.12.0.1"))
assert.is_false(skynet_access.match_allowed_internal_networks("115.23.44.17"))
assert.is_false(skynet_access.match_allowed_internal_networks("198.19.0.20"))
assert.is_false(skynet_access.match_allowed_internal_networks("169.254.1.1"))
assert.is_false(skynet_access.match_allowed_internal_networks("212.32.41.12"))
end)
end)

describe("should_block_access", function()
local remote_addr = "127.0.0.1"

before_each(function()
stub(utils, "getenv")
stub(skynet_access, "match_allowed_internal_networks")
end)

after_each(function()
mock.revert(utils)
mock.revert(skynet_access)
end)

it("should not block access if DENY_PUBLIC_ACCESS is not set", function()
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS").returns(nil)

assert.is_false(skynet_access.should_block_access(remote_addr))
end)

it("should not block access if DENY_PUBLIC_ACCESS is set to false", function()
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS").returns(false)

assert.is_false(skynet_access.should_block_access(remote_addr))
end)

it("should not block access if DENY_PUBLIC_ACCESS is set to true but request is from internal network", function()
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS").returns(true)
skynet_access.match_allowed_internal_networks.on_call_with(remote_addr).returns(true)

assert.is_false(skynet_access.should_block_access(remote_addr))
end)

it("should block access if DENY_PUBLIC_ACCESS is set to true and request is not from internal network", function()
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS", "boolean").returns(true)
skynet_access.match_allowed_internal_networks.on_call_with(remote_addr).returns(false)

assert.is_true(skynet_access.should_block_access(remote_addr))
end)
end)

describe("exit_public_access_forbidden", function()
before_each(function()
stub(skynet_utils, "exit")
end)

after_each(function()
mock.revert(skynet_utils)
end)

it("should call exit with status code 403 and proper message", function()
skynet_access.exit_public_access_forbidden()

assert.stub(skynet_utils.exit).was_called_with(403, "Server public access denied")
end)
end)
15 changes: 9 additions & 6 deletions nginx/libs/skynet/account.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
local _M = {}

-- imports
local skynet_utils = require("skynet.utils")

-- constant tier ids
local tier_id_anonymous = 0
local tier_id_free = 1
Expand Down Expand Up @@ -52,15 +55,15 @@ end

-- handle request exit when access to portal should be restricted to authenticated users only
function _M.exit_access_unauthorized()
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
return skynet_utils.exit(ngx.HTTP_UNAUTHORIZED)
end

-- handle request exit when access to portal should be restricted to subscription users only
function _M.exit_access_forbidden(message)
ngx.status = ngx.HTTP_FORBIDDEN
ngx.header["content-type"] = "text/plain"
ngx.say(message or "Portal operator restricted access to users with active subscription only")
return ngx.exit(ngx.status)
function _M.exit_access_forbidden()
return skynet_utils.exit(
ngx.HTTP_FORBIDDEN,
"Portal operator restricted access to users with active subscription only"
)
end

function _M.accounts_enabled()
Expand Down
Loading

0 comments on commit efc2913

Please sign in to comment.