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>
This commit is contained in:
91
edge-script/index.ts
Normal file
91
edge-script/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user