DNS Ad Blocker — Network-wide blocking with Cloudflare Gateway
Block ads and trackers network-wide by deploying the 144K-domain AdGuard DNS Filter to Cloudflare Zero Trust Gateway — automated list generation and API upload.
One Gateway policy blocks 144,452 ad and tracker domains for every device on my network — no Pi-hole, no new hardware, nothing on-prem to maintain. A pair of Python scripts turns the AdGuard DNS Filter into Cloudflare Zero Trust Gateway lists and keeps them fresh.
View on GitHubThe problem
My network already resolves every DNS query through Cloudflare Gateway — at home via the DoH on UniFi setup, and anywhere else via the WARP client, which doubles as a personal switch: toggle it on and the connection is ad-free, toggle it off when a site insists on its trackers. Either way the queries land in the same policy engine — so that's where ad blocking belongs, not on a new box beside the router. Gateway will happily block anything on a list you bring it. The catch is bringing the list.
The good public blocklists are maintained in AdGuard's filter format, and the one this project uses — the AdGuard DNS Filter — is 144,452 domains and 66 IPs. Cloudflare's Standard plan caps Gateway lists at 1,000 entries each. Nobody is pasting 145 CSV files into a dashboard by hand, and nobody is doing it again every week when the filter updates.
Working the limits
So the project is really a format-and-limits bridge. generate-lists.py downloads the filter, separates domains from IPs, and splits them into plan-sized CSVs — 145 domain lists and one IP list. upload_to_cloudflare.py pushes all 146 lists through the API and builds a single DNS policy that ORs across them: domains match with any(dns.domains[*] in $list), IPs with any(dns.resolved_ips[*] in $list).
The alternative was regex policies — Gateway accepts them, but at a 6,500-character limit per rule the same filter takes 499 separate policies. One list-backed policy versus 499 regex rules isn't a close call: it's one thing to update, one thing to disable, one thing to reason about when a site breaks.
Using it
Two commands to deploy, one cron line to keep current. The upload script prompts before replacing existing lists; --auto-approve makes it unattended for scheduled refreshes.
# fetch the AdGuard filter, split into plan-sized CSVs
python3 generate-lists.py
# create 146 Gateway lists + one blocking policy, via API
python3 upload_to_cloudflare.py
# refresh weekly, unattended
0 2 * * 0 python3 generate-lists.py && python3 upload_to_cloudflare.py --auto-approveOne honest warning, straight from the README: 144K domains is a comprehensive filter, and comprehensive filters occasionally break things you wanted. Test before pointing the whole household at it.
Status
MIT-licensed and running against my own network — the same pipe the DoH setup feeds. Works on the free Zero Trust plan; Enterprise plans can raise the per-list cap to 5,000 and cut the list count to ~30.