# framedrag > Curated IP reputation feeds, dragged into the null route. A single static Go binary that consumes curated IP-reputation feeds (the pfBlockerNG catalog) and turns them into live firewall policy for pfSense and OPNsense, with silent-failure detection as the core feature. framedrag is a pre-release, open-source project licensed under Apache-2.0. The module path is `framedrag.dev/framedrag`, the binary is named `framedrag`, and the source lives at https://github.com/framedrag/framedrag (full product spec at docs/SPEC.md). The project home is https://framedrag.dev. It was designed and built by Akeem Jenkins (https://akeemjenkins.com). The name: frame-dragging, the Lense-Thirring effect, is what a rotating black hole does to the spacetime around it. Nothing nearby can stay still. It is also what this does to Ethernet frames. ## Why it exists pfBlockerNG is the reason many people cannot leave pfSense. It turns tiered IP-reputation feeds into pf tables and firewall rules, and it has no equivalent on OPNsense. Its IP-reputation half is the single biggest blocker to migrating a firewall. The usual workaround, a cron job that curls a list into an alias, has no curation, no tiers, no aggregation, and no health checks: when the feed dies, nothing tells you, and the firewall quietly protects you less. framedrag decouples the IP-blocking half of pfBlockerNG into a single static Go binary. It tracks the same feed catalog, a decade of curation by BBcan177 and the pfBlockerNG community, rather than recreating it. The result: the same tiered, deduplicated, CIDR-aggregated blocklists, consumable by pfSense today and OPNsense identically, with a machine instead of a human watching for silent failures. ## Silent-failure detection (the core, built first) A blocklist that quietly stopped updating looks identical to one that works. pfBlockerNG's real value over ten years was a human noticing when feeds broke; framedrag replaces the human. Six checks run per feed, on every run: 1. HTTP failure: non-2xx, timeout, or TLS error. Feed marked FAILED. 2. Zero entries after a 200: parsed successfully but produced no prefixes. FAILED. This is the number one real-world failure: a 404 page or Cloudflare interstitial served with HTTP 200. 3. Count delta: entry count changed more than 40% (configurable per feed) versus the last good run. SUSPECT. 4. Rejected-line ratio: more than 10% of non-comment lines failed to parse (format drift). SUSPECT. 5. Staleness: feed content byte-identical for longer than its stated update cadence allows. STALE. 6. Sanity floor: feed contains 0.0.0.0/0, ::/0, RFC1918, or the user's own networks. The entry is dropped and the feed marked SUSPECT. Failure policy, never fail open, never fail catastrophically: - A FAILED feed does not produce an empty list. It falls back to the last-good cached copy, and the run is flagged. - If a feed stays FAILED past stale_max (default 14 days), the cached copy is dropped from output with a loud error. Stale threat intel is worse than none. - The run exits non-zero if any feed is FAILED or SUSPECT, so cron/systemd surfaces it. - `framedrag health` prints a per-feed table with status (OK, SUSPECT, STALE, FAILED), entry count, delta, and last-good timestamp. It is the command to run when you wonder whether it is actually working. Notifications are minimal and pluggable: stdout and exit codes are the baseline, plus an optional generic webhook (POST JSON) for Slack/Discord/ntfy. ## How it works Pipeline, one-shot, invoked by cron or a systemd timer: fetch -> parse -> normalize -> health -> targets. - Fetch: each feed pulled over HTTP with etag/last-modified caching, retries, and timeouts. - Parse: per-format parsers (plain CIDR/IP, IP ranges, CSV, comment-bearing lines, Spamhaus DROP, Emerging Threats), IPv4 and IPv6 from day one. - Normalize: deduplicate and CIDR-aggregate into the minimal covering set (required, not an optimization: naive concatenation can blow pf table limits), suppress the user's own networks (logged, always), emit deterministic sorted output. - Health: the six checks above. - Targets: consolidated lists written to a directory and optionally served over localhost HTTP. The firewall consumes the output via a URL Table alias, which both pfSense and OPNsense support natively, with zero API integration. An OPNsense API target (aliases plus Automation filter rules) is on the roadmap; today both firewalls consume URL Table aliases. Every alias and rule framedrag creates carries the fd_ prefix, and those are the only objects it will ever modify or delete. Legal stance: framedrag fetches at the edge. Each user's box pulls feeds directly from the provider; nothing is aggregated centrally or re-hosted, because several providers' terms forbid redistribution. The localhost HTTP server binds to loopback by default. API keys for gated feeds live in the user's local overlay config and are never committed, logged, or transmitted anywhere except to the feed provider. Explicitly out of scope: DNS blocking / DNSBL (AdGuard Home already does DNS well on OPNsense), and any GUI. ## Lineage - 1997: Vixie & Rand's RBL, originally a BGP feed that null-routed spammer IPs. Blackholing curated IP feeds is the actual technique, known as RTBH (Remotely Triggered Black Hole) filtering. - pfBlockerNG: BBcan177's package spent a decade curating which IP-reputation feeds are trustworthy, tiered PRI1 (most reputable) through PRI5. That catalog is the expensive asset; framedrag tracks it upstream (via `framedrag catalog sync`) instead of forking it. - framedrag: the same idea at the network edge, without BGP. ## Commands - `framedrag update`: fetch, parse, normalize, apply to targets - `framedrag update --dry-run`: everything except Apply; print what would change - `framedrag health`: per-feed status table (OK, SUSPECT, STALE, FAILED) - `framedrag catalog sync`: diff the vendored catalog against upstream - `framedrag catalog list`: show available feeds and tiers - `framedrag version`: print the version Global flags: `--config`, `--verbose`, `--json` (machine-readable output for all commands). ## Install Go (the vanity import path resolves via framedrag.dev): ``` go install framedrag.dev/framedrag/cmd/framedrag@latest ``` From source: ``` git clone https://github.com/framedrag/framedrag && cd framedrag && make build ``` v1 runs one-shot from cron or a systemd timer. Configuration is a single YAML file: ```yaml state_dir: /var/lib/framedrag suppress: - 192.168.0.0/16 - 10.0.0.0/8 health: delta_threshold_pct: 40 stale_max_days: 14 webhook: "" # optional aliases: - name: fd_pri1 action: deny direction: both feeds: [PRI1] # references catalog tiers/feeds - name: fd_pri2 action: deny direction: in feeds: [PRI2] targets: - type: file dir: /var/lib/framedrag/lists serve: 127.0.0.1:8080 ``` ## Attribution The feed catalog and a decade of curation come from pfBlockerNG (BBcan177 / Rubicon Communications, Apache-2.0), credited in the project's NOTICE file. framedrag is an independent project, not affiliated with, endorsed by, or sponsored by Netgate/pfSense, Deciso/OPNsense, or Spamhaus; those names are trademarks of their respective owners, used only to describe interoperability. More tools by the same author: https://higgscli.com, https://ouracli.com, https://useokf.com.