1. Overview
The FastFilter module provides a header-only, ultra-fast anti-phishing decision engine designed for high-throughput URL screening in desktop AV and endpoint protection workloads.
The engine implements a layered detection pipeline optimized for early exits and predictable latency:
- Bloom filter pre-check (probabilistic negative filter)
- Aho–Corasick automaton for multi-pattern substring matching
- Optional RE2 safe-regex stage for higher-precision rules
- LRU cache for repeated URL lookups
- SQLite loader and file loaders for rule ingestion
The module is written in C++17, supports drop-in inclusion, and provides thread-safe read operations for the hot-path (check_url()) via an immutable data layout after build-time.
2. Dataflow and Pipeline Stages
The evaluation path for each URL is strictly staged to minimize CPU work:
- Normalize input (lowercase)
- Cache lookup (LRU, keyed by 64-bit Murmur3 hash)
- Bloom negative test (fast reject if not present)
- Aho–Corasick scan (substring match across rule-set)
- Optional RE2 scan (partial-match against safe regex rules)
- Cache store (persist verdict for subsequent hits)
The engine is designed so that the most expensive stages (AC scan, regex checks) are only executed after passing cheaper probabilistic and cache gates.
3. Build and Integration
3.1 Header-Only Inclusion
The module is distributed as a single header file (fast_filter.h). It can be included directly into a C++17 project:
#define FASTFILTER_IMPLEMENTATION
#include "fast_filter.h"
No separate compilation unit is required.
3.2 Optional Dependencies
- SQLite: required for
load_patterns_sqlite()(-lsqlite3or amalgamation) - RE2: optional; enabled by defining
FASTFILTER_USE_RE2and linking against-lre2
3.3 Recommended Usage Pattern
In production usage, rules are typically loaded from SQLite at startup:
fastfilter::Config cfg;
cfg.cache_capacity = 100000;
cfg.bloom_fp_rate = 0.01;
fastfilter::Engine eng(cfg);
eng.load_patterns_sqlite("/path/to/db.sqlite", "phishing_rules");
auto verdict = eng.check_url("https://bad.example/phish");
if (verdict.block) { /* enforce block */ }
4. Core Utilities and Hashing
4.1 Murmur3 64-bit Hash
The engine uses a 64-bit Murmur3 hash (x64_128 truncated) as a fast, non-cryptographic digest for:
- LRU cache keys
- Bloom filter hashing (double-hash technique)
The implementation includes:
rotl64()rotation helperfmix64()finalization functionmurmur3_64(key, len, seed)with configurable seed
Different seeds are used to derive independent hashes for Bloom filter hashing.
5. Bloom Filter Stage
5.1 Purpose
The Bloom filter provides a constant-time, memory-efficient probabilistic membership test that is used to quickly reject URLs that cannot match any known substring pattern.
A Bloom negative result is treated as a definitive non-match and returns immediately.
5.2 Parameterization
The Bloom filter is initialized via:
Bloom::init(n_items, fp_rate)
It computes:
- m (number of bits) using:
m = - (n * ln(fp)) / (ln2^2) - k (number of hashes) using:
k = (m/n) * ln2
Bit storage uses a compact byte buffer (std::vector<uint8_t>) with bit addressing.
5.3 Hashing Strategy
Insertion and lookup use a double-hash scheme:
h1 = murmur3_64(s, seed1)h2 = murmur3_64(s, seed2)- Hashes are generated as:
h1 + i*h2fori in [0..k-1]
6. Aho–Corasick Matcher
6.1 Purpose
The Aho–Corasick stage provides deterministic multi-pattern substring matching in a single pass over the URL string.
Patterns are inserted as lowercased strings and the input URL is normalized to lowercase before scanning.
6.2 Automaton Representation
The automaton uses a compact node structure:
next: transition map (std::unordered_map<char,int>)fail: failure link indexout: terminal marker indicating at least one pattern ends at this node
The matcher stores nodes in a contiguous vector (std::vector<ACNode>) to improve locality relative to pointer-heavy designs.
6.3 Build Phase
Automaton construction occurs in two stages:
- Insert patterns using
Automaton::add() - Build failure links using BFS via
Automaton::build()
During build, each node propagates terminal state:
g.out = g.out || g[g.fail].out
This ensures that matches on failure transitions are detected without additional output lists.
6.4 Search Execution
The search scans the normalized URL once:
- Transition on matching characters
- Follow failure links on mismatch
- Return immediately on first terminal state (
out == true)
The engine intentionally exposes a boolean match decision (search_any()) rather than full match metadata to keep the hot-path minimal.
7. Optional Safe Regex Stage (RE2)
7.1 Enablement
Regex support is compiled only when FASTFILTER_USE_RE2 is defined. Without this macro, regex loading returns false and the runtime stage is skipped.
7.2 Regex Loading Format
Regex patterns are loaded from a file where relevant lines begin with:
Pattern:
The parser strips the prefix, trims the remainder, and compiles each pattern into an RE2 object.
7.3 Regex Evaluation
When enabled, regex evaluation occurs only after an Aho–Corasick hit:
- Each pattern is checked using
RE2::PartialMatch() - A match triggers an immediate
block=trueverdict with stageREGEX - If no regex matches, the engine still blocks based on AC match (
ac-match)
This strategy positions regex as a precision enhancement step rather than a primary detector.
8. LRU Cache
8.1 Purpose
The LRU cache is designed to reduce CPU usage and memory churn when the same URLs (or equivalent normalized URLs) are evaluated repeatedly, which is common in browser/network monitoring scenarios.
8.2 Implementation Details
The cache uses:
std::list<uint64_t>for eviction order (most-recent at front)std::unordered_mapmapping key → (verdict, iterator)
Operations:
get()updates recency on hit and returns cached verdictput()inserts/updates and evicts the least-recent entry when capacity is reached
Cache keys are computed as Murmur3-64 of the normalized URL.
8.3 Capacity Control
Capacity is configurable via Config::cache_capacity (default: 100000).
9. Rule Loading
9.1 Loading from Text Files
The engine supports loading substring patterns from a plain text file:
bool Engine::load_patterns_file(const std::string& path)
File parsing rules:
- Lines are trimmed
- Empty lines are ignored
- Comment lines starting with
#are ignored - Patterns are normalized to lowercase before insertion
After loading, the engine builds:
- Bloom filter (initialized with total pattern count)
- Aho–Corasick automaton (insert + build)
9.2 Loading from SQLite
Recommended production ingestion uses SQLite:
bool Engine::load_patterns_sqlite(const std::string& db_path, const std::string& table)
The loader executes:
SELECT pattern, is_regex FROM <table>;
Rows are processed as follows:
patternis trimmed and lowercased- If
is_regexis false: the pattern is added to Bloom and Aho–Corasick - If
is_regexis true: the pattern is stored as a regex candidate (compiled only if RE2 is enabled)
After ingestion:
- Bloom filter and Aho–Corasick are rebuilt from substring patterns
- Regex vector is replaced atomically (compile phase) when RE2 is enabled
total_patternstracks substring rule count (non-regex)
Database connections and statements are closed/finalized deterministically (sqlite3_finalize, sqlite3_close).
10. check_url() Runtime Behavior
10.1 Normalization
The input URL is normalized to lowercase to provide case-insensitive matching consistency across all stages.
10.2 Cache First Strategy
Before any rule evaluation, the engine checks the LRU cache using the normalized URL hash.
On cache hit:
- The function returns immediately
- The returned verdict includes a “cache-hit” rationale
10.3 Bloom Negative Exit
If the Bloom filter is uninitialized or returns a negative test (might_contain == false), the URL is treated as clean and the result is cached as false.
This provides a high-performance reject path for the vast majority of benign URLs.
10.4 Aho–Corasick Match Decision
If Bloom is positive, the engine executes the Aho–Corasick scan. Behavior:
- No match → allow, cache false, return
ac-nohit - Match → proceed to regex stage (if enabled), otherwise block immediately
10.5 Regex Refinement (Optional)
If RE2 is enabled and regexes are loaded:
- Any regex partial match results in
block=truewith stageREGEX - If no regex matches, the engine still blocks based on AC match with stage
AC
If RE2 is not enabled, an Aho–Corasick hit always yields block=true.
11. Output Contract
The runtime decision is returned as a Verdict structure:
block– boolean enforcement decisionstage– indicates which pipeline stage determined the outcome (BLOOM,AC,REGEX)why– a short rationale string (e.g.,bloom-negative,ac-nohit,regex-match)
The module also exposes get_pattern_count() for reporting and diagnostics.
12. Runtime and Security Considerations
- Deterministic runtime: staged early-exit design bounds CPU work for most inputs (cache/bloom dominate)
- Memory efficiency: Bloom filter compact bit-array reduces memory relative to large hash sets
- Safe regex engine: RE2 (when enabled) avoids catastrophic backtracking by design
- Case normalization: all matching is performed on lowercased input to avoid bypass via casing
- Rule integrity: SQLite ingestion relies on trusted table schema (
pattern TEXT,is_regex INTEGER) - Cache semantics: cache key uses a non-cryptographic hash; collisions are statistically unlikely but not impossible and should be considered if used in adversarial contexts
- Thread safety note: the hot-path is designed for concurrent reads after build, but the cache is mutable; if used from multiple threads, cache access should be guarded or replaced with a thread-safe variant
- Input validation: normalization is limited to trimming/lowercasing; URL parsing/decoding should be performed upstream if required for robust canonicalization