improved, now handles domain OR path protection with 0, 1, 2 setting for none, pow, captcha

global override does POW only (for now --or can be easily changed for captcha+pow) until i make that customisable level too
no more confusing inverted map
use maps correctly as k:v
cleaned up some stuff
added comments
This commit is contained in:
Thomas Lynch
2021-12-04 21:42:27 +11:00
parent e6ed817746
commit 6f52ee8977
9 changed files with 70 additions and 87 deletions

View File

@ -14,6 +14,7 @@ Some issues fixed and various improvements:
- Fix resolving domain of hcaptcha, no longer uses a hack
- Fix multiple security issues that could result in bypassing the captcha
- Fix challenge cookies lasting forever, they are now limited by a bucket duration on server side
- Ability to set values for domains (or domain+path!) to select off, pow, or captcha and override for paths over domains
#### Screenshot
@ -29,6 +30,7 @@ Add some env vars to docker-compose file:
- CAPTCHA_COOKIE_SECRET - random string, a salt for captcha cookies
- POW_COOKIE_SECRET - different random string, a salt for pow cookies
- RAY_ID - string to identify the haproxy node by
- BUCKET_DURATION - how long between bucket changes, invalidating cookies
Run docker compose:
```bash
@ -41,19 +43,12 @@ DDoS-protection mode is enabled by default.
#### Installation
Before installing the tool, ensure that HaProxy is built with Lua support (in package and ubuntu recommended PPA, it is.)
Before installing the tool, ensure that HAProxy is built with Lua support (in debian package and ubuntu recommended PPA, it is.)
- Copy haproxy config and make sure that `lua-load` directive contains absolute path to [register.lua](src/scripts/register.lua)
- Copy or link [scripts](src/scripts) to /etc/haproxy/scripts
- Copy or link [libs](src/libs) to /etc/haproxy/libs (or a path where Lua looks for modules).
- Create `/etc/haproxy/ddos.map` for domains with protection mode enabled
- Create `/etc/haproxy/no_captcha.map` for domains with no captcha, only pow
If you want to try with tor and haproxy PROXY mode:
- Uncomment the tor service in `docker-compose.yml`
- Change the haproxy mount in `docker-compose.yml` for haproxy.cfg to haproxy.tor.cfg
- Add your hidden service folder (with keys, etc) to `tor/hidden_service`
- Run `docker-compose build` to rebuild the tor container with it.
- Create `/etc/haproxy/ddos.map` for domains or paths with protection mode enabled
#### CLI
The system comes with CLI. It can be used to manage protection global/per-domain and control nocaptcha mode.

View File

@ -9,11 +9,12 @@ services:
context: ./
dockerfile: haproxy/Dockerfile
ports:
- 80:80
- 80:80 #http
- 2000:2000 #port 2000 haproxy socket for external management
volumes:
- ./haproxy/haproxy.cfg:/etc/haproxy/haproxy.cfg
- ./haproxy/ddos.map:/etc/haproxy/ddos.map
- ./haproxy/no_captcha.map:/etc/haproxy/no_captcha.map
- ./haproxy/hosts.map:/etc/haproxy/hosts.map
- ./src/scripts/:/etc/haproxy/scripts/
- ./src/libs/:/etc/haproxy/libs/
- ./haproxy/js/:/var/www/js/

View File

@ -1 +1 @@
localhost
localhost 1

View File

@ -4,6 +4,7 @@ global
log stdout format raw local0 debug
lua-load /etc/haproxy/scripts/register.lua
stats socket /var/run/haproxy.sock mode 666 level admin
stats socket *:2000 level operator
defaults
mode http
@ -14,29 +15,34 @@ defaults
frontend http-in
bind *:80
acl ddos_mode_enabled hdr_cnt(xr3la1rfFc) eq 0
acl ddos_mode_enabled hdr(host) -i -f /etc/haproxy/ddos.map
# you can repeat this acl (which ORs them) to add more conditions where ddos_mode_enabled
acl is_existing_vhost hdr(host),lower,map_str(/etc/haproxy/hosts.map) -m found
http-request silent-drop unless is_existing_vhost
# check captcha cookie
# acl ORs for when ddos_mode_enabled
acl ddos_mode_enabled_override hdr_cnt(xr3la1rfFc) eq 0 # note: global only enables POW not captcha atm until
acl ddos_mode_enabled hdr(host),lower,map(/etc/haproxy/ddos.map) -m bool
acl ddos_mode_enabled base,map(/etc/haproxy/ddos.map) -m bool
# create acl for bools updated by lua
acl captcha_passed var(txn.captcha_passed) -m bool
acl captcha_passed hdr(host),map_str(/etc/haproxy/no_captcha.map) -m found
# check proof of work cookie
acl pow_passed var(txn.pow_passed) -m bool
acl validate_captcha var(txn.validate_captcha) -m bool
acl validate_pow var(txn.validate_pow) -m bool
# exclude favicon, and serve script files directly in haproxy
acl on_captcha_url path -m beg /bot-check
acl is_favicon path /favicon.ico
# define excluded paths, and serve script files directly in haproxy
acl is_excluded path /favicon.ico
acl is_sha1_js path /js/sha1.js
acl is_worker_js path /js/worker.js
http-request return file /var/www/js/sha1.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if is_sha1_js
http-request return file /var/www/js/worker.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if is_worker_js
# check pow/captcha and show page if necessary
http-request use-service lua.hcaptcha-view if on_captcha_url !is_favicon
http-request lua.hcaptcha-check if !is_favicon !on_captcha_url ddos_mode_enabled
http-request lua.pow-check if !is_favicon !on_captcha_url ddos_mode_enabled
http-request redirect location /bot-check?%[capture.req.uri] code 302 if !captcha_passed !on_captcha_url ddos_mode_enabled !is_favicon OR !pow_passed !on_captcha_url ddos_mode_enabled !is_favicon
acl on_captcha_url path /bot-check
http-request use-service lua.hcaptcha-view if on_captcha_url !is_excluded
http-request lua.decide-checks-necessary if !is_excluded !on_captcha_url ddos_mode_enabled #OR !is_excluded !on_captcha_url ddos_mode_enabled_override
http-request lua.hcaptcha-check if !is_excluded !on_captcha_url validate_captcha #OR !is_excluded !on_captcha_url ddos_mode_enabled_override
http-request lua.pow-check if !is_excluded !on_captcha_url validate_pow OR !is_excluded !on_captcha_url ddos_mode_enabled_override
http-request redirect location /bot-check?%[capture.req.uri] code 302 if validate_captcha !captcha_passed !on_captcha_url ddos_mode_enabled !is_excluded OR validate_pow !pow_passed !on_captcha_url ddos_mode_enabled !is_excluded OR !pow_passed ddos_mode_enabled_override !on_captcha_url !is_excluded
default_backend servers

View File

@ -1,53 +0,0 @@
global
daemon
maxconn 256
log stdout format raw local0 notice
lua-load /etc/haproxy/scripts/register.lua
stats socket /var/run/haproxy.sock mode 666 level admin
defaults
log global
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend http-in
bind :80 accept-proxy
#forwardfor sets the circuit identifier sent by tor daemon in haproxy PROXY protocol header as the x-forwarded-for header
option forwardfor
acl ddos_mode_enabled hdr_cnt(xr3la1rfFc) eq 0
acl ddos_mode_enabled hdr(host) -i -f /etc/haproxy/ddos.map
# you can repeat this acl (which ORs them) to add more conditions where ddos_mode_enabled
# check captcha cookie
acl captcha_passed var(txn.captcha_passed) -m bool
acl captcha_passed hdr(host),map_str(/etc/haproxy/no_captcha.map) -m found
# check proof of work cookie
acl pow_passed var(txn.pow_passed) -m bool
# exclude favicon, and serve script files directly in haproxy
acl on_captcha_url path -m beg /bot-check
acl is_favicon path /favicon.ico
acl is_sha1_js path /js/sha1.js
acl is_worker_js path /js/worker.js
http-request return file /var/www/js/sha1.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if is_sha1_js
http-request return file /var/www/js/worker.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if is_worker_js
# check pow/captcha and show page if necessary
http-request use-service lua.hcaptcha-view if on_captcha_url !is_favicon
http-request lua.hcaptcha-check if !is_favicon !on_captcha_url ddos_mode_enabled
http-request lua.pow-check if !is_favicon !on_captcha_url ddos_mode_enabled
http-request redirect location /bot-check?%[capture.req.uri] code 302 if !captcha_passed !on_captcha_url ddos_mode_enabled !is_favicon OR !pow_passed !on_captcha_url ddos_mode_enabled !is_favicon
default_backend servers
backend servers
#can alternatively use a socket
#server server1 unix@/var/run/haproxy-nginx.sock check
server server1 nginx:80 check
backend hcaptcha
server hcaptcha hcaptcha.com:443

1
haproxy/hosts.map Normal file
View File

@ -0,0 +1 @@
localhost

View File

@ -1,2 +0,0 @@
localhost
ehbaiyb5cqqjgviglqcrbd3g7rj2gvc4jahiyyhupq6osz7hfrzgnzqd.onion

View File

@ -15,7 +15,7 @@ local ray_id = os.getenv("RAY_ID")
local captcha_provider_domain = "hcaptcha.com"
local captcha_map = Map.new("/etc/haproxy/no_captcha.map", Map._dom);
local captcha_map = Map.new("/etc/haproxy/ddos.map", Map._str);
-- main page template
local body_template = [[
@ -83,20 +83,35 @@ function _M.view(applet)
local response_body = ""
local response_status_code
if applet.method == "GET" then
-- get challenge string for proof of work
generated_work = utils.generate_secret(applet, pow_cookie_secret, true, "")
-- define body sections
local captcha_body = ""
local pow_body = ""
-- pretty much same as decice_checks but path is different. todo: refactor and pass the applet, with some ifs for applet vs txn
local captcha_enabled = false
local host = applet.headers['host'][0]
loc = captcha_map:lookup(host);
if loc == nil then
local domain_lookup = captcha_map:lookup(host) or 0
domain_lookup = tonumber(domain_lookup)
local path = applet.qs; --because on /bot-check?/whatever, .qs (query string) holds the "path"
local path_lookup = captcha_map:lookup(host..path) or 0
path_lookup = tonumber(path_lookup)
if (path_lookup == 2 and path_lookup >= domain_lookup) or domain_lookup == 2 then
captcha_enabled = true
end
--
-- pow at least is always enabled when reaching bot-check page
if captcha_enabled then
captcha_body = string.format(captcha_section_template, captcha_sitekey)
else
pow_body = pow_section_template
end
-- sub in the body sections
response_body = string.format(body_template, generated_work, pow_body, captcha_body, ray_id)
response_status_code = 403
elseif applet.method == "POST" then
@ -120,14 +135,13 @@ function _M.view(applet)
"set-cookie",
string.format("z_ddos_captcha=%s; expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; SameSite=Strict; Secure=true;", floating_hash)
)
-- else
-- core.Debug("HCAPTCHA FAILED: " .. json.encode(api_response))
end
end
-- if failed captcha, will just get sent back here so 302 is fine
response_status_code = 302
applet:add_header("location", applet.qs)
else
--other methods
-- other methods
response_status_code = 403
end
applet:set_status(response_status_code)
@ -137,15 +151,35 @@ function _M.view(applet)
applet:send(response_body)
end
-- decide which checks to do based on domain and path and domain acls
function _M.decide_checks_necessary(txn)
local host = txn.sf:hdr("Host")
local domain_lookup = captcha_map:lookup(host) or 0
domain_lookup = tonumber(domain_lookup)
local path = txn.sf:path();
local path_lookup = captcha_map:lookup(host..path) or 0
path_lookup = tonumber(path_lookup)
-- probably should make this check less shit
if (path_lookup == 2 and path_lookup >= domain_lookup) or domain_lookup == 2 then
-- check both if captcha mode enabled
txn:set_var("txn.validate_captcha", true)
txn:set_var("txn.validate_pow", true)
elseif (path_lookup == 1 and path_lookup >= domain_lookup) or domain_lookup == 1 then
-- only check pow if mode=1
txn:set_var("txn.validate_pow", true)
end
end
-- check if captcha token is valid, separate secret from POW
function _M.check_captcha_status(txn)
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
local expected_cookie = utils.generate_secret(txn, hcaptcha_cookie_secret, false, nil)
if parsed_request_cookies["z_ddos_captcha"] == expected_cookie then
--core.Debug("CAPTCHA STATUS CHECK SUCCESS")
return txn:set_var("txn.captcha_passed", true)
end
end
-- check if pow token is valid
function _M.check_pow_status(txn)
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
if parsed_request_cookies["z_ddos_pow"] then

View File

@ -5,3 +5,4 @@ local hcaptcha = require("hcaptcha")
core.register_service("hcaptcha-view", "http", hcaptcha.view)
core.register_action("hcaptcha-check", { 'http-req', }, hcaptcha.check_captcha_status)
core.register_action("pow-check", { 'http-req', }, hcaptcha.check_pow_status)
core.register_action("decide-checks-necessary", { 'http-req', }, hcaptcha.decide_checks_necessary)