unboundでDNSを使った自前広告フィルタを実装する

なんか最近やたらと広告の表示がでかくなってきました。
全面にどーんと表示したり、サイトの5割ぐらいが広告だったりします。

無料閲覧できる記事のサイトの裏では、サーバ維持と記事を書く人の給料のため(あるいは個人の収益のため)に、たくさんお金が必要であることは言うまでもないとは思いますが、サイトの閲覧に支障が出るほどのコレはアカンやろ、程度の問題を過ぎてしまってるやろ、なんて思います。

とはいえ、いちいち自分の管制下のスマホとPCに全部広告ブロッカー入れるとかやってられないし、広告ブロックつきの公開DNSサービスはいまいち応答が遅いし…と悩んでいたので、ならば自分で広告ブロックつきキャッシュDNSサーバを立ててみようと試してみたメモを置いておきます。そう、なければ作れば良いのだ。

用意するもの

  • どこのご家庭にでもあるFreeBSD😈サーバ (必須)
    あなたは当然FreeBSDを使っていると思いますし、各家庭への普及率は300%を超えると思いますが、たいへん遺憾ではありますが、万が一そうでなければ、unboundとpythonが動く環境であれば問題ありません。
    LInux🐧サーバとかならまあいいでしょう。
  • どこのご家庭でも契約しているChatGPT (オプション)
    あなたは当然ChatGPTを使って…普及率は300%…(以下略)
    ぶっちゃけコードを書くのがめんどくさいので丸投げしましょう。
    もしかしたら、Gemini Advancedとかでもいいかもしれません。

作り方

とりあえずunboundをインストールする

DNSサーバといえばなんでしょう。
とりあえずbind!って答えた人は容赦なくインターネット老人会送りです。

なんかいまどきはローカルDNSキャッシュサーバに特化した、もっといい実装があるとかないとかいう噂も聞きますが、とりあえずみんなだいたい使ってるunboundを今回は採用します。

pkg install python312
pkg install unbound (FreeBSD)

とか

sudo apt install python3
sudo apt install unbound (Debian/Ubuntu)

とかやるといいと思います。

ブロック用定義ファイルを出力する

AdGuard開発チームが、AdGuard DNS用のブロックリストを公開しているので、ありがたく利用させてもらいます。

ただ、filter.txtは独自形式になっているので、

||hogehoge.com

こういうのを、unboundが理解できるように、

local-zone: "hogehoge.com" redirect

local-data: "hogehoge.com A 0.0.0.0"

こんな感じに書き換えてやる必要があります。
参考:0.0.0.0は無効なIPなので、これで「指定のドメイン(ゾーン)の名前引きをすると無効な応答を返す」という指示になります。

めんどくさいですね。こういうことはChatGPTに丸投げして、変換スクリプトを書いてもらいましょう。

import urllib.request
import subprocess

def fetch_and_process_adguard_list(url, exclusion_words):
    with urllib.request.urlopen(url) as response:
        data = response.read().decode('utf-8')

    domains = set()
    for line in data.splitlines():
        if line.startswith("||"):
            domain = line[2:].split("^")[0]
            if not any(exclusion_word in domain for exclusion_word in exclusion_words):
                domains.add(domain)

    processed_lines = []
    for domain in domains:
        processed_lines.append(f"local-zone: \"{domain}\" redirect")
        processed_lines.append(f"local-data: \"{domain} A 0.0.0.0\"")

    return processed_lines

def main():
    url = "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"
    output_path = "/usr/local/etc/unbound/blocklist.conf"
    exclusion_words = [
        'amazonaws.com',
        'googlesyndication.com',
        'analytics.google.com',
        'google-analytics.com',
        'googleanalytics.com',
        'posthog',
        ]

    processed_data = fetch_and_process_adguard_list(url, exclusion_words)

    with open(output_path, "w") as file:
        file.write("\n".join(processed_data))

    subprocess.run(["/usr/local/sbin/unbound-control", "reload"])

if __name__ == "__main__":
    main()

いい感じで書いてもらえましたね!一番下のスクリプトを適当なところに "cron-blocklist.py" とかいう感じで保存して、cronで定期実行しましょう。

何?cronの設定方法がわからん?貴様モグリだな?
私もわからんので、これもChatGPTに聞きました。

毎日朝の4時半に上記のスクリプトをcronで実行するには、crontabファイルに適切なエントリを追加します。まず、コマンドラインで crontab -e を実行して、crontabファイルを編集します。次に、以下の行をファイルに追加します。

30 4 * * * /usr/bin/python3 /path/to/your/script.py

ここで /usr/bin/python3 はPythonの実行可能ファイルのパス(Pythonがインストールされている場所)です。これはシステムによって異なる場合があるので、正しいパスを使用してください。which python3 コマンドを実行することで、Pythonのパスを見つけることができます。

unboundの設定ファイルに追記する

注意:上のサンプルスクリプトでは、
/usr/local/etc/unbound/blocklist.conf
に変換済みのブロックリストが出力され、
/usr/local/sbin/unbound-control
にunbound-controlの実行ファイルが存在する前提になっています。

これはFreeBSDを前提としたパスなので、Linuxをお使いの方は適当に書き換えてください。ただし、
/etc/unbound/unbound.conf.d/
の中だと自動で読み込まれてしまってエラーになるんじゃないかと思うので、別の場所にしましょう。

次に、unbound.conf ( FreeBSDなら /usr/local/etc/unbound/unbound.conf ) を適当なエディタで開いて、

server:

と書いてある行を探して、それ以降に

include: "/usr/local/etc/unbound/blocklist.conf"

と書いて、ブロックリストを読み込むようにしてください。
(Linuxなら適当にパスを書き換えてください)

終わったらunboundを起動/再起動しましょう。

(初回起動)
sudo sysrc unbound_enable="YES"
sudo service unbound start

(再起動)
sudo service unbound restart

これでだいたいちゃんと動くと思います。たぶん。

あとがき

正直自分で作らなくでも

adguard-dns.io

上記のAdGuard DNSを設定すれば、同等の環境が得られます。

  • DNSの応答がキャッシュされている場合の応答がものすごく速い
  • 他社(AdGuard DNS)のサーバにDNSの名前引きパケットを飛ばさなくて良いから、混雑の影響やパケット往復時間のロスを削減できる

ぐらいが利点かなあ、なんて思います。
私が作ろうと思い立ったのはこの利点がほしいからですが、ぶっちゃけ普通の人類がミリ秒単位の遅延が感じ取れるわけがないので、気にしなくて良いです。

なので、ヤバい人類用とかヤバいAI用とかに活用していただければと思います。

以上、物好きが適当に作ってみたログということで、笑って許してくださいね!

3/27追記

例外設定にposthogを追加しないと、OpenAI APIの呼び出しがおかしくなるので、サンプルコードに追記しました。