Skip to content

Spam Prevention

The crate.spam package bundles a set of independent, composable helpers for filtering bots and abusive traffic. Each module does one thing and can be used on its own — mix and match to match your threat model.

ModulePurpose
crate.spam.antispamClient IP extraction, simple bot heuristics, tarpit, offense tracking
crate.spam.rateLimitFixed-window per-key rate limiter
crate.spam.domainCheckCached DNS existence check for a domain
crate.spam.dnsblDNSBL (Spamhaus, SpamCop, …) lookup
crate.spam.stopForumSpamStopForumSpam blocklist lookup
crate.spam.altchaALTCHA proof-of-work challenge — see ALTCHA Module
crate.spam.spamfilterHeuristic classification of form fields — see Spam Filter

All caching helpers use TimedCache, so lookups only hit the network (or DNS resolver) on cache misses.

Helpers that run at the HTTP edge: figure out who the client actually is, decide whether it smells like a bot, optionally stall the caller, and remember repeat offenders.

import crate.spam.antispam;

Returns the real client IP, honoring X-Forwarded-For only when the immediate peer is in the trusted-proxy list (or is on a private network when trustPrivateNetworks is true). Untrusted peers never get to inject headers.

string extractClientIp(HTTPServerRequest req, ProxyConfig proxy);
string extractClientIp(HTTPServerRequest req, const string[] trustedProxies = [],
bool trustPrivateNetworks = false);
auto ip = extractClientIp(req, ProxyConfig(["10.0.0.1"], true));
FieldTypeDefaultDescription
trustedProxiesconst(string[])[]Peer addresses whose X-Forwarded-For we honor
trustPrivateNetworksboolfalseAlso trust any peer on RFC1918 / ULA / loopback

Returns true for loopback (127.0.0.0/8, ::1), RFC1918 IPv4 (10/8, 172.16/12, 192.168/16), IPv6 ULA (fc00::/7), and link-local IPv6 (fe80::/10). Useful outside the proxy flow too.

bool isPrivateAddress(string ip);

Cheap heuristic: flags the request as a bot if it’s missing Accept-Language, missing User-Agent, or the UA matches any of defaultBotUaPatterns (python-requests, curl/, go-http-client, okhttp). Pass your own patterns to override.

bool isBotRequest(HTTPServerRequest req, const string[] userAgentPatterns = defaultBotUaPatterns);

Sleeps for config.base + random(0, jitterSeconds) and then writes a 204 No Content. Used to waste a detected bot’s time without signaling the block.

void tarpit(HTTPServerResponse res, TarpitConfig config);

TarpitConfig fields: base (default 25.seconds), jitterSeconds (default 5).

Thin wrapper over a TimedCache!bool. Mark IPs that triggered any of your spam rules, then check future requests against the same cache to decide whether to tarpit or outright block.

auto tracker = new OffenseTracker(new TimedCache!bool(null, 1.hours, 5000));
if(tracker.isKnown(ip)) {
tarpit(res, TarpitConfig());
return;
}
if(spamFilter.classify(fields, config).isSpam) {
tracker.mark(ip);
}

matchesBotUserAgent / parseForwardedForFirstHop

Section titled “matchesBotUserAgent / parseForwardedForFirstHop”

Lower-level helpers used by isBotRequest and extractClientIp. Exposed so you can reuse them in custom middleware.

import crate.spam.rateLimit;

Fixed-window limiter keyed by arbitrary string (IP, user id, endpoint, …). Each key gets limit allowances per window; the count resets on the first call after the window elapses.

auto rl = new RateLimiter(10, 1.minutes);
if(!rl.allow(clientIp)) {
res.statusCode = 429;
return;
}
Constructor argDefaultDescription
limitrequiredMax calls per window
windowrequiredDuration of the window
maxEntries100_000Upper bound before a reap pass cleans stale keys

Empty keys always pass (fail-open — useful when extractClientIp can’t determine a value).

import crate.spam.domainCheck;

Checks whether a domain has an A/AAAA record. Positive and negative results are cached; transient DNS failures fail open (return true and are not cached) so a flaky resolver doesn’t lock out legitimate domains for the whole TTL.

auto checker = new DomainChecker(10.minutes, 1000, 2.seconds);
if(!checker.exists(extractDomain(email))) {
return badRequest("Unknown email domain");
}
Constructor argDefaultDescription
ttl10.minutesCache lifetime for positive/negative results
maxSize1000Max cached entries
dnsTimeout2.secondsPer-lookup DNS timeout

A second constructor accepts a bool delegate(string) lookup for testing.

Result semantics:

  • Domain resolves → true (cached)
  • Definitive NXDOMAIN / “host not found” → false (cached)
  • Timeout / network / other error → true (not cached)
import crate.spam.dnsbl;

Classic DNSBL: reverse the IPv4 octets, append a blocklist zone (zen.spamhaus.org, bl.spamcop.net, …), try to resolve. A successful resolution means “listed.” Returns on the first listed blocklist. IPv6 is not supported by most free DNSBLs and is treated as not-listed.

auto dnsbl = new DnsblChecker(["zen.spamhaus.org", "bl.spamcop.net"]);
if(dnsbl.isListed(clientIp)) {
tarpit(res, TarpitConfig());
return;
}
Constructor argDefaultDescription
blocklists["zen.spamhaus.org"]DNSBL zones to query
ttl1.hoursCache lifetime
maxSize10_000Max cached entries
dnsTimeout2.secondsPer-lookup DNS timeout

Returns "4.3.2.1" for "1.2.3.4", or "" if the input isn’t a well-formed IPv4 string.

import crate.spam.stopForumSpam;

Queries the free StopForumSpam HTTP API with an IP and optional email. Results are cached (default 6 hours) to stay within the free-tier rate limit. Fails open — network errors, non-200 responses, and parse failures all return “not listed.”

auto sfs = new StopForumSpamChecker();
if(sfs.isSpam(clientIp, submittedEmail)) {
return forbidden();
}
Constructor argDefaultDescription
apiUrl"https://api.stopforumspam.org/api"API endpoint
ttl6.hoursCache lifetime
maxSize10_000Max cached entries

A second constructor accepts a string delegate(string url) fetcher for testing.

A middleware that layers several checks — cheap ones first, expensive ones last:

import crate.spam.antispam;
import crate.spam.rateLimit;
import crate.spam.domainCheck;
import crate.spam.dnsbl;
import crate.spam.stopForumSpam;
auto proxy = ProxyConfig(["10.0.0.1"], true);
auto rl = new RateLimiter(20, 1.minutes);
auto domains = new DomainChecker();
auto dnsbl = new DnsblChecker();
auto sfs = new StopForumSpamChecker();
auto tracker = new OffenseTracker(new TimedCache!bool(null, 1.hours, 5000));
void guard(HTTPServerRequest req, HTTPServerResponse res) {
auto ip = extractClientIp(req, proxy);
if(tracker.isKnown(ip) || isBotRequest(req)) {
tarpit(res, TarpitConfig());
return;
}
if(!rl.allow(ip)) {
res.statusCode = 429;
return;
}
if(dnsbl.isListed(ip) || sfs.isSpam(ip)) {
tracker.mark(ip);
tarpit(res, TarpitConfig());
return;
}
// proceed to the real handler
}