はじめに
WebViewでページを閲覧していると、ブラウザにAdBlock拡張を入れていても広告が表示されることがある。
そこで、DNSレベルで広告配信ドメインをブロックすれば良いのだが、無料で使えて、フィルタールールを自分で管理できるサービスは意外とほとんどない。(そりゃビジネスだもんね)
最近Cloudflare Zero Trustを触っていたところ、GatewayにDNSフィルタリング機能があることに気づいた。これを使えば、Pi-holeやAdGuard Home用の常駐マシンを用意しなくても、自宅ネットワーク向けの広告フィルターを構築できそう。
そこで、Cloudflare Zero Trust GatewayとHaGeZi Lightを組み合わせて、DNSレベルの広告フィルタリングを試してみた。
構成
今回の構成は次のとおり
お家ネットワーク
↓ DNS
お家ルーター
↓
Cloudflare Zero Trust Gateway
↓
HaGeZi Lightを参照するDNS Blockポリシー
ルーターのDNS転送先をCloudflare Gatewayへ変更して、LAN内の端末の広告ブロックする。
手順
1. ブロック対象のドメインリストを取得する
ブロック対象となるドメインリストを用意する。広告やトラッカーを対象にしたリストは、複数のプロジェクトから公開されている。
例:
Cloudflare Zero TrustのListsには、free tierでは以下の制限がある。
- 1リストあたり最大1,000件
- 1アカウントあたり最大100リスト
- CSVファイルは2MB未満
そのため、お試しの意を込めHaGeZi Light domains/light.txtを使用する。
HaGeZi Lightではレコードがおよそ8.8万件あったため、今回はデータを加工してListsに登録する。
2. データを加工
Node.jsで、Cloudflare APIを呼び出して同期するツールを作成した。
処理内容は次の通り。
- HaGeZi LightをHTTPSで取得
- ドメインを小文字へ正規化
- 重複や不正な行を除外
- 親ドメインでカバーできるサブドメインを削除
- SHA-256を使って64個の固定バケットへ分割
- 変更されたバケットだけをCloudflareへ送信
CloudflareのDomainセレクターは、指定したドメインに対して、サブドメインも自動適用してくれるので親ドメインが登録されていれば、サブドメインの重複は不要になる。
以下は、親ドメインに包含されるサブドメインを削除する処理。
import { createHash } from "node:crypto";
// 親ドメインがリストにあれば、そのサブドメインは不要。
// 例: example.com があれば ads.example.com は削除する。
function compressCoveredDomains(domains) {
const all = new Set(domains);
const compressed = [];
for (const domain of all) {
const labels = domain.split(".");
let covered = false;
for (let index = 1; index < labels.length - 1; index += 1) {
const parent = labels.slice(index).join(".");
if (all.has(parent)) {
covered = true;
break;
}
}
if (!covered) compressed.push(domain);
}
return compressed.sort();
}
続いて、ドメイン名の SHA-256 ハッシュを使って 64 個の固定バケットに分割する。固定バケットにすることで、上流リストに項目が追加されても既存ドメインの格納先は変わらず、差分のある List だけを更新できる。
// ドメイン名から SHA-256 を計算し、固定のバケットへ振り分ける。
// 上流リストに項目が増えても、既存ドメインの格納先は変わらない。
function bucketDomains(domains, bucketCount = 64) {
const buckets = Array.from({ length: bucketCount }, () => []);
for (const domain of domains) {
const digest = createHash("sha256").update(domain).digest();
const index = digest.readUInt32BE(0) % bucketCount;
buckets[index].push(domain);
}
return buckets.map((bucket) => bucket.sort());
}
処理結果は以下の通りとなる。
取得件数: 88,801
圧縮後: 43,162
リスト数: 64
最小件数: 631
最大件数: 745
npm run sync
上記でdry runして問題がなければCloudflareへ反映する。
npm run sync:apply
APIトークンは、アカウントレベルで作成し、Zero Trust のEdit権限を付与すること。
3. Listsへアップロードする
同期ツールで、Cloudflare Zero TrustにDomain Listを作成し、DashboardでListsを確認する。
Zero Trust
→Reusable components
→lists
4. DNS Blockポリシーを作成する
作成したリストを参照するGateway DNS ポリシーを作成して、ListsのDNSをフィルタリングする。
作成したツールで以下の設定を行っている。
Policy name: HaGeZi Light - managed block
Action: Block
Precedence: 20
5. DNS Locationを追加する
次に、自宅ネットワークにCloudflare Gatewayの設定をする。
Zero Trust
→ Networks
→ Resolvers & Proxies
→ DNS locations
→ Add a location
今回は以下のように設定した。
Location name: Home ad filtering
IPv4 DNS: ON
DoH: ON
Default DNS Location: ON
IPv4 endpoint filtering: ON
Default DNS Locationに設定する場合、DoH endpointも有効にする必要があるっぽい。
IPv4方式では、CloudflareがDNS問い合わせ元のグローバルIPv4を使ってLocationを判定している。そのため、Source IPv4 Addressが現在の自宅のグローバルIPv4と一致していることを確認しておく。
dig @<ipv4> whoami.cloudflare TXT CH +short
6. ルーターのDNSを変更する
DNS Locationを作成したら、ルーターのDNSサーバーをGatewayの共有IPv4 endpointへ変更する。
ルーターに「サーバーから割り当てられたDNSを使用する」といった設定がある場合はOFFにする。
7. 動作確認
HaGeZi Lightに含まれ、Aレコードを返すドメインで確認。
dig @1.1.1.1 pagead2.googlesyndication.com A +short
Cloudflare Public DNSでは、実際のIPアドレスが返ってくる。
続いてGatewayへ問い合わせ
dig @<primary-ipv4-endpoint> pagead2.googlesyndication.com A +short
ブロックポリシーが適用されていれば、0.0.0.0が帰ってくる
おわりに
今回は、Cloudflare Zero Trust Gatewayを利用して、お家のルーターへDNSレベルの広告フィルタリングを追加した。
大抵、DNSレベルの広告フィルタリングだと、Pi-holeやAdGuard Homeがよく使われる。しかしながら、これらを運用するには別途ラズパイなどで常時稼働させておく必要がある。
今回の構成だと、DNSのリソルバとフィルタリングをCloudflareへ委任するため、常駐マシンを置くことなく適用できる。
ただし、共有 IPv4 endpoint を使う場合、Cloudflare は DNS 問い合わせ元の WAN 側グローバル IPv4 で DNS Location を識別する。PPPoE の再接続などで IP アドレスが変わると、登録済みの送信元 IP と一致しなくなり、フィルタリングが適用されなくなる。
継続して運用するには、固定 IP を契約するか、IP アドレスの変更を検知して DNS Location の送信元 IP を更新する仕組みが必要になる。
