l7-firewall
v0.7.0
Published
Extensible Layer 7 (HTTP) firewall prototype with pluggable rules (path, rate limit, captcha challenge) and optional telemetry stats.
Maintainers
Readme
L7 Firewall (Extensible Layer 7 Firewall Prototype)
An extensible Layer 7 (HTTP) firewall / request decision engine for Node.js / Express with a pluggable rule chain. Supports path allow/block rules, rate limiting, interactive challenge (captcha‑style) flow with attempt limits + temporary blocking, and optional live telemetry statistics.
Features
- Pluggable rule engine executed by priority (lowest number first).
- Built‑in rules:
- PathRule – allow / block lists (exact or wildcard suffix
*). - RateLimitRule – simple token bucket per key (IP by default).
- CaptchaRule – math challenge for configured paths; includes:
- Ephemeral pending challenges with TTL.
- Attempt counter & configurable max attempts.
- Automatic temporary block of key after repeated failures.
- Issued token (header) for future bypass until expiry.
- TurnstileRule – Cloudflare Turnstile widget integration (HTML challenge page or JSON with HTML snippet) for frictionless human verification.
- BotDetectionRule – heuristic bot / spoof detection (UA length/patterns, OS allow/block, header presence, path entropy) returning challenge or block.
- PathRule – allow / block lists (exact or wildcard suffix
- Default allow/block fallback logic with optional short‑circuit on allow.
- Telemetry counters (allow / block / challenge / pass / fail) and per‑rule breakdown.
- Optional periodic console stats (every 1s by default) when enabled.
- Jest + Supertest tests (allow, block, challenge, rate limit).
- Zero build step (pure CommonJS JavaScript).
Quick Start
Install dependencies:
npm installRun the server (port 80 by default; may need admin privileges on some systems – or set PORT):
npm startIn another terminal run the demo client (targets http://localhost):
npm run demoYou should see an allowed response for /pass and a blocked response for /block.
Run tests:
npm testAdding / Customizing Rules
In src/server.js (or your own app) you can register, enable, or disable rules:
firewall.register(new PathRule({ allow: ['/public*'], block: ['/admin'] }), { id: 'paths', priority: 10 });
firewall.register(new RateLimitRule({ windowMs: 60000, max: 100 }), { id: 'ratelimit', priority: 50 });
firewall.register(new CaptchaRule({ triggerPaths: ['/login','/signup'] }), { id: 'captcha', priority: 80 });Lower numeric priority runs earlier. Rules return decisions; first block/challenge short-circuits; allow may short-circuit if shortCircuitOnAllow enabled.
Full Customization Examples
Block extra paths dynamically:
const { firewall } = require('l7-firewall');
firewall.enable('paths', true); // ensure paths rule enabled
// Modify PathRule instance (retrieve via internal reference)
const pathRule = firewall.rules.find(r => r.id === 'paths').rule;
pathRule.block.push('/secret');Add a new high-priority temporary block rule:
class TempBlockRule { constructor(list){ this.list=list; this.id='temp-block'; }
evaluate(req){ return this.list.includes(req.path) ? {action:'block', status:451, reason:'Temp block'}:null; } }
firewall.register(new TempBlockRule(['/maintenance']), { id:'temp-block', priority:5 });Rate limit a specific path harder:
const { RateLimitRule } = require('l7-firewall/rules');
firewall.register(new RateLimitRule({ windowMs: 10000, max: 5, keyFn: r => 'login:'+ (r.ip||'global') }), { id:'login-rate', priority:40 });Captcha only on /signup and /reset:
const { CaptchaRule } = require('l7-firewall/rules');
firewall.register(new CaptchaRule({ triggerPaths: ['/signup','/reset'] }), { id: 'captcha-signup', priority: 70 });Challenge (Captcha) Flow
When CaptchaRule decides a challenge is required it returns something like:
{
action: 'challenge',
status: 403,
reason: 'Captcha required',
challenge: {
type: 'math',
question: '3+4?',
param: 'cf_answer', // query parameter expected for answer
header: 'x-captcha-token',// header to send back once solved
remaining: 3 // attempts remaining before temp block
}
}Client handling steps:
- Display
challenge.questionto user. - User solves locally; resend original request adding
?cf_answer=7. - On success response:
{ action:'allow', token:'<token>', reason:'Captcha passed' }(middleware example includes token if you expose it) – store this token (e.g. in a cookie or memory) and send in headerx-captcha-token: <token>for subsequent protected requests. - If answer wrong: server returns a new challenge with
reason: 'Captcha retry'andremainingdecreased. AftermaxAttemptsfailures the key (IP) is temporarily blocked (Captcha failure temporary block).
CaptchaRule key options:
new CaptchaRule({
triggerPaths: ['/login','/signup'], // where to require captcha
challengeParam: 'cf_answer', // query param for answer
tokenHeader: 'x-captcha-token', // header carrying solved token
ttlMs: 10 * 60 * 1000, // token validity
maxAttempts: 3, // failures before block
blockDurationMs: 5 * 60 * 1000, // temporary block length
challengeTtlMs: 2 * 60 * 1000 // time window to answer specific challenge
});Returned decision meta.challengeResult values (when telemetry enabled): passed, failed, or blocked.
Turnstile (Cloudflare) Human Verification
If you prefer Cloudflare Turnstile over math captchas enable TurnstileRule with your keys:
const { TurnstileRule } = require('l7-firewall/rules');
firewall.register(new TurnstileRule({
triggerPaths: ['/login','/signup'],
siteKey: process.env.TURNSTILE_SITE_KEY,
secretKey: process.env.TURNSTILE_SECRET_KEY,
ttlMs: 10*60*1000
}), { id: 'turnstile', priority: 70 });On first access decision:
{
action: 'challenge',
ruleId: 'turnstile',
challenge: { type: 'turnstile', siteKey: '<your-site-key>', html: "<!DOCTYPE html>..." },
reason: 'Turnstile verification required'
}If you return this as JSON, your frontend can inject challenge.html into a modal / page. The default server demo returns JSON; you can instead detect Accept: text/html and serve raw HTML (customize via htmlTemplate). After the user completes the widget the browser submits a POST containing cf-turnstile-response which the rule verifies server-side (unless SKIP_TURNSTILE_VERIFY=1 set for tests). A successful verification caches an allow for that key for ttlMs.
Environment variables fallback: TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY. For tests you can set SKIP_TURNSTILE_VERIFY=1 to bypass remote calls.
Bot Detection Heuristics
BotDetectionRule flags suspicious requests and either challenges (default) or blocks them.
Key options:
new BotDetectionRule({
allowUserAgents: ['^Mozilla/.*'],
blockUserAgents: ['curl/.*','wget'],
allowOS: [], // only these OS allowed (empty = any)
blockOS: ['unknown'],
minUserAgentLength: 25,
entropyPathThreshold: 0.6, // normalized per-char threshold
entropyMinLength: 12,
suspiciousAction: 'challenge', // or 'block'
pathAllowList: ['/','/pass','/login'],
pathBlockList: ['/bad*'],
headerChecks: { acceptRequired: true, languageRequired: false }
});Heuristics applied (in order): path blocklist, UA allowlist (neutral), UA blocklist, UA length, OS allow/block lists (parsed simply from UA), header presence (Accept / Accept-Language), path entropy (detects random probing). Path entropy uses Shannon entropy per character; long highly random paths are challenged. Returned challenge example:
{
action: 'challenge',
status: 403,
reason: 'High-entropy path',
ruleId: 'bot-detect',
challenge: { type: 'bot-check', reason: 'High-entropy path', entropy: '0.731' }
}Telemetry & Live Stats
Firewall options now include:
const fw = new Firewall({
telemetry: true, // collect counters
statsEnabled: true, // periodically log snapshot
statsIntervalMs: 1000, // interval for console output
// existing options ...
});Console sample:
[Firewall stats] { uptimeSec: 12.0, total: 42, allowed: 30, blocked: 8, challenged: 4, challengePassed: 3, challengeFailed: 1, challengeBlocked: 0 }Programmatic access:
fw.getStats(); // returns snapshot including per-rule countsShut down interval (e.g., in tests): fw.close().
Building & Publishing (npm)
Pure JS – no transpile step. Ensure package.json fields (name, version, description, repository, author, license, keywords) are set. Bump version and publish:
npm version patch # or minor / major
npm publish --access publicDry run first if you like:
npm pack # creates tarball you can inspectAdd a files array in package.json (already configured if you see it) to limit published content, e.g.:
"files": ["src", "README.md", "LICENSE"]If you need 2FA: npm login --otp <code> then publish.
Using As Dependency
npm install l7-firewallconst { Firewall } = require('l7-firewall/firewall');
const { PathRule, RateLimitRule, CaptchaRule } = require('l7-firewall/rules');
const fw = new Firewall({ defaultAction: 'allow' });
fw.register(new PathRule({ block:['/forbidden'] }), { id:'paths', priority:10 });
fw.register(new RateLimitRule({ windowMs:60000, max:100 }), { id:'rl', priority:50 });
// Express usage
app.use(async (req,res,next)=>{ const d=await fw.inspect(req); if(d.action==='block') return res.status(d.status||403).json({blocked:true,reason:d.reason}); if(d.action==='challenge') return res.status(d.status||403).json(d); next(); });Rule Return Contract
{
action: 'allow' | 'block' | 'challenge',
status?: number,
reason?: string,
ruleId?: string,
challenge?: { type: string, question?: string, ...custom },
token?: string, // (CaptchaRule) on successful challenge
meta?: { // optional metadata (telemetry / rule specific)
challengeResult?: 'passed' | 'failed' | 'blocked'
}
}Roadmap Ideas
- Config-driven rule engine (YAML / JSON rules) with priorities.
- Allowlist / blocklist for IPs, CIDR ranges, headers, paths.
- Rate limiting (sliding window + token bucket hybrid).
- Request body pattern signatures & size limits.
- Anomaly scoring (suspicious header combos, unusual methods).
- Caching of repeat decisions for performance.
- Pluggable modules (e.g., RegexRule, GeoIPRule, RateLimitRule).
- Metrics & structured logging (Prometheus format, JSON logs).
- Admin API to reload rules without restart.
Decision Object Shape (Simplified)
At minimum decisions include: action, optionally status, reason, ruleId plus challenge / token / meta fields as above.
Security Notes
Prototype quality: in‑memory stores (rate limits, captcha tokens, blocks) are NOT distributed or persistent. For production you would:
- Replace in‑memory Maps with Redis / durable store.
- Sign or encrypt challenge tokens instead of trusting raw random strings.
- Add input validation, structured logging, metrics export, graceful shutdown.
- Harden around spoofable headers (
x-forwarded-for) behind trusted proxy only.
License
MIT
