pre-release · open source · Apache-2.0

Threat feeds into the null route. Failures into the light.

framedrag turns the curated pfBlockerNG feed catalog into live firewall policy for pfSense and OPNsense. One static Go binary, one YAML file, and health checks that treat a silently broken blocklist as the emergency it is.

$ go install framedrag.dev/framedrag/cmd/framedrag@latest
framedrag health
$ framedrag health
FEED             TIER  STATUS   ENTRIES  DELTA   LAST GOOD
Spamhaus_DROP    PRI1  OK         1,412  +0.3%   2m ago
ET_Compromised   PRI1  OK           486  -1.6%   2m ago
CINS_Army        PRI1  OK        15,004  +2.1%   2m ago
Abuse_FeodoC2    PRI2        318      2m ago
Talos_BL         PRI2  FAILED         -  -       3d ago  serving last-good copy
Firehol_L3       PRI3  OK        18,973  +0.9%   2m ago

exit 1 (1 failed, 1 suspect)
Why this exists

pfBlockerNG is why you can't leave pfSense.

Ask anyone who has tried to migrate to OPNsense what stopped them. It is almost never the firewall. It is the decade of curated IP-reputation feeds with nothing equivalent on the other side.

The problem

One package holds you hostage

pfBlockerNG turns tiered threat feeds into pf tables and rules, and it only exists on pfSense. Its IP-reputation half is the single biggest blocker to migrating a firewall to OPNsense.

The workaround

Scripts that fail silently

The usual answer is a cron job that curls a list into an alias. No curation, no tiers, no aggregation, and worst of all: when the feed dies, nothing tells you. The firewall just quietly protects you less.

The way out

Decouple the engine

framedrag is the IP-blocking half of pfBlockerNG as a single static Go binary. It tracks the same feed catalog, a decade of curation by BBcan177 and the pfBlockerNG community, rather than pretending to recreate it.

The result

Same feeds, either firewall

Tiered, deduplicated, CIDR-aggregated blocklists that pfSense consumes today and OPNsense consumes identically, with a machine instead of a human watching for silent failures.

The core, built first

Silent-failure detection is the product.

pfBlockerNG's real value over ten years was a human noticing when feeds broke. framedrag replaces the human. Six checks run on every feed, on every run.

"A blocklist that quietly stopped updating looks identical to one that works."
Failed

HTTP failure

Non-2xx, timeout, or TLS error. The feed is marked FAILED and never produces an empty list.

Failed

Zero entries after a 200

Parsed fine, produced nothing. The number one real-world failure: a 404 page or Cloudflare interstitial served with HTTP 200.

Suspect

Count delta

Entry count swung more than 40% against the last good run (configurable per feed; some feeds legitimately swing). Marked SUSPECT.

Suspect

Rejected-line ratio

More than 10% of non-comment lines failed to parse. That is format drift, and it gets flagged before it gets ignored.

Stale

Staleness

Feed content byte-identical for longer than its stated update cadence allows. A frozen feed is a broken feed with better manners.

Suspect

Sanity floor

Feed contains 0.0.0.0/0, ::/0, RFC1918, or your own networks. The entry is dropped, the feed is marked, and your LAN stays reachable.

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 it stays failed past stale_max (14 days by default), the cached copy is dropped from output with a loud error, because stale threat intel is worse than none.

The run exits non-zero if any feed is FAILED or SUSPECT, so cron and systemd surface it. And framedrag health prints the table above whenever you wonder: is this actually working?

How it works

A pipeline your firewall already knows how to eat.

Five stages, one-shot, run from cron or a systemd timer. The output lands somewhere both pfSense and OPNsense have consumed natively for years.

01

Fetch

Each feed pulled over HTTP with etag caching, retries, and timeouts.

02

Parse

Per-format parsers: plain CIDR, ranges, CSV, Spamhaus DROP, and more.

03

Normalize

Dedup and CIDR-aggregate into the minimal covering set. Suppress your own networks.

04

Health

Six checks per feed. OK, SUSPECT, STALE, or FAILED. Never fail open.

05

Targets

Consolidated lists written where a URL Table alias can consume them.

URL Table aliases, today

Both pfSense and OPNsense natively refresh alias tables from a URL. framedrag serves its consolidated lists to localhost, and the firewall does the rest: zero API integration required. An OPNsense API target is on the roadmap; today both firewalls consume URL Table aliases.

Owns only its prefix

Every alias and rule framedrag creates carries the fd_ prefix, and those are the only objects it will ever modify or delete. It never touches a rule it does not own. Your hand-built firewall config stays yours.

Fetches at the edge

Your box pulls each feed directly from its provider. Nothing is aggregated centrally, nothing is re-hosted, and the local HTTP server binds to loopback by default. Several providers forbid redistribution; framedrag is built to respect that.

Lineage

A 1997 idea, still the right one.

1997

Vixie & Rand's RBL

The original Real-time Blackhole List was a BGP feed that null-routed spammer IPs. Blackholing curated IP feeds is not a metaphor; it is the actual technique, known in the trade as RTBH, Remotely Triggered Black Hole filtering.

2015+

pfBlockerNG

BBcan177's pfBlockerNG spent a decade curating which IP-reputation feeds are trustworthy, tiering them from PRI1 to PRI5. That catalog is the expensive asset. framedrag tracks it upstream instead of forking it.

Now

framedrag

The same idea at the network edge, without BGP: curated feeds, dragged into the null route of your own firewall, with health checks standing in for the human who used to notice.

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.

CLI surface

Six commands. No daemon.

One-shot by design: cron or a systemd timer invokes it, exit codes carry the news.

framedrag updateFetch, parse, normalize, apply to targets
framedrag update --dry-runEverything except Apply; print what would change
framedrag healthPer-feed status table: OK, SUSPECT, STALE, FAILED
framedrag catalog syncDiff the vendored catalog against upstream
framedrag catalog listShow available feeds and tiers
framedrag versionPrint the version

Global flags on every command: --config, --verbose, and --json for machine-readable output.

Install

One binary, one YAML file.

v1 runs one-shot from cron or a systemd timer. Configuration is a single YAML file; feeds come from the vendored catalog, with a local overlay for your own additions and API keys.

Go install

Builds the static binary from the vanity module path.

$ go install framedrag.dev/framedrag/cmd/framedrag@latest

From source

Clone and build with make.

$ git clone https://github.com/framedrag/framedrag && cd framedrag && make build
/etc/framedrag/config.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
Pre-release

Drag your feeds somewhere they can't sit still.

Open source, Apache-2.0, built in the open. Read the spec, watch the repo, and bring your pfBlockerNG habits with you.