I was recently looking for a solution to handle ad blocking at the DNS level. I wanted to use the DNS level specifically (rather than an app or browser extension or something like that) because needed it to work on my iOS devices’ Chrome browsers. As you might know, iOS support for adblockers is not nearly as good as (rooted) Android, and it’s mostly limited to VPN-based solutions like AdGuard or Safari extensions like 1Blocker. Since I’m a Chrome user and I also need to sometimes use Tailscale VPN on my Apple devices, neither of these solutions would work.
Enter DNS-based filtering. DNS is great because a) iOS actually supports configuring DNS at a level comparable to other operating systems, which isn’t the case for most other parts of the networking stack, and b) it applies to every app and website, not just a web browser (in the case of a Safari extension). Of course, I’m not the first one to think of this, and there are numerous solutions out there that offer DNS-based ad-blocking as a service – see NextDNS and AdGuard DNS for examples. However, while they’re convenient, both of these services require a subscription to filter beyond 300,000 queries per month, a figure I managed to exceed in less than a week while trying out NextDNS. An option without the restriction of a SaaS is self-hosting something like Pi-Hole, which is essentially a DNS forwarder with the ability to blackhole certain domains (i.e., advertising and tracking domains). This, however, also won’t work for me – there’s the clear reliability and performance hit compared to a commercial solution, and I’m on cellular data most of the time anyways.
Cloudflare Zero Trust
Then, I discovered Cloudflare Zero Trust. Among many other features in Zero Trust, Cloudflare offers a product called Gateway, designed to allow organizations to filter and secure their end-users’ internet activity without the need for network security appliances or even a traditional enterprise network perimeter. Most Gateway features, including data loss prevention (DLP), TLS decryption, blocking outgoing ports, etc., rely on pushing end-users’ traffic through a Cloudflare VPN. However, a set of features can be used by configuring DNS only, no VPN needed. This includes DNS-based domain and host filtering, which is perfect for an adblocker. (I won’t dive into any of Cloudflare Zero Trust’s other features, but please check out Zero Trust documentation if you’re curious about them.)
The domain and host filtering product is packed with features, but the most important is the ability to set custom firewall rules that block DNS resolution of certain domains/hosts if they meet certain condition(s). There’s a wide variety of conditions to choose from, including blocking known phishing and malware domains, but most important is the ability to block domains/hosts that appear in a custom list. This feature is the heart of ad-blocking with Cloudflare Gateway. The premise is to create a big group of lists containing advertising/tracking hosts (keeping in mind Cloudflare lists can only contain 1000 hosts each on the free plan, and 5000 hosts each on enterprise plans), and a firewall rule blocking a host if it appears in one of those lists. Unlike with a local hosts file, Cloudflare can evaluate rules almost instantly, so performance won’t take a hit from this strategy. The only real caveats here are that a) we need a way to automate the creation of a large number of blocked host lists in Cloudflare, and b) Cloudflare limits accounts to 300 lists each (1000 hosts * 300 lists = 300,000 blocked hosts total).
Luckily, I’m again not the first person who thought of using Cloudflare Gateway for this purpose. There’s a great GitHub repository with scripts to bulk create lists using the Cloudflare API from a CSV file containing domains to block. It also handles creating the firewall rule to check if a domain appears in any of the lists. It’s easy to generate CSV file to use with the script – there are many publicly available domain blocklists for use with adblockers or Pi-Hole that can easily be adapted. The repo even has a GitHub Actions workflow to automatically update the blocklists in Cloudflare, which I’ve deployed here.
Once the lists and firewall rule are in place, the only further action needed to make the ad-blocking work is to ensure your devices’ traffic is subject to Gateway policies. That means either setting up and enabling the WARP VPN client on those devices, or changing those devices’ DNS settings. Naturally, I opted for the latter, which merely involves creating a DNS Location in the Cloudflare Zero Trust console and using operating system or browser features to set the DNS resolver. (Note that many browsers don’t respect the operating system DNS over HTTPS settings and need to be configured manually; the documentation is available here.)
Cloudflare Gateway DNS over HTTPS on iOS
One confusing hiccup I ran into was that everything would time out if I tried to configure my iOS devices to use my Gateway DNS over HTTPS endpoint. You might know that iOS clients require an “Encrypted DNS profile” to enable DNS over HTTPS or DNS over TLS. (The WARP VPN client on iOS has a “DNS over HTTPS only” setting, but this will install a VPN profile rather than an Encrypted DNS profile.) I originally tried to set up this profile by simply copying the Cloudflare 220.127.116.11 DNS over HTTPS profile from paulmillr/encrypted-dns (which is a huge repository of Encrypted DNS profiles for a variety of providers) and replacing the
https://cloudflare-dns.com/dns-query endpoint with my Cloudflare Gateway DNS over HTTPS endpoint.
The solution is that the ServerAddresses key in the Encrypted DNS profile must be modified as well. It still won’t work if you use the IPv4 and IPv6 DNS addresses shown in the Cloudflare Gateway settings. Rather, the ServerAddresses must be set to every IP address that your Cloudflare Gateway DNS over HTTPS endpoint resolves to – a simple dig or nslookup for the endpoint’s A and AAAA records will surface these. Once the ServerAddresses are modified, domains should resolve properly on an iOS device. I don’t know why iOS requires this – maybe someone better at DNS than me can explain.