mirror of
https://gitgud.io/fatchan/haproxy-protection.git
synced 2025-05-09 02:05:37 +00:00
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:
13
README.MD
13
README.MD
@ -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.
|
||||
|
@ -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/
|
||||
|
@ -1 +1 @@
|
||||
localhost
|
||||
localhost 1
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
1
haproxy/hosts.map
Normal file
@ -0,0 +1 @@
|
||||
localhost
|
@ -1,2 +0,0 @@
|
||||
localhost
|
||||
ehbaiyb5cqqjgviglqcrbd3g7rj2gvc4jahiyyhupq6osz7hfrzgnzqd.onion
|
@ -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
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user