Compare commits

...

2 commits

Author SHA1 Message Date
ceebd53626 More redaction on readme and challenge page 2025-04-06 11:56:40 +02:00
07ae82809e keepalive 2025-04-06 09:42:14 +02:00
3 changed files with 64 additions and 11 deletions

View file

@ -1,11 +1,22 @@
# Mesozoa
Intermediate reverse proxy that protects your server from crawlers by forcing the browser to run proof of work.
## Why?
Why not Anubis? Because it provides no build instructions and only supports Docker.
Why not using Realm completely? Because the hook system is useless and only allows filtering.
And because it looked like a fun little project.
## Install
[Install rustup](https://rustup.rs) and a nightly Rust toolchain.
# Build executable at ./target/release/mesozoa
cargo build --release
Must be used behind a reverse proxy providing `X-Forwarded-For`.
## Challenge protocol
@ -14,24 +25,28 @@ Must be used behind a reverse proxy providing `X-Forwarded-For`.
Sent by the server as a cookie.
`secret <- chosen randomly, long term`
`salt <- chosen randomly, not stored`
`timestamp <- UNIX time in seconds, 64 bits, big endian`
`ua <- User-Agent from request header`
`ip <- X-Forwarded-For from request header (client's IP)`
* `secret`: chosen randomly at startup
* `salt`: chosen randomly each time
* `timestamp`: UNIX time in seconds, 64 bits, big endian
* `ua`: `User-Agent` from request header
* `ip`: `X-Forwarded-For` from request header (client's IP)
`set-cookie: mesozoa-challenge=BASE64(salt || timestamp || SHA3-256(secret || salt || timestamp || ip || "/" || ua))`
Where `BASE64` is unpadded.
Where `BASE64` is URL-safe unpadded.
### Challenge verification
Request must contain both cookies `mesozoa-challenge` and `mesozoa-proof`.
Server checks challenge is correct and timestamp not too old.
`cookie: mesozoa-proof=nonce`
`hash = SHA2-256(nonce || challenge)`
Client must find a `nonce` matching `/[0-9a-zA-Z_-]{8}/` such that `hash` starts with at least some number of zeros (in binary representation, MSB-first).
## Security
### Network handling and HTTP parsing
@ -50,6 +65,18 @@ SHA2 (used for PoW) is vulnerable but nonce is at the beginning so this is not a
I would like a better PoW: memory-bound and ideally non-parallel. Cuckoo seems a good candidate.
## Contribution
Patches and forks are welcome. Send an e-mail to `tuxmain ât zettascript ðøt org`.
If people are interested, I may switch to a public forge like Codeberg.
The "A" in GNU AGPL means that if you host a publicly available instance of a modified version, then you should also make the modified source code available to users.
For example, this can take the form of a link to a repository in the challenge page or in the protected website.
As the challenge page's source code is directly distributed by the server, you can modify it independently.
(unless adding a compiled object, like WASM. Then you have to publish its source.)
Configuration file can be modified and kept secret, of course.
## License
[Support me via LiberaPay](https://liberapay.com/tuxmain/donate)

View file

@ -4,9 +4,29 @@
<meta charset="utf-8"/>
<meta name="robots" content="noindex"/>
<title>Antispam working...</title>
<style type="text/css">
html {
text-align: center;
}
@media (prefers-color-scheme: dark) {
html {
background-color: black;
color: white;
}
}
</style>
</head>
<body>
<h1>Fighting crawlers</h1>
<p>
This service is protected against abusive crawlers by a proof of work mechanism.<br/>
We are sorry for the inconvenience.
</p>
<p>
Powered by <a href="https://git.txmn.tk/tuxmain/mesozoa" rel="nofollow">Mesozoa</a>,<br/>
distributed under license GNU AGPL v3 without any warranty.
</p>
<script type="text/javascript">
const target_zeros = 15;
const sha256 = async (input) => {

View file

@ -6,6 +6,7 @@ use http::HeaderLineIterator;
use policy::{CompiledPolicies, Policy};
use rand::Rng;
use realm_syscall::socket2::TcpKeepalive;
use regex::bytes::Regex;
use std::{io::Write, net::SocketAddr, time::Duration};
use tokio::{
@ -234,10 +235,15 @@ async fn main() {
}
async fn do_proxy(pass_addr: SocketAddr, mut client_stream: TcpStream) {
// TODO reuse connections
let keepalive_dur = Duration::from_secs(15);
let mut keepalive = TcpKeepalive::new().with_time(keepalive_dur);
keepalive = TcpKeepalive::with_interval(keepalive, keepalive_dur);
keepalive = TcpKeepalive::with_retries(keepalive, 3);
let pass_socket = realm_syscall::new_tcp_socket(&pass_addr).unwrap();
pass_socket.set_reuse_address(true).ok();
pass_socket.set_tcp_keepalive(&keepalive).ok();
let pass_socket = TcpSocket::from_std_stream(pass_socket.into());