diff --git a/README.MD b/README.MD index ef3ee6c..34de653 100644 --- a/README.MD +++ b/README.MD @@ -1,25 +1,22 @@ -## HaProxy DDoS protection system PoC +## HAProxy DDoS protection system -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 holding page where users solve a captcha (think cloudflare CDN). -Intended to stop bots, spam, probably some forms of ddos, etc. +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, etc. -Some issues fixed and various improvements: +Will soon™ be accompanied by a control panel allowing to manage clusters of servers with this installed. Allowing you to add/remove/edit domains, protection rules, blocked ips, backend server IPs, etc during runtime. -- Add a proof-of-work element to the bot-check page as an optional weaker but more user-friendly mode -- Add more options to CLI for nocaptcha -- Add examples and support for .onion/tor using the haproxy PROXY protocol to provide some kind of "ip" discrimination of tor users (circuit identifiers) -- Add serving javascript files directly from haproxy with http-request return, so no extra backend is needed -- Improved the appearance of the challenge page -- Fix a lot of bugs -- 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 +Improvements in this fork: -#### Screenshot - -![captcha](img/captcha.png "captcha mode (pow done asynchronously in background)") -![nocaptcha](img/nocaptcha.png "no captcha mode") +- 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 dns resolution hack, use proper backend address. +- 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. +- Ability to enable globally, per-domain or per-domain+path, with paths taking priority. +- Include dataplaneapi, to sync map files to disk if edited during runtime. +- Many bugfixes. #### How to test @@ -29,17 +26,22 @@ Add some env vars to docker-compose file: - HCAPTCHA_SECRET - your hcaptcha secret key - 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 +- RAY_ID - string to identify the HAProxy node by - BUCKET_DURATION - how long between bucket changes, invalidating cookies - BACKEND_NAME - name of backend to build from hosts.map -- SERVER_PREFIX - prefix of server names i.e. n where n is the number, in server-template +- SERVER_PREFIX - prefix of server names used in server-template + +Add a domain name + backend IP to `haproxy/hosts.map` like: +```plain +localhost 127.0.0.1:81 +``` Run docker compose: ```bash docker compose up ``` -- visit *http://127.0.0.1* +Visit http://localhost DDoS-protection mode is enabled by default. @@ -47,35 +49,15 @@ DDoS-protection mode is enabled by default. 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 [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 sha1.js and worker.js in the `http-request return` directive to the absolut path to the respective files in the haproxy/js folder +- Copy [dataplaneapi.hcl](haproxy/dataplaneapi.hcl) to /etc/haproxy - 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 or paths with protection mode enabled +- Copy the map files from the haproxy folder to /etc/haproxy -#### CLI -The system comes with CLI. It can be used to manage protection global/per-domain and control nocaptcha mode. -Ensure that stat socket is configured in HaProxy for CLI support. +#### Screenshot -```bash -Usage: ddos-cli [options] - -Command line interface to manage per-domain and global DDoS protection. - -optional arguments: - -h, --help Show this help message and exit. - -Commands: - Global management: - src/cli/ddos-cli global status Show status of global server ddos mode. - src/cli/ddos-cli global enable Enable global ddos mode. - src/cli/ddos-cli global disable Disable global ddos mode. - - Domain management: - src/cli/ddos-cli domain list List all domains with ddos mode on. - src/cli/ddos-cli domain nocaptcha List all domains with nocaptcha mode on. - src/cli/ddos-cli domain status Get ddos mode status for a domain. - src/cli/ddos-cli domain enable Enable ddos mode for a domain. - src/cli/ddos-cli domain disable Disable ddos mode for a domain. - src/cli/ddos-cli domain mode Toggle nocaptcha mode for a domain. - - ``` +![captcha](img/captcha.png "captcha mode (pow done asynchronously in background)") +![nocaptcha](img/nocaptcha.png "no captcha mode") diff --git a/docs/interaction_diagram.txt b/docs/interaction_diagram.txt deleted file mode 100644 index 5ec9a11..0000000 --- a/docs/interaction_diagram.txt +++ /dev/null @@ -1,32 +0,0 @@ -@startuml -actor Browser as user -participant Proxy as proxy -participant "Captcha Provider" as captcha -participant "Backend" as backend -user -> proxy: Request /resource?foo=bar -activate proxy -proxy -> user: Redirect /{captcha_url} -deactivate proxy - -user -> captcha: Submit challenge -activate captcha -captcha -> user: Pass challenge result hash -deactivate captcha -user -> proxy: Submit captcha form - -activate proxy -proxy -> captcha: Validate challenge results -activate captcha -captcha -> proxy: Pass validation results -deactivate captcha -proxy -> user: Redirect /resource?foo=bar -deactivate proxy -user -> proxy: Request /resource?foo=bar -activate proxy -proxy -> backend: Request /resource?foo=bar -activate backend -backend -> proxy: Serve /resource?foo=bar -deactivate backend -proxy -> user: Serve /resource?foo=bar -deactivate proxy -@enduml \ No newline at end of file diff --git a/src/cli/ddos-cli b/src/cli/ddos-cli deleted file mode 100755 index 52f8c31..0000000 --- a/src/cli/ddos-cli +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env bash - -HAPROXY_DDOS_DOMAINS_FILE="/etc/haproxy/ddos.map" -HAPROXY_NOCAPTCHA_DOMAINS_FILE="/etc/haproxy/no_captcha.map" -HAPROXY_GLOBAL_ACL="hdr_cnt" -HAPROXY_SOCKET="/var/run/haproxy.sock" -SOCAT="$(which socat)" - -DOMAIN_REGEX='(?=^.{5,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)' - -_h_show_acl() { - local cmd - if [[ ${1} ]]; then - cmd="show acl #${1}" - else - cmd="show acl" - fi - echo "${cmd}" | ${SOCAT} ${HAPROXY_SOCKET} stdio -} - -_h_add_acl() { - echo "add acl #${1} ${2}" | ${SOCAT} ${HAPROXY_SOCKET} stdio -} - -_h_del_acl() { - echo "del acl #${1} ${2}" | ${SOCAT} ${HAPROXY_SOCKET} stdio -} - -_h_show_map() { - local cmd - if [[ ${1} ]]; then - cmd="show map #${1}" - else - cmd="show map" - fi - echo "${cmd}" | ${SOCAT} ${HAPROXY_SOCKET} stdio -} - -_h_add_map() { - echo "add map #${1} ${2} ${2}" | ${SOCAT} ${HAPROXY_SOCKET} stdio -} - -_h_del_map() { - echo "del map #${1} ${2}" | ${SOCAT} ${HAPROXY_SOCKET} stdio -} - -_help() { -/bin/cat < [options] - -Command line interface to manage per-domain and global DDoS protection. - -optional arguments: - -h, --help Show this help message and exit. - -Commands: - Global management: - $0 global status Show status of global server ddos mode. - $0 global enable Enable global ddos mode. - $0 global disable Disable global ddos mode. - - Domain management: - $0 domain list List all domains with ddos mode on. - $0 domain nocaptcha List all domains with nocaptcha mode on. - $0 domain status Get ddos mode status for a domain. - $0 domain enable Enable ddos mode for a domain. - $0 domain disable Disable ddos mode for a domain. - $0 domain mode Toggle nocaptcha mode for a domain. -EOF -} - -_ensure_domain_passed() { - if ! [[ ${1} ]]; then - echo "Error: 'domain' argument is required for this action" - _help - exit 1 - elif ! echo "${1}" | grep -qP "${DOMAIN_REGEX}"; then - echo "Error: '${1}' is not a valid domain" - _help - exit 1 - fi -} - -_domain_list() { - local domain_acl_id - domain_acl_id=$(_h_show_acl | grep ${HAPROXY_DDOS_DOMAINS_FILE} | cut -d' ' -f1) - _h_show_acl "${domain_acl_id}" | cut -d' ' -f2 -} - -_nocaptcha_list() { - local nocaptcha_map_id - nocaptcha_map_id=$(_h_show_map | grep ${HAPROXY_NOCAPTCHA_DOMAINS_FILE} | cut -d' ' -f1) - _h_show_map "${nocaptcha_map_id}" | cut -d' ' -f2 -} - -_domain_status() { - local ddos_domains - local nocaptcha_domains - local global_ddos_acl_id - local global_ddos_status - - ddos_domains="$(_domain_list)" - nocaptcha_domains="$(_nocaptcha_list)" - global_ddos_acl_id=$(_h_show_acl | grep ${HAPROXY_GLOBAL_ACL} | cut -d' ' -f1) - global_ddos_status=$(_h_show_acl "${global_ddos_acl_id}" | cut -d' ' -f2) - - - if echo "${ddos_domains}" | grep -q "^${1}$"; then - echo "DDoS-protection mode is enabled for ${1}" - else - echo "DDoS-protection mode is disabled for ${1}" - if [[ ${global_ddos_status} -eq 0 ]]; then - echo "ATTENTION: DDoS-protection mode is enabled globally" - fi - fi - if echo "${nocaptcha_domains}" | grep -q "^${1}$"; then - echo "Nocaptcha mode is enabled for ${1}" - fi -} - -_domain_enable() { - local ddos_domains - local domain_acl_id - - ddos_domains="$(_domain_list)" - - if echo "${ddos_domains}" | grep -q "^${1}$"; then - echo "DDoS-protection mode is already enabled for ${1}" - exit 0 - fi - - domain_acl_id=$(_h_show_acl | grep ${HAPROXY_DDOS_DOMAINS_FILE} | cut -d' ' -f1) - _h_add_acl "${domain_acl_id}" "${1}" &>/dev/null - - if ! grep -q "^${1}$" ${HAPROXY_DDOS_DOMAINS_FILE}; then - echo "${1}" >> ${HAPROXY_DDOS_DOMAINS_FILE} - fi - echo "DDoS-protection mode was enabled for ${1}" -} - -_domain_changemode() { - local nocaptcha_domains - local domain_map_id - - nocaptcha_domains="$(_nocaptcha_list)" - - domain_map_id=$(_h_show_acl | grep ${HAPROXY_NOCAPTCHA_DOMAINS_FILE} | cut -d' ' -f1) - if echo "${nocaptcha_domains}" | grep -q "^${1}$"; then - _h_del_map "${domain_acl_id}" "${1}" &>/dev/null - if grep -q "^${1}$" ${HAPROXY_NOCAPTCHA_DOMAINS_FILE}; then - sed -i "/^${1}$/d" ${HAPROXY_NOCAPTCHA_DOMAINS_FILE} - fi - echo "Nocaptcha mode was disabled for ${1}" - exit 0 - fi - _h_add_map "${domain_acl_id}" "${1}" &>/dev/null - if ! grep -q "^${1}$" ${HAPROXY_NOCAPTCHA_DOMAINS_FILE}; then - echo "${1}" >> ${HAPROXY_NOCAPTCHA_DOMAINS_FILE} - fi - echo "Nocaptcha mode was enabled for ${1}" -} - -_domain_disable() { - local ddos_domains - local domain_acl_id - - ddos_domains="$(_domain_list)" - - if ! echo "${ddos_domains}" | grep -q "^${1}$"; then - echo "DDoS-protection mode is already disabled for ${1}" - exit 0 - fi - - domain_acl_id=$(_h_show_acl | grep ${HAPROXY_DDOS_DOMAINS_FILE} | cut -d' ' -f1) - _h_del_acl "${domain_acl_id}" "${1}" &>/dev/null - - if grep -q "^${1}$" ${HAPROXY_DDOS_DOMAINS_FILE}; then - sed -i "/^${1}$/d" ${HAPROXY_DDOS_DOMAINS_FILE} - fi - echo "DDoS-protection mode was disabled for ${1}" -} - -_global_status() { - local global_ddos_acl_id - local global_ddos_status - - global_ddos_acl_id=$(_h_show_acl | grep ${HAPROXY_GLOBAL_ACL} | head -1 | cut -d' ' -f1) - global_ddos_status=$(_h_show_acl "${global_ddos_acl_id}" | cut -d' ' -f2) - - if [[ ${global_ddos_status} -eq 0 ]]; then - echo "DDoS-protection mode is enabled globally" - else - echo "DDoS-protection mode is disabled globally" - fi -} - -_global_enable() { - declare -a global_ddos_acl_ids - local global_ddos_status - - global_ddos_acl_ids=($(_h_show_acl | grep ${HAPROXY_GLOBAL_ACL} | cut -d' ' -f1)) - global_ddos_status=$(_h_show_acl "${global_ddos_acl_ids[0]}" | cut -d' ' -f2) - - if [[ ${global_ddos_status} -eq 0 ]]; then - echo "DDoS-protection mode is already enabled globally" - exit 0 - fi - - for id in "${global_ddos_acl_ids[@]}"; do - _h_add_acl "${id}" 0 &>/dev/null - _h_del_acl "${id}" 1 &>/dev/null - done - echo "DDoS-protection mode was enabled globally" -} - -_global_disable() { - declare -a global_ddos_acl_ids - local global_ddos_status - - global_ddos_acl_ids=($(_h_show_acl | grep ${HAPROXY_GLOBAL_ACL} | cut -d' ' -f1)) - global_ddos_status=$(_h_show_acl "${global_ddos_acl_ids[0]}" | cut -d' ' -f2) - - if [[ ${global_ddos_status} -eq 1 ]]; then - echo "DDoS-protection mode is already disabled globally" - exit 0 - fi - - for id in "${global_ddos_acl_ids[@]}"; do - _h_add_acl "${id}" 1 &>/dev/null - _h_del_acl "${id}" 0 &>/dev/null - done - echo "DDoS-protection mode was disabled globally" -} - -_handle_global_management() { - case ${1} in - status) _global_status;; - enable) _global_enable;; - disable) _global_disable;; - *) _help; exit 1;; - esac -} - -_handle_domain_management() { - case ${1} in - list) _domain_list;; - nocaptcha) _nocaptcha_list;; - status) - _ensure_domain_passed "${2}" - _domain_status "${2}";; - enable) - _ensure_domain_passed "${2}" - _domain_enable "${2}";; - disable) - _ensure_domain_passed "${2}" - _domain_disable "${2}";; - mode) - _ensure_domain_passed "${2}" - _domain_changemode "${2}";; - *) _help; exit 1;; - esac -} - -if ! [[ "${*}" ]]; then - _help - exit 1 -fi - -for i in "${@}"; do - case ${i} in - -h|--help) _help; exit 0;; - domain) MODE=DOMAIN; shift; break;; - global) MODE=GLOBAL; shift; break;; - *) _help; exit 1;; - esac -done - -case ${MODE} in - DOMAIN) _handle_domain_management "${@}";; - GLOBAL) _handle_global_management "${@}";; -esac