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

Commit efc2913

Browse files
authored
block external traffic when takedown flag is present (#24)
* 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
1 parent 784c47f commit efc2913

13 files changed

+268
-32
lines changed

.github/workflows/ci_release.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ jobs:
2424

2525
tests:
2626
runs-on: ubuntu-latest
27-
container: openresty/openresty:1.19.9.1-focal
27+
container: openresty/openresty:1.21.4.1-focal
2828
steps:
2929
- uses: actions/checkout@v3
3030

3131
- name: Install Dependencies
3232
run: |
3333
luarocks install lua-resty-http
34+
luarocks install lua-resty-ipmatcher
3435
luarocks install hasher
3536
luarocks install busted
3637
luarocks install luacov

Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ WORKDIR /
55
RUN apt-get update && apt-get --no-install-recommends -y install bc=1.* && \
66
apt-get clean && rm -rf /var/lib/apt/lists/* && \
77
luarocks install lua-resty-http && \
8+
luarocks install lua-resty-ipmatcher && \
89
luarocks install hasher
910

1011
# reload nginx every 6 hours (for reloading certificates)

nginx.conf

+20-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pcre_jit on;
3232
env PORTAL_DOMAIN;
3333
env SERVER_DOMAIN;
3434
env PORTAL_MODULES;
35+
env DENY_PUBLIC_ACCESS;
3536
env ACCOUNTS_LIMIT_ACCESS;
3637
env SIA_API_PASSWORD;
3738
env ACCOUNTS_REDIRECT_URL;
@@ -82,12 +83,25 @@ http {
8283
# proxy cache definition
8384
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;
8485

85-
# this runs before forking out nginx worker processes
86-
init_by_lua_block {
87-
require "cjson"
88-
require "resty.http"
89-
require "skynet.skylink"
90-
require "skynet.utils"
86+
# init phase runs before forking out nginx worker processes
87+
# and preloads the libraries once to improve performance
88+
init_by_lua_block {
89+
-- preload external libraries
90+
require("cjson")
91+
require("resty.http")
92+
require("resty.ipmatcher")
93+
-- preload local lua general use libraries
94+
require("basexx")
95+
require("utils")
96+
-- preload local lua skynet libraries
97+
require("skynet.access")
98+
require("skynet.account")
99+
require("skynet.modules")
100+
require("skynet.pinner")
101+
require("skynet.scanner")
102+
require("skynet.skylink")
103+
require("skynet.tracker")
104+
require("skynet.utils")
91105
}
92106

93107
# include skynet-portal-api and skynet-server-api header on every request

nginx/conf.d/include/location-skylink

+13-6
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,26 @@ set_by_lua_block $skylink { return require("skynet.skylink").parse(ngx.var.skyli
1010
set $limit_rate 0;
1111

1212
access_by_lua_block {
13-
if require("skynet.account").accounts_enabled() then
13+
local skynet_access = require("skynet.access")
14+
local skynet_account = require("skynet.account")
15+
16+
if skynet_access.should_block_access(ngx.var.remote_addr) then
17+
return skynet_access.exit_public_access_forbidden()
18+
end
19+
20+
if skynet_account.accounts_enabled() then
1421
-- check if portal is in authenticated only mode
15-
if require("skynet.account").is_access_unauthorized() then
16-
return require("skynet.account").exit_access_unauthorized()
22+
if skynet_account.is_access_unauthorized() then
23+
return skynet_account.exit_access_unauthorized()
1724
end
1825

1926
-- check if portal is in subscription only mode
20-
if require("skynet.account").is_access_forbidden() then
21-
return require("skynet.account").exit_access_forbidden()
27+
if skynet_account.is_access_forbidden() then
28+
return skynet_account.exit_access_forbidden()
2229
end
2330

2431
-- get account limits of currently authenticated user
25-
local limits = require("skynet.account").get_account_limits()
32+
local limits = skynet_account.get_account_limits()
2633

2734
-- apply download speed limit
2835
ngx.var.limit_rate = limits.download

nginx/conf.d/include/location-skynet-registry

+13-6
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,26 @@ proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
99
proxy_pass http://sia:9980/skynet/registry;
1010

1111
access_by_lua_block {
12-
if require("skynet.account").accounts_enabled() then
12+
local skynet_access = require("skynet.access")
13+
local skynet_account = require("skynet.account")
14+
15+
if skynet_access.should_block_access(ngx.var.remote_addr) then
16+
return skynet_access.exit_public_access_forbidden()
17+
end
18+
19+
if skynet_account.accounts_enabled() then
1320
-- check if portal is in authenticated only mode
14-
if require("skynet.account").is_access_unauthorized() then
15-
return require("skynet.account").exit_access_unauthorized()
21+
if skynet_account.is_access_unauthorized() then
22+
return skynet_account.exit_access_unauthorized()
1623
end
1724

1825
-- check if portal is in subscription only mode
19-
if require("skynet.account").is_access_forbidden() then
20-
return require("skynet.account").exit_access_forbidden()
26+
if skynet_account.is_access_forbidden() then
27+
return skynet_account.exit_access_forbidden()
2128
end
2229

2330
-- get account limits of currently authenticated user
24-
local limits = require("skynet.account").get_account_limits()
31+
local limits = skynet_account.get_account_limits()
2532

2633
-- apply registry rate limits (forced delay)
2734
if limits.registry > 0 then
+12-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
access_by_lua_block {
2+
local skynet_access = require("skynet.access")
3+
local skynet_account = require("skynet.account")
4+
5+
-- check if portal should be blocking public access to api
6+
if skynet_access.should_block_access(ngx.var.remote_addr) then
7+
return skynet_access.exit_public_access_forbidden()
8+
end
9+
210
-- check portal access rules and exit if access is restricted
3-
if require("skynet.account").is_access_unauthorized() then
4-
return require("skynet.account").exit_access_unauthorized()
11+
if skynet_account.is_access_unauthorized() then
12+
return skynet_account.exit_access_unauthorized()
513
end
614

715
-- check if portal is in subscription only mode
8-
if require("skynet.account").is_access_forbidden() then
9-
return require("skynet.account").exit_access_forbidden()
16+
if skynet_account.is_access_forbidden() then
17+
return skynet_account.exit_access_forbidden()
1018
end
1119
}

nginx/conf.d/server/server.api

+12
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,14 @@ location /skynet/tus {
291291
proxy_pass http://sia:9980;
292292

293293
access_by_lua_block {
294+
local skynet_access = require("skynet.access")
294295
local skynet_account = require("skynet.account")
295296

297+
-- check if portal should be blocking public access to api
298+
if skynet_access.should_block_access(ngx.var.remote_addr) then
299+
return skynet_access.exit_public_access_forbidden()
300+
end
301+
296302
if skynet_account.accounts_enabled() then
297303
-- check if portal is in authenticated only mode
298304
if skynet_account.is_access_unauthorized() then
@@ -448,8 +454,14 @@ location /skynet/trustless/basesector {
448454
set $limit_rate 0;
449455

450456
access_by_lua_block {
457+
local skynet_access = require("skynet.access")
451458
local skynet_account = require("skynet.account")
452459

460+
-- check if portal should be blocking public access to api
461+
if skynet_access.should_block_access(ngx.var.remote_addr) then
462+
return skynet_access.exit_public_access_forbidden()
463+
end
464+
453465
if skynet_account.accounts_enabled() then
454466
-- check if portal is in authenticated only mode
455467
if skynet_account.is_access_unauthorized() then

nginx/libs/skynet/access.lua

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
local _M = {}
2+
3+
-- imports
4+
local utils = require("utils")
5+
local skynet_utils = require("skynet.utils")
6+
7+
function _M.match_allowed_internal_networks(ip_addr)
8+
local ipmatcher = require("resty.ipmatcher")
9+
local ipmatcher_private_network = ipmatcher.new({
10+
"127.0.0.0/8", -- host network
11+
"10.0.0.0/8", -- private network
12+
"172.16.0.0/12", -- private network
13+
"192.168.0.0/16", -- private network
14+
})
15+
16+
return ipmatcher_private_network:match(ip_addr)
17+
end
18+
19+
-- function that decides whether the request should be blocked or not
20+
-- based on portal settings and request properties (ngx.var.remote_addr)
21+
function _M.should_block_access(remote_addr)
22+
-- deny public access has to be explictly set to true to block traffic
23+
if utils.getenv("DENY_PUBLIC_ACCESS", "boolean") ~= true then
24+
return false
25+
end
26+
27+
-- block access only when the request does not come from allowed internal network
28+
return _M.match_allowed_internal_networks(remote_addr) == false
29+
end
30+
31+
-- handle request exit when access to portal should deny public access
32+
function _M.exit_public_access_forbidden()
33+
return skynet_utils.exit(ngx.HTTP_FORBIDDEN, "Server public access denied")
34+
end
35+
36+
return _M

nginx/libs/skynet/access.spec.lua

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
local utils = require("utils")
2+
local skynet_access = require("skynet.access")
3+
local skynet_utils = require("skynet.utils")
4+
5+
describe("match_allowed_internal_networks", function()
6+
it("should return true for addresses from 127.0.0.0/8 host network", function()
7+
-- start and end of the range
8+
assert.is_true(skynet_access.match_allowed_internal_networks("127.0.0.0"))
9+
assert.is_true(skynet_access.match_allowed_internal_networks("127.255.255.255"))
10+
-- random addresses from the network
11+
assert.is_true(skynet_access.match_allowed_internal_networks("127.0.0.1"))
12+
assert.is_true(skynet_access.match_allowed_internal_networks("127.10.0.100"))
13+
assert.is_true(skynet_access.match_allowed_internal_networks("127.99.210.3"))
14+
assert.is_true(skynet_access.match_allowed_internal_networks("127.210.20.99"))
15+
end)
16+
17+
it("should return true for addresses from 10.0.0.0/8 private network", function()
18+
-- start and end of the range
19+
assert.is_true(skynet_access.match_allowed_internal_networks("10.0.0.0"))
20+
assert.is_true(skynet_access.match_allowed_internal_networks("10.255.255.255"))
21+
-- random addresses from the network
22+
assert.is_true(skynet_access.match_allowed_internal_networks("10.10.1.0"))
23+
assert.is_true(skynet_access.match_allowed_internal_networks("10.10.10.10"))
24+
assert.is_true(skynet_access.match_allowed_internal_networks("10.87.10.120"))
25+
assert.is_true(skynet_access.match_allowed_internal_networks("10.210.23.255"))
26+
end)
27+
28+
it("should return true for addresses from 172.16.0.0/12 private network", function()
29+
-- start and end of the range
30+
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.0.0"))
31+
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.255.255"))
32+
-- random addresses from the network
33+
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.1.0"))
34+
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.10.10"))
35+
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.10.120"))
36+
assert.is_true(skynet_access.match_allowed_internal_networks("172.16.230.55"))
37+
end)
38+
39+
it("should return true for addresses from 192.168.0.0/16 private network", function()
40+
-- start and end of the range
41+
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.0.0"))
42+
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.255.255"))
43+
-- random addresses from the network
44+
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.1.0"))
45+
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.10.10"))
46+
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.10.120"))
47+
assert.is_true(skynet_access.match_allowed_internal_networks("192.168.230.55"))
48+
end)
49+
50+
it("should return false for addresses from outside of allowed networks", function()
51+
assert.is_false(skynet_access.match_allowed_internal_networks("8.8.8.8"))
52+
assert.is_false(skynet_access.match_allowed_internal_networks("16.12.0.1"))
53+
assert.is_false(skynet_access.match_allowed_internal_networks("115.23.44.17"))
54+
assert.is_false(skynet_access.match_allowed_internal_networks("198.19.0.20"))
55+
assert.is_false(skynet_access.match_allowed_internal_networks("169.254.1.1"))
56+
assert.is_false(skynet_access.match_allowed_internal_networks("212.32.41.12"))
57+
end)
58+
end)
59+
60+
describe("should_block_access", function()
61+
local remote_addr = "127.0.0.1"
62+
63+
before_each(function()
64+
stub(utils, "getenv")
65+
stub(skynet_access, "match_allowed_internal_networks")
66+
end)
67+
68+
after_each(function()
69+
mock.revert(utils)
70+
mock.revert(skynet_access)
71+
end)
72+
73+
it("should not block access if DENY_PUBLIC_ACCESS is not set", function()
74+
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS").returns(nil)
75+
76+
assert.is_false(skynet_access.should_block_access(remote_addr))
77+
end)
78+
79+
it("should not block access if DENY_PUBLIC_ACCESS is set to false", function()
80+
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS").returns(false)
81+
82+
assert.is_false(skynet_access.should_block_access(remote_addr))
83+
end)
84+
85+
it("should not block access if DENY_PUBLIC_ACCESS is set to true but request is from internal network", function()
86+
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS").returns(true)
87+
skynet_access.match_allowed_internal_networks.on_call_with(remote_addr).returns(true)
88+
89+
assert.is_false(skynet_access.should_block_access(remote_addr))
90+
end)
91+
92+
it("should block access if DENY_PUBLIC_ACCESS is set to true and request is not from internal network", function()
93+
utils.getenv.on_call_with("DENY_PUBLIC_ACCESS", "boolean").returns(true)
94+
skynet_access.match_allowed_internal_networks.on_call_with(remote_addr).returns(false)
95+
96+
assert.is_true(skynet_access.should_block_access(remote_addr))
97+
end)
98+
end)
99+
100+
describe("exit_public_access_forbidden", function()
101+
before_each(function()
102+
stub(skynet_utils, "exit")
103+
end)
104+
105+
after_each(function()
106+
mock.revert(skynet_utils)
107+
end)
108+
109+
it("should call exit with status code 403 and proper message", function()
110+
skynet_access.exit_public_access_forbidden()
111+
112+
assert.stub(skynet_utils.exit).was_called_with(403, "Server public access denied")
113+
end)
114+
end)

nginx/libs/skynet/account.lua

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
local _M = {}
22

3+
-- imports
4+
local skynet_utils = require("skynet.utils")
5+
36
-- constant tier ids
47
local tier_id_anonymous = 0
58
local tier_id_free = 1
@@ -52,15 +55,15 @@ end
5255

5356
-- handle request exit when access to portal should be restricted to authenticated users only
5457
function _M.exit_access_unauthorized()
55-
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
58+
return skynet_utils.exit(ngx.HTTP_UNAUTHORIZED)
5659
end
5760

5861
-- handle request exit when access to portal should be restricted to subscription users only
59-
function _M.exit_access_forbidden(message)
60-
ngx.status = ngx.HTTP_FORBIDDEN
61-
ngx.header["content-type"] = "text/plain"
62-
ngx.say(message or "Portal operator restricted access to users with active subscription only")
63-
return ngx.exit(ngx.status)
62+
function _M.exit_access_forbidden()
63+
return skynet_utils.exit(
64+
ngx.HTTP_FORBIDDEN,
65+
"Portal operator restricted access to users with active subscription only"
66+
)
6467
end
6568

6669
function _M.accounts_enabled()

0 commit comments

Comments
 (0)