diff --git a/README.MD b/README.MD index 451086a..007609b 100644 --- a/README.MD +++ b/README.MD @@ -1,30 +1,29 @@ -## HAProxy DDoS protection system +## haproxy-protection -A fork and further development of a proof of concept from https://github.com/mora9715/haproxy_ddos_protector, a HAProxy configuration and lua scripts allowing a challenge-response page where users solve a captcha and/or proof-of-work. -Intended to stop bots, spam, ddos, +A fork and further development of a proof of concept from https://github.com/mora9715/haproxy_ddos_protector, a HAProxy configuration and lua scripts allowing a challenge-response page where users solve a captcha and/or proof-of-work. Intended to stop bots, spam, ddos. Integrates with https://gitgud.io/fatchan/haproxy-panel-next to add/remove/edit domains, protection rules, blocked ips, backend server IPs, etc during runtime. Improvements in this fork: - Add a proof-of-work element, instead of only captcha. -- Add examples and support for .onion/tor using the HAProxy PROXY protocol to provide some kind of "ip" discrimination of tor users (circuit identifiers). -- Use HAProxy http-request return to improve performance/caching for the challenge page, without an extra backend http server. -- Improved the appearance of the challenge page. -- Remove hcaptcha dns resolution hack, use proper backend address. +- Supports hcaptcha or recaptcha. +- Support .onion/tor with the HAProxy PROXY protocol, using circuit identifiers as a substitute for IPs. +- Use HAProxy `http-request return` directive to directly serve challenge pages from the edge, with no separate backend. - Fix multiple security issues that could result in bypassing the captcha. - Add a bucket duration for cookie validity, so valid cookies don't last forever. -- Cluster toggle, for pow mode only. - Choose protection modes "none", "pow" or "pow+captcha" per-domain or per-domain+path, with paths taking priority. -- Whitelist IPs/subnets. +- Provide a bash script that solves the proof-of-work and a form submission box for noscript users. +- Whitelist or blacklist IPs/subnets. - Maintenance mode page for selected domains. -- In POW only mode, provide instructions and an encoded script to find the solution. +- Improved the appearance of the challenge page. - Many bugfixes. -#### How to test +#### Environment variables -Add some env vars to docker-compose file: +For docker, these are in docker-compose.yml. For production deployments, add them to `/etc/default/haproxy`. +NOTE: Use either HCAPTCHA_ or RECAPTHCA_, not both. - HCAPTCHA_SITEKEY - your hcaptcha site key - HCAPTCHA_SECRET - your hcaptcha secret key - RECAPTCHA_SITEKEY - your recaptcha site key @@ -39,12 +38,9 @@ Add some env vars to docker-compose file: - SERVER_PREFIX - Optional, prefix of server names used in server-template - POW_TIME - argon2 iterations - POW_KB - argon2 memory usage in KB -- POW_DIFFICULTY - pow "difficulty" (you should change all 3 POW_ parameters to tune the difficulty) +- POW_DIFFICULTY - pow "difficulty" (you should use all 3 POW_ parameters to tune the difficulty) -Add a domain name + backend IP to `haproxy/hosts.map` like: -```plain -localhost 127.0.0.1:81 -``` +#### Run in docker (for testing/development) Run docker compose: ```bash @@ -53,20 +49,27 @@ docker compose up Visit http://localhost -DDoS-protection mode is enabled by default. - #### Installation -Before installing the tool, ensure that HAProxy is built with Lua support and version >=2.5 for the native httpclient support. For Debian and Ubuntu (and -based) distros, see https://haproxy.debian.net/ for packages. +Requires HAProxy compiled with lua support, and version >=2.5 for the native lua httpclient support. For Debian and Ubuntu (and -based) distros, see https://haproxy.debian.net/ for packages. -- Copy [haproxy.cfg](haproxy/haproxy.cfg) to /etc/haproxy - - Edit the `lua-load` directive to be the absolute path to [register.lua](src/scripts/register.lua) - - Edit the paths of challenge.js and worker.js in the `http-request return` directive to the absolut path to the respective files in the haproxy/js folder -- 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). -- Copy the map files from the haproxy folder to /etc/haproxy +- Clone the repo somewhere. `/var/www/haproxy-protection` works. +- Copy [haproxy.cfg](haproxy/haproxy.cfg) to `/etc/haproxy/haproxy.cfg`. + - Please note this configuration is very minimal, and is simply an example configuration for haproxy-protection. You are expected to customise it significantly or otherwise copy the relevant parts into your own haproxy config. +- Copy (preferably link) [scripts](src/scripts) to `/etc/haproxy/scripts`. +- Copy (preferably link) [libs](src/libs) to `/etc/haproxy/libs`. +- Copy the map files from haproxy folder to `/etc/haproxy`. +- Install argon2, and the lua argon2 module with luarocks: +```bash +sudo apt install -y git lua5.3 liblua5.3-dev argon2 libargon2-dev luarocks +sudo git config --global url."https://".insteadOf git:// #don't ask. +sudo luarocks install argon2 +``` +- Test your haproxy config, `sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg`. You should see "Configuration file is valid". -#### Screenshot +If you have problems, read the error messages before opening an issue that is simply a bad configuration. + +#### Screenshots -![captcha](img/captcha.png "captcha mode (pow done asynchronously in background)") ![nocaptcha](img/nocaptcha.png "no captcha mode") +![captcha](img/captcha.png "captcha mode (pow done asynchronously in background)") diff --git a/docker-compose.yml b/docker-compose.yml index 04d843c..37e5e8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,6 @@ version: "3.9" services: -# tor: -# build: -# context: ./ -# dockerfile: tor/Dockerfile + haproxy: network_mode: host ports: @@ -37,13 +34,19 @@ services: - BUCKET_DURATION=43200 - BACKEND_NAME=servers - SERVER_PREFIX=websrv - #- CHALLENGE_INCLUDES_IP=1 + - CHALLENGE_INCLUDES_IP=1 - POW_TIME=1 - POW_KB=6000 - POW_DIFFICULTY=3 + nginx: ports: - 81:80 image: "nginx:latest" volumes: - ./nginx:/usr/share/nginx/html + +# tor: +# build: +# context: ./ +# dockerfile: tor/Dockerfile diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg index 156cbbf..de3bf2c 100644 --- a/haproxy/haproxy.cfg +++ b/haproxy/haproxy.cfg @@ -28,11 +28,17 @@ defaults # server stats-localhost 127.0.0.1:1999 frontend http-in + + # Clearnet http (you'll have to figure out https yourself) bind *:80 + # Or instead, for Tor, to use circuit IDs as "IP": + #bind 127.0.0.1:80 accept-proxy + #option forwardfor + # drop requests with invalid host header - #acl is_existing_vhost hdr(host),lower,map_str(/etc/haproxy/hosts.map) -m found - #http-request silent-drop unless is_existing_vhost + acl is_existing_vhost hdr(host),lower,map_str(/etc/haproxy/hosts.map) -m found + http-request silent-drop unless is_existing_vhost # debug only, /cdn-cgi/trace #http-request return status 200 content-type "text/plain; charset=utf-8" lf-file /etc/haproxy/trace.txt if { path /cdn-cgi/trace } diff --git a/src/libs/utils.lua b/src/libs/utils.lua index af41593..3eff50d 100644 --- a/src/libs/utils.lua +++ b/src/libs/utils.lua @@ -26,7 +26,7 @@ function _M.generate_secret(context, salt, user_key, is_applet) user_agent = context.sf:req_fhdr('user-agent') or "" end - return sha.sha256(salt .. bucket .. ip .. user_key .. user_agent) + return sha.sha3_256(salt .. bucket .. ip .. user_key .. user_agent) end diff --git a/src/scripts/hcaptcha.lua b/src/scripts/hcaptcha.lua index 8b17f1c..2c97004 100644 --- a/src/scripts/hcaptcha.lua +++ b/src/scripts/hcaptcha.lua @@ -180,7 +180,7 @@ function _M.view(applet) -- get the user_key#challenge#sig local user_key = sha.bin_to_hex(randbytes(16)) local challenge_hash = utils.generate_secret(applet, pow_cookie_secret, user_key, true) - local signature = sha.hmac(sha.sha256, hmac_cookie_secret, user_key .. challenge_hash) + local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. challenge_hash) local combined_challenge = user_key .. "#" .. challenge_hash .. "#" .. signature -- define body sections @@ -263,13 +263,14 @@ function _M.view(applet) local user_key = sha.bin_to_hex(randbytes(16)) local user_hash = utils.generate_secret(applet, captcha_cookie_secret, user_key, true) - local signature = sha.hmac(sha.sha256, hmac_cookie_secret, user_key .. user_hash) + local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. user_hash) local combined_cookie = user_key .. "#" .. user_hash .. "#" .. signature applet:add_header( "set-cookie", string.format( - "z_ddos_captcha=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; SameSite=Strict;%s", + "z_ddos_captcha=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s", combined_cookie, + applet.headers['host'][0], secure_cookie_flag ) ) @@ -294,7 +295,7 @@ function _M.view(applet) if given_challenge_hash == generated_challenge_hash then -- regenerate the signature and compare it - local generated_signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash) + local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash) if given_signature == generated_signature then -- do the work with their given answer @@ -308,13 +309,14 @@ function _M.view(applet) if hex_hash_sub == string.rep('0', pow_difficulty) then -- the answer was good, give them a cookie - local signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer) + local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer) local combined_cookie = given_user_key .. "#" .. given_challenge_hash .. "#" .. given_answer .. "#" .. signature applet:add_header( "set-cookie", string.format( - "z_ddos_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; SameSite=Strict;%s", + "z_ddos_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s", combined_cookie, + applet.headers['host'][0], secure_cookie_flag ) ) @@ -377,7 +379,7 @@ function _M.check_captcha_status(txn) return end -- regenerate the signature and compare it - local generated_signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_user_hash) + local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_user_hash) if given_signature == generated_signature then return txn:set_var("txn.captcha_passed", true) end @@ -402,7 +404,7 @@ function _M.check_pow_status(txn) return end -- regenerate the signature and compare it - local generated_signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer) + local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer) if given_signature == generated_signature then return txn:set_var("txn.pow_passed", true) end