Files
crowdsec-bunny-bouncer/edge-script/index.ts
kappa da199bce8c Switch to Edge Script + Bunny Database architecture for unlimited IP blocking
Replace Shield Access List (5,000 IP limit) with Bunny Database (libSQL) +
Edge Script middleware to support CAPI community blocklists (tens of thousands
of IPs). Bouncer now uses CrowdSec streaming API for incremental sync with
periodic full resync every 6 hours.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 09:25:43 +09:00

92 lines
2.6 KiB
TypeScript

import { createClient } from "@libsql/client/web";
import * as BunnySDK from "@bunny.net/edgescript-sdk";
import process from "node:process";
// ---------------------------------------------------------------------------
// Database client
// ---------------------------------------------------------------------------
const db = createClient({
url: process.env.DB_URL!,
authToken: process.env.DB_TOKEN!,
});
// ---------------------------------------------------------------------------
// In-memory cache (per edge node, 60s TTL)
// ---------------------------------------------------------------------------
interface CacheEntry {
blocked: boolean;
expiresAt: number;
}
const cache = new Map<string, CacheEntry>();
const CACHE_TTL_MS = 60_000;
const CACHE_MAX_SIZE = 50_000;
function cacheGet(ip: string): boolean | null {
const entry = cache.get(ip);
if (!entry) return null;
if (entry.expiresAt <= Date.now()) {
cache.delete(ip);
return null;
}
return entry.blocked;
}
function cacheSet(ip: string, blocked: boolean): void {
if (cache.size >= CACHE_MAX_SIZE) {
// Evict expired entries first
const now = Date.now();
for (const [key, val] of cache) {
if (val.expiresAt <= now) cache.delete(key);
}
// If still too large, clear entirely
if (cache.size >= CACHE_MAX_SIZE) cache.clear();
}
cache.set(ip, { blocked, expiresAt: Date.now() + CACHE_TTL_MS });
}
// ---------------------------------------------------------------------------
// Blocklist lookup
// ---------------------------------------------------------------------------
async function isBlocked(ip: string): Promise<boolean> {
const cached = cacheGet(ip);
if (cached !== null) return cached;
try {
const result = await db.execute({
sql: "SELECT 1 FROM blocklist WHERE ip = ? AND (expires_at IS NULL OR expires_at > datetime('now'))",
args: [ip],
});
const blocked = result.rows.length > 0;
cacheSet(ip, blocked);
return blocked;
} catch (err) {
// Fail-open: on DB error, allow the request through
console.error("Blocklist lookup failed, allowing request:", err);
return false;
}
}
// ---------------------------------------------------------------------------
// Middleware
// ---------------------------------------------------------------------------
BunnySDK.net.http
.servePullZone()
.onOriginRequest(async (ctx) => {
const ip = ctx.request.headers.get("X-Real-Ip");
if (!ip) {
return ctx.request;
}
if (await isBlocked(ip)) {
return new Response("Forbidden", { status: 403 });
}
return ctx.request;
});