mirror of
https://gitgud.io/fatchan/haproxy-protection.git
synced 2025-05-09 02:05:37 +00:00
Merge branch 'master' into kikeflare
This commit is contained in:
17
README.MD
17
README.MD
@ -39,6 +39,7 @@ NOTE: Use either HCAPTCHA_ or RECAPTHCA_, not both.
|
|||||||
- POW_TIME - argon2 iterations
|
- POW_TIME - argon2 iterations
|
||||||
- POW_KB - argon2 memory usage in KB
|
- POW_KB - argon2 memory usage in KB
|
||||||
- POW_DIFFICULTY - pow "difficulty" (you should use all 3 POW_ parameters to tune the difficulty)
|
- POW_DIFFICULTY - pow "difficulty" (you should use all 3 POW_ parameters to tune the difficulty)
|
||||||
|
- TOR_CONTROL_PORT_PASSWORD - the control port password for tor daemon
|
||||||
|
|
||||||
#### Run in docker (for testing/development)
|
#### Run in docker (for testing/development)
|
||||||
|
|
||||||
@ -69,6 +70,22 @@ sudo luarocks install argon2
|
|||||||
|
|
||||||
If you have problems, read the error messages before opening an issue that is simply a bad configuration.
|
If you have problems, read the error messages before opening an issue that is simply a bad configuration.
|
||||||
|
|
||||||
|
### Tor
|
||||||
|
|
||||||
|
- Check the `bind` line comments. Switch to the one with `accept-proxy` and `option forwardfor`
|
||||||
|
- To generate a tor control port password:
|
||||||
|
```
|
||||||
|
$ tor --hash-password example
|
||||||
|
16:0175C41DDD88C5EA605582C858BC08FA29014215F233479A99FE78EDED
|
||||||
|
```
|
||||||
|
- Set `TOR_CONTROL_PORT_PASSWORD` env var to the same password (NOT the output hash)
|
||||||
|
- Add to your torrc (where xxxx is the output of `tor --hash-password`):
|
||||||
|
```
|
||||||
|
ControlPort 9051
|
||||||
|
HashedControlPassword xxxxxxxxxxxxxxxxx
|
||||||
|
```
|
||||||
|
- Don't forget to restart tor
|
||||||
|
|
||||||
#### Screenshots
|
#### Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
@ -38,6 +38,7 @@ services:
|
|||||||
- POW_TIME=2
|
- POW_TIME=2
|
||||||
- POW_KB=512
|
- POW_KB=512
|
||||||
- POW_DIFFICULTY=25
|
- POW_DIFFICULTY=25
|
||||||
|
- TOR_CONTROL_PORT_PASSWORD=changeme
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
ports:
|
ports:
|
||||||
|
@ -10,10 +10,13 @@ global
|
|||||||
tune.bufsize 51200
|
tune.bufsize 51200
|
||||||
|
|
||||||
defaults
|
defaults
|
||||||
|
log global
|
||||||
mode http
|
mode http
|
||||||
|
option httplog
|
||||||
timeout connect 5000ms
|
timeout connect 5000ms
|
||||||
timeout client 50000ms
|
timeout client 50000ms
|
||||||
timeout server 50000ms
|
timeout server 50000ms
|
||||||
|
timeout tarpit 5000ms
|
||||||
|
|
||||||
#frontend stats-frontend
|
#frontend stats-frontend
|
||||||
# bind *:2000
|
# bind *:2000
|
||||||
@ -47,6 +50,11 @@ frontend http-in
|
|||||||
acl blocked_ip_or_subnet src,map_ip(/etc/haproxy/blocked.map) -m found
|
acl blocked_ip_or_subnet src,map_ip(/etc/haproxy/blocked.map) -m found
|
||||||
http-request deny deny_status 403 if blocked_ip_or_subnet
|
http-request deny deny_status 403 if blocked_ip_or_subnet
|
||||||
|
|
||||||
|
# ratelimit (and for tor, kill circuit) on POST bot-check. legitimate users shouldn't hit this.
|
||||||
|
http-request track-sc0 src table bot_check_post_throttle if { path /bot-check } { method POST }
|
||||||
|
http-request lua.kill-tor-circuit if { sc_http_req_rate(0) gt 1 }
|
||||||
|
http-request tarpit if { sc_http_req_rate(0) gt 1 }
|
||||||
|
|
||||||
# acl for lua check whitelisted IPs/subnets and some excluded paths
|
# acl for lua check whitelisted IPs/subnets and some excluded paths
|
||||||
acl is_excluded src,map_ip(/etc/haproxy/whitelist.map) -m found
|
acl is_excluded src,map_ip(/etc/haproxy/whitelist.map) -m found
|
||||||
acl is_excluded path /favicon.ico #add more
|
acl is_excluded path /favicon.ico #add more
|
||||||
@ -74,9 +82,10 @@ frontend http-in
|
|||||||
# check pow/captcha and show page if necessary
|
# check pow/captcha and show page if necessary
|
||||||
acl on_captcha_url path /bot-check
|
acl on_captcha_url path /bot-check
|
||||||
http-request use-service lua.hcaptcha-view if on_captcha_url !is_excluded
|
http-request use-service lua.hcaptcha-view if on_captcha_url !is_excluded
|
||||||
|
|
||||||
|
# challenge decisions, checking, and redirecting to /bot-check
|
||||||
http-request lua.decide-checks-necessary if !is_excluded !on_captcha_url ddos_mode_enabled
|
http-request lua.decide-checks-necessary if !is_excluded !on_captcha_url ddos_mode_enabled
|
||||||
# global override enabled pow-check only, uncomment the OR to also do hcaptcha-check
|
http-request lua.hcaptcha-check if !is_excluded !on_captcha_url validate_captcha
|
||||||
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 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
|
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
|
||||||
|
|
||||||
@ -104,6 +113,9 @@ backend servers
|
|||||||
# use server based on hostname
|
# use server based on hostname
|
||||||
use-server %[req.hdr(host),lower,map(/etc/haproxy/backends.map)] if TRUE
|
use-server %[req.hdr(host),lower,map(/etc/haproxy/backends.map)] if TRUE
|
||||||
|
|
||||||
|
backend bot_check_post_throttle
|
||||||
|
stick-table type ipv6 size 100k expire 60s store http_req_rate(60s)
|
||||||
|
|
||||||
backend hcaptcha
|
backend hcaptcha
|
||||||
mode http
|
mode http
|
||||||
server hcaptcha hcaptcha.com:443
|
server hcaptcha hcaptcha.com:443
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
function insertError(str) {
|
function insertError(str) {
|
||||||
const ring = document.querySelector('.lds-ring');
|
const ring = document.querySelector('.lds-ring');
|
||||||
ring.insertAdjacentHTML('afterend', `<p class="red">Error: ${str}</p>`);
|
const captcha = document.querySelector('#captcha');
|
||||||
ring.remove();
|
(ring || captcha).insertAdjacentHTML('afterend', `<p class="red">Error: ${str}</p>`);
|
||||||
|
ring && ring.remove();
|
||||||
|
captcha && captcha.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishRedirect() {
|
function finishRedirect() {
|
||||||
|
@ -3,7 +3,9 @@ local _M = {}
|
|||||||
local sha = require("sha")
|
local sha = require("sha")
|
||||||
local secret_bucket_duration = tonumber(os.getenv("BUCKET_DURATION"))
|
local secret_bucket_duration = tonumber(os.getenv("BUCKET_DURATION"))
|
||||||
local challenge_includes_ip = os.getenv("CHALLENGE_INCLUDES_IP")
|
local challenge_includes_ip = os.getenv("CHALLENGE_INCLUDES_IP")
|
||||||
|
local tor_control_port_password = os.getenv("TOR_CONTROL_PORT_PASSWORD")
|
||||||
|
|
||||||
|
-- generate the challenge hash/user hash
|
||||||
function _M.generate_secret(context, salt, user_key, is_applet)
|
function _M.generate_secret(context, salt, user_key, is_applet)
|
||||||
|
|
||||||
-- time bucket for expiry
|
-- time bucket for expiry
|
||||||
@ -30,14 +32,16 @@ function _M.generate_secret(context, salt, user_key, is_applet)
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- split string by delimiter
|
||||||
function _M.split(inputstr, sep)
|
function _M.split(inputstr, sep)
|
||||||
local t = {}
|
local t = {}
|
||||||
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
for str in string.gmatch(inputstr, "([^"..sep.."]*)") do
|
||||||
table.insert(t, str)
|
table.insert(t, str)
|
||||||
end
|
end
|
||||||
return t
|
return t
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- return true if hash passes difficulty
|
||||||
function _M.checkdiff(hash, diff)
|
function _M.checkdiff(hash, diff)
|
||||||
local i = 1
|
local i = 1
|
||||||
for j = 0, (diff-8), 8 do
|
for j = 0, (diff-8), 8 do
|
||||||
@ -51,5 +55,15 @@ function _M.checkdiff(hash, diff)
|
|||||||
return (lnm & msk) == 0
|
return (lnm & msk) == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- connect to the tor control port and instruct it to close a circuit
|
||||||
|
function _M.send_tor_control_port(circuit_identifier)
|
||||||
|
local tcp = core.tcp();
|
||||||
|
tcp:settimeout(1);
|
||||||
|
tcp:connect("127.0.0.1", 9051);
|
||||||
|
-- not buffered, so we are better off sending it all at once
|
||||||
|
tcp:send('AUTHENTICATE "' .. tor_control_port_password .. '"\nCLOSECIRCUIT ' .. circuit_identifier ..'\n')
|
||||||
|
tcp:close()
|
||||||
|
end
|
||||||
|
|
||||||
return _M
|
return _M
|
||||||
|
|
||||||
|
@ -133,10 +133,10 @@ local noscript_extra_template = [[
|
|||||||
<code style="word-break: break-all;">
|
<code style="word-break: break-all;">
|
||||||
echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s
|
echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s
|
||||||
</code>
|
</code>
|
||||||
<li>Paste the output from the script into the box and submit:
|
<li>Paste the script output into the box and submit:
|
||||||
<form method="POST">
|
<form method="post">
|
||||||
<textarea type="text" name="pow_response"></textarea>
|
<textarea name="pow_response" placeholder="script output" required></textarea>
|
||||||
<input type="submit" value="submit" />
|
<div><input type="submit" value="submit" /></div>
|
||||||
</form>
|
</form>
|
||||||
</ol>
|
</ol>
|
||||||
</details>
|
</details>
|
||||||
@ -145,7 +145,7 @@ local noscript_extra_template = [[
|
|||||||
-- title with favicon and hostname
|
-- title with favicon and hostname
|
||||||
local site_name_section_template = [[
|
local site_name_section_template = [[
|
||||||
<h3 class="pt">
|
<h3 class="pt">
|
||||||
<img src="/favicon.ico" width="32" height="32">
|
<img src="/favicon.ico" width="32" height="32" alt="favicon">
|
||||||
%s
|
%s
|
||||||
</h3>
|
</h3>
|
||||||
]]
|
]]
|
||||||
@ -171,6 +171,24 @@ local captcha_section_template = [[
|
|||||||
</div>
|
</div>
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
-- kill a tor circuit
|
||||||
|
function _M.kill_tor_circuit(txn)
|
||||||
|
local ip = txn.sf:src()
|
||||||
|
if ip:sub(1,19) ~= "fc00:dead:beef:4dad" then
|
||||||
|
return -- not a tor circuit id/ip. we shouldn't get here, but just in case.
|
||||||
|
end
|
||||||
|
-- split the IP, take the last 2 sections
|
||||||
|
local split_ip = utils.split(ip, ":")
|
||||||
|
local aa_bb = split_ip[5] or "0000"
|
||||||
|
local cc_dd = split_ip[6] or "0000"
|
||||||
|
aa_bb = string.rep("0", 4 - #aa_bb) .. aa_bb
|
||||||
|
cc_dd = string.rep("0", 4 - #cc_dd) .. cc_dd
|
||||||
|
-- convert the last 2 sections to a number from hex, which makes the circuit ID
|
||||||
|
local circuit_identifier = tonumber(aa_bb..cc_dd, 16)
|
||||||
|
print('Closing Tor circuit ID: '..circuit_identifier..', "IP": '..ip)
|
||||||
|
utils.send_tor_control_port(circuit_identifier)
|
||||||
|
end
|
||||||
|
|
||||||
function _M.view(applet)
|
function _M.view(applet)
|
||||||
|
|
||||||
-- set response body and declare status code
|
-- set response body and declare status code
|
||||||
@ -223,6 +241,9 @@ function _M.view(applet)
|
|||||||
-- if request is POST, check the answer to the pow/cookie
|
-- if request is POST, check the answer to the pow/cookie
|
||||||
elseif applet.method == "POST" then
|
elseif applet.method == "POST" then
|
||||||
|
|
||||||
|
-- if they fail, set a var for use in ACLs later
|
||||||
|
local valid_submission = false
|
||||||
|
|
||||||
-- parsed POST body
|
-- parsed POST body
|
||||||
local parsed_body = url.parseQuery(applet.receive(applet))
|
local parsed_body = url.parseQuery(applet.receive(applet))
|
||||||
|
|
||||||
@ -232,9 +253,58 @@ function _M.view(applet)
|
|||||||
secure_cookie_flag = ""
|
secure_cookie_flag = ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- handle setting the POW cookie
|
||||||
|
local user_pow_response = parsed_body["pow_response"]
|
||||||
|
if user_pow_response then
|
||||||
|
|
||||||
|
-- split the response up (makes the nojs submission easier because it can be a single field)
|
||||||
|
local split_response = utils.split(user_pow_response, "#")
|
||||||
|
|
||||||
|
if #split_response == 4 then
|
||||||
|
local given_user_key = split_response[1]
|
||||||
|
local given_challenge_hash = split_response[2]
|
||||||
|
local given_signature = split_response[3]
|
||||||
|
local given_answer = split_response[4]
|
||||||
|
|
||||||
|
-- regenerate the challenge and compare it
|
||||||
|
local generated_challenge_hash = utils.generate_secret(applet, pow_cookie_secret, given_user_key, true)
|
||||||
|
if given_challenge_hash == generated_challenge_hash then
|
||||||
|
|
||||||
|
-- regenerate the signature and compare it
|
||||||
|
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
|
||||||
|
local full_hash = argon2.hash_encoded(given_challenge_hash .. given_answer, given_user_key)
|
||||||
|
|
||||||
|
-- check the output is correct
|
||||||
|
local hash_output = utils.split(full_hash, '$')[6]:sub(0, 43) -- https://github.com/thibaultcha/lua-argon2/issues/37
|
||||||
|
local hex_hash_output = sha.bin_to_hex(sha.base64_to_bin(hash_output));
|
||||||
|
if utils.checkdiff(hex_hash_output, pow_difficulty) then
|
||||||
|
|
||||||
|
-- the answer was good, give them a cookie
|
||||||
|
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=/; Domain=.%s; SameSite=Strict;%s",
|
||||||
|
combined_cookie,
|
||||||
|
applet.headers['host'][0],
|
||||||
|
secure_cookie_flag
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_submission = true
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- handle setting the captcha cookie
|
-- handle setting the captcha cookie
|
||||||
local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"]
|
local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"]
|
||||||
if user_captcha_response then
|
if valid_submission and user_captcha_response then -- only check captcha if POW is already correct
|
||||||
-- format the url for verifying the captcha response
|
-- format the url for verifying the captcha response
|
||||||
local captcha_url = string.format(
|
local captcha_url = string.format(
|
||||||
"https://%s%s",
|
"https://%s%s",
|
||||||
@ -277,56 +347,13 @@ function _M.view(applet)
|
|||||||
secure_cookie_flag
|
secure_cookie_flag
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
valid_submission = valid_submission and true
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- handle setting the POW cookie
|
if not valid_submission then
|
||||||
local user_pow_response = parsed_body["pow_response"]
|
_M.kill_tor_circuit(applet)
|
||||||
if user_pow_response then
|
|
||||||
|
|
||||||
-- split the response up (makes the nojs submission easier because it can be a single field)
|
|
||||||
local split_response = utils.split(user_pow_response, "#")
|
|
||||||
if #split_response == 4 then
|
|
||||||
local given_user_key = split_response[1]
|
|
||||||
local given_challenge_hash = split_response[2]
|
|
||||||
local given_signature = split_response[3]
|
|
||||||
local given_answer = split_response[4]
|
|
||||||
|
|
||||||
-- regenerate the challenge and compare it
|
|
||||||
local generated_challenge_hash = utils.generate_secret(applet, pow_cookie_secret, given_user_key, true)
|
|
||||||
if given_challenge_hash == generated_challenge_hash then
|
|
||||||
|
|
||||||
-- regenerate the signature and compare it
|
|
||||||
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
|
|
||||||
local full_hash = argon2.hash_encoded(given_challenge_hash .. given_answer, given_user_key)
|
|
||||||
|
|
||||||
-- check the output is correct
|
|
||||||
local hash_output = utils.split(full_hash, '$')[5]:sub(0, 43) -- https://github.com/thibaultcha/lua-argon2/issues/37
|
|
||||||
local hex_hash_output = sha.bin_to_hex(sha.base64_to_bin(hash_output));
|
|
||||||
|
|
||||||
if utils.checkdiff(hex_hash_output, pow_difficulty) then
|
|
||||||
|
|
||||||
-- the answer was good, give them a cookie
|
|
||||||
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=/; Domain=.%s; SameSite=Strict;%s",
|
|
||||||
combined_cookie,
|
|
||||||
applet.headers['host'][0],
|
|
||||||
secure_cookie_flag
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- redirect them to their desired page in applet.qs (query string)
|
-- redirect them to their desired page in applet.qs (query string)
|
||||||
|
@ -6,4 +6,5 @@ core.register_service("hcaptcha-view", "http", hcaptcha.view)
|
|||||||
core.register_action("hcaptcha-check", { 'http-req', }, hcaptcha.check_captcha_status)
|
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("pow-check", { 'http-req', }, hcaptcha.check_pow_status)
|
||||||
core.register_action("decide-checks-necessary", { 'http-req', }, hcaptcha.decide_checks_necessary)
|
core.register_action("decide-checks-necessary", { 'http-req', }, hcaptcha.decide_checks_necessary)
|
||||||
|
core.register_action("kill-tor-circuit", { 'http-req', }, hcaptcha.kill_tor_circuit)
|
||||||
core.register_init(hcaptcha.setup_servers)
|
core.register_init(hcaptcha.setup_servers)
|
||||||
|
Reference in New Issue
Block a user