さくらのアドブロッカー ライト

月額数百円で利用できる「さくらのレンタルサーバ ライト」を活用して、自分専用の広告ブロック機能付き DoH (DNS-over-HTTPS) サーバを構築してみた話です。

ライトプランは安価ですが、SSH が使えず、PHPCGI モードで動作するなど制約があります。しかし、今回開発した軽量な PHP 製 DoH プロキシ「Dohmasq」を使えば、この環境でも十分に実用的なアドブロッカーが作れます。

Dohmasq

Dohmasq は、Dnsmasq にインスパイアされた、次の特徴を持つ軽量な PHPDNS-over-HTTPS (DoH) プロキシです。

  • hosts ファイル形式のブラックリストを使って広告ドメインNXDOMAIN (存在しない) として返す
  • URL にトークンを含めることで、自分だけが使えるセキュアなサーバになる
  • データベースが不要で PHP さえ動けば動作する
  • ドメインリストの有効期限が切れるとバックグラウンドで自動更新する

さくらのレンタルサーバ ライト

「さくらのレンタルサーバ ライト」は、月額換算で約 165 円〜(年間一括払い時)と非常に安価です。 しかし、以下の制約があります。

  • SSH 接続が禁止されているため、コマンドラインcomposer install したり、cron を設定したりできない
  • PHPCGI モードなので、モジュールモードに比べてプロセス起動のオーバーヘッドがある

Dohmasq はこれらの制約をうまく回避できます。また、リクエストをトリガーにしたリスト自動更新機能があるため、cron がなくても最新の広告ブロックリストを維持できます。

構築手順

SSH が使えないため、「ローカルで準備して FTP でアップロード」という古き良き手法をとります。

1. ローカル環境で準備

まず、手元の PC でファイルを準備します。PHP と Composer が入っている環境で、リポジトリをクローンし、依存関係をインストールします。

git clone https://github.com/asannou/dohmasq.git
cd dohmasq
composer install --no-dev --optimize-autoloader
2. 設定ファイルの編集

自分だけがアクセスできるように、tokens.php に推測されにくいトークンを設定します。

return [
    'BXl4ANTWVL6rf7rVv8fCEsCn6yet6Ry01ZZGxTKz',
];

generate-domains.phphttps://example.com/hosts の部分を強力なブロックリストの URL に書き換えます。

$sourceUrls = [
    'https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts',
];
3. 初期ドメインリストの生成

サーバにアップロードする前に、一度ローカルでリストを生成しておきます。

php generate-domains.php

これで domains.php が生成されます。

4. アップロード

FTP クライアントを使って、さくらのサーバにアップロードします。例えば、公開フォルダーが www の場合、そこに全てのファイルをアップロードします。

DoH の設定

各種 OS やブラウザで、DNS サーバとして以下の URL を設定してください。

https://[あなたのドメイン]/[設定したトークン]/dns-query.php

iPhone / Mac への設定

Dohmasq には、Apple 製品向けの構成プロファイルを生成する機能があります。 Safari で以下の URL にアクセスしてください。

https://[あなたのドメイン]/[設定したトークン]/mobileconfig.php

画面の指示に従ってプロファイルをインストールすれば、そのデバイスDNS 通信は全てこのサーバ経由になり、広告がブロックされるようになります。

自動更新について

Dohmasq には、DNS クエリを受け取った際に domains.php が古くなっている(デフォルト 24 時間)と、バックグラウンドで generate-domains.php を実行してリストを更新する機能が含まれています。 これにより、SSH で入って cron を設定できないライトプランでも、常に最新のブロックリストを維持できます。

速度について

CGI モードなので、リクエストごとに PHP プロセスが立ち上がります。通常の DNS に比べれば遅延はありますが、ウェブブラウジング程度であれば、そこまで気にならないレベルで動作します。

Dependabot CLI がローカルディレクトリをサポートした

Dependabot のテストやデバッグに使用する dependabot/cli は、これまで GitHub 等のレポジトリを指定して、依存関係の更新ジョブを出力することしかできなかったが、新たにローカルディレクトリの指定が可能になった。

github.com

毎回 GitHub レポジトリにプッシュする必要がなくなり、テストが容易になる。

現時点で Usage に反映されていないが、以下のとおり --local とパスを指定する。レポジトリ名がないとエラーになるが、適当な文字列で問題なかった。

$ dependabot update terraform dummy --local . -o job.yaml

これを利用して、Dependabot が、特定の条件で Terraform モジュール内のバージョン制約を更新しない挙動を、いくつか確認した。Dependabot は、与えられたディレクトリを起点に *.tf ファイルの内容を読み取って、参照されているモジュールのみをたどるアプローチを採用している。

ひとつは JSON Configuration Syntax で記述された *.tf.json ファイルから参照されたモジュールが更新されない問題で、Add Terraform JSON support by melendezd · Pull Request #5293 · dependabot/dependabot-core · GitHub で解決すると思われる。

もうひとつは、モジュールの参照が main.tf -> a/main.tf -> a/b/main.tf -> a/b/c/main.tf のように 3 回以上になる場合、更新されない問題があった。

どちらも、ワークアラウンドとしては、更新されないモジュールのディレクトリを dependabot.yml ファイルに追加すればよい。

version: 2

updates:
  - package-ecosystem: terraform
    directory: /
    schedule:
      interval: monthly
  - package-ecosystem: terraform
    directory: /a/b/c
    schedule:
      interval: monthly

ここに terraform graph があるじゃろ?これをこうして…こうじゃ。

github.blog

GitHub のマークダウンファイルにおいて、Mermaid.js が使えるようになったので、練習のために terraform graph の出力を、わかりやすく Mermaid 構文に変換して、README.md ファイル等に埋め込む GitHub Action を作成しました。

サンプルとして、以下のような Terraform AWS provider の使用例 の出力で説明します。

[root] aws_elb.web (expand) aws_elb.web [root] aws_instance.web (expand) aws_instance.web [root] aws_elb.web (expand)->[root] aws_instance.web (expand) [root] aws_security_group.elb (expand) aws_security_group.elb [root] aws_elb.web (expand)->[root] aws_security_group.elb (expand) [root] aws_key_pair.auth (expand) aws_key_pair.auth [root] aws_instance.web (expand)->[root] aws_key_pair.auth (expand) [root] aws_security_group.default (expand) aws_security_group.default [root] aws_instance.web (expand)->[root] aws_security_group.default (expand) [root] aws_subnet.default (expand) aws_subnet.default [root] aws_instance.web (expand)->[root] aws_subnet.default (expand) [root] var.aws_amis var.aws_amis [root] aws_instance.web (expand)->[root] var.aws_amis [root] aws_internet_gateway.default (expand) aws_internet_gateway.default [root] aws_vpc.default (expand) aws_vpc.default [root] aws_internet_gateway.default (expand)->[root] aws_vpc.default (expand) [root] provider["registry.terraform.io/hashicorp/aws"] provider["registry.terraform.io/hashicorp/aws"] [root] aws_key_pair.auth (expand)->[root] provider["registry.terraform.io/hashicorp/aws"] [root] var.key_name var.key_name [root] aws_key_pair.auth (expand)->[root] var.key_name [root] var.public_key_path var.public_key_path [root] aws_key_pair.auth (expand)->[root] var.public_key_path [root] aws_route.internet_access (expand) aws_route.internet_access [root] aws_route.internet_access (expand)->[root] aws_internet_gateway.default (expand) [root] aws_security_group.default (expand)->[root] aws_vpc.default (expand) [root] aws_security_group.elb (expand)->[root] aws_vpc.default (expand) [root] aws_subnet.default (expand)->[root] aws_vpc.default (expand) [root] aws_vpc.default (expand)->[root] provider["registry.terraform.io/hashicorp/aws"] [root] output.address output.address [root] output.address->[root] aws_elb.web (expand) [root] var.aws_region var.aws_region [root] provider["registry.terraform.io/hashicorp/aws"]->[root] var.aws_region [root] provider["registry.terraform.io/hashicorp/aws"] (close) [root] provider["registry.terraform.io/hashicorp/aws"] (close) [root] provider["registry.terraform.io/hashicorp/aws"] (close)->[root] aws_elb.web (expand) [root] provider["registry.terraform.io/hashicorp/aws"] (close)->[root] aws_route.internet_access (expand) [root] root [root] root [root] root->[root] output.address [root] root->[root] provider["registry.terraform.io/hashicorp/aws"] (close)

初手、出力された GraphViz の DOT フォーマットを、単純に Mermaid 構文に置き換えます。

flowchart LR
n0["aws_elb.web"]
n1["aws_instance.web"]
n2["aws_internet_gateway.default"]
n3["aws_key_pair.auth"]
n4["aws_route.internet_access"]
n5["aws_security_group.default"]
n6["aws_security_group.elb"]
n7["aws_subnet.default"]
n8["aws_vpc.default"]
n9(["output.address"])
na[/"provider<br/>[&quot;registry.terraform.io/hashicorp/aws&quot;]"\]
nb(["var.aws_amis"])
nc(["var.aws_region"])
nd(["var.key_name"])
ne(["var.public_key_path"])
n0-->n1
n0-->n6
n1-->n3
n1-->n5
n1-->n7
n1-->nb
n2-->n8
n3-->na
n3-->nd
n3-->ne
n4-->n2
n5-->n8
n6-->n8
n7-->n8
n8-->na
n9-->n0
na-->n0
na-->n4
na-->nc

各リソースと値を、種類ごとに subgraph にまとめ、スタイルを適用して、リソースを強調します。

%%tfmermaid
%%{init:{"theme":"default","themeVariables":{"lineColor":"#6f7682","textColor":"#6f7682"}}}%%
flowchart LR
classDef r fill:#5c4ee5,stroke:#444,color:#fff
classDef v fill:#eeedfc,stroke:#eeedfc,color:#5c4ee5
classDef ms fill:none,stroke:#dce0e6,stroke-width:2px
classDef vs fill:none,stroke:#dce0e6,stroke-width:4px,stroke-dasharray:10
classDef ps fill:none,stroke:none
classDef cs fill:#f7f8fa,stroke:#dce0e6,stroke-width:2px
subgraph "n0"["ELB Classic"]
n1["aws_elb.web"]:::r
end
class n0 cs
subgraph "n2"["EC2 (Elastic Compute Cloud)"]
n3["aws_instance.web"]:::r
n4["aws_key_pair.auth"]:::r
end
class n2 cs
subgraph "n5"["VPC (Virtual Private Cloud)"]
n6["aws_internet_gateway.default"]:::r
n7["aws_route.internet_access"]:::r
n8["aws_security_group.default"]:::r
n9["aws_security_group.elb"]:::r
na["aws_subnet.default"]:::r
nb["aws_vpc.default"]:::r
end
class n5 cs
subgraph "nc"["Output Values"]
nd(["output.address"]):::v
end
class nc vs
ne[/"provider<br/>[&quot;registry.terraform.io/hashicorp/aws&quot;]"\]
subgraph "nf"["Input Variables"]
ng(["var.aws_amis"]):::v
nh(["var.aws_region"]):::v
ni(["var.key_name"]):::v
nj(["var.public_key_path"]):::v
end
class nf vs
n1-->n3
n1-->n9
n3-->n4
n3-->n8
n3-->na
n3--->ng
n6-->nb
n4-->ne
n4--->ni
n4--->nj
n7-->n6
n8-->nb
n9-->nb
na-->nb
nb-->ne
nd--->n1
ne-->n1
ne-->n7
ne--->nh

provider は 1 個しかなく、役割は自明なので省略します。

%%tfmermaid
%%{init:{"theme":"default","themeVariables":{"lineColor":"#6f7682","textColor":"#6f7682"}}}%%
flowchart LR
classDef r fill:#5c4ee5,stroke:#444,color:#fff
classDef v fill:#eeedfc,stroke:#eeedfc,color:#5c4ee5
classDef ms fill:none,stroke:#dce0e6,stroke-width:2px
classDef vs fill:none,stroke:#dce0e6,stroke-width:4px,stroke-dasharray:10
classDef ps fill:none,stroke:none
classDef cs fill:#f7f8fa,stroke:#dce0e6,stroke-width:2px
subgraph "n0"["ELB Classic"]
n1["aws_elb.web"]:::r
end
class n0 cs
subgraph "n2"["EC2 (Elastic Compute Cloud)"]
n3["aws_instance.web"]:::r
n4["aws_key_pair.auth"]:::r
end
class n2 cs
subgraph "n5"["VPC (Virtual Private Cloud)"]
n6["aws_internet_gateway.default"]:::r
n7["aws_route.internet_access"]:::r
n8["aws_security_group.default"]:::r
n9["aws_security_group.elb"]:::r
na["aws_subnet.default"]:::r
nb["aws_vpc.default"]:::r
end
class n5 cs
subgraph "nc"["Output Values"]
nd(["output.address"]):::v
end
class nc vs
subgraph "ne"["Input Variables"]
nf(["var.aws_amis"]):::v
ng(["var.aws_region"]):::v
nh(["var.key_name"]):::v
ni(["var.public_key_path"]):::v
end
class ne vs
n1-->n3
n1-->n9
n3-->n4
n3-->n8
n3-->na
n3--->nf
n6-->nb
n4--->nh
n4--->ni
n7-->n6
n8-->nb
n9-->nb
na-->nb
nd--->n1

矢印は、依存元から依存先を指しているのですが、値の代入元から代入先を向くほうが直感的なので、逆にします。

%%tfmermaid
%%{init:{"theme":"default","themeVariables":{"lineColor":"#6f7682","textColor":"#6f7682"}}}%%
flowchart LR
classDef r fill:#5c4ee5,stroke:#444,color:#fff
classDef v fill:#eeedfc,stroke:#eeedfc,color:#5c4ee5
classDef ms fill:none,stroke:#dce0e6,stroke-width:2px
classDef vs fill:none,stroke:#dce0e6,stroke-width:4px,stroke-dasharray:10
classDef ps fill:none,stroke:none
classDef cs fill:#f7f8fa,stroke:#dce0e6,stroke-width:2px
subgraph "n0"["ELB Classic"]
n1["aws_elb.web"]:::r
end
class n0 cs
subgraph "n2"["EC2 (Elastic Compute Cloud)"]
n3["aws_instance.web"]:::r
n4["aws_key_pair.auth"]:::r
end
class n2 cs
subgraph "n5"["VPC (Virtual Private Cloud)"]
n6["aws_internet_gateway.default"]:::r
n7["aws_route.internet_access"]:::r
n8["aws_security_group.default"]:::r
n9["aws_security_group.elb"]:::r
na["aws_subnet.default"]:::r
nb["aws_vpc.default"]:::r
end
class n5 cs
subgraph "nc"["Output Values"]
nd(["output.address"]):::v
end
class nc vs
subgraph "ne"["Input Variables"]
nf(["var.aws_amis"]):::v
ng(["var.aws_region"]):::v
nh(["var.key_name"]):::v
ni(["var.public_key_path"]):::v
end
class ne vs
n3-->n1
n9-->n1
n4-->n3
n8-->n3
na-->n3
nf--->n3
nb-->n6
nh--->n4
ni--->n4
n6-->n7
nb-->n8
nb-->n9
nb-->na
n1--->nd

左から、リソースが作成されていく流れが俯瞰しやすくなったように思います。Terraform 設定ファイルを含む Github リポジトリにおいて、次のとおりに Github Action を使用することで、このようなグラフが自動的に README.md ファイルに埋め込まれます。

github.com

  • 下記の内容の .github/workflows/tfmermaid.yml ファイルを作成する
name: tfmermaid
on:
  push:
jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: asannou/tfmermaid-action@v1
        with:
          file: README.md
      - name: commit
        run: |
          git add README.md
          if ! git diff --cached --quiet --exit-code
          then
            git config user.name "github-actions[bot]"
            git config user.email "github-actions[bot]@users.noreply.github.com"
            git commit -m "generated"
            git push
          fi
  • README.md ファイルの埋め込む場所に %%tfmermaid とコメントされた mermaid コードブロックを配置する
 ```mermaid
 %%tfmermaid
 ```

状況に合わせて、provider を表示したり、var 等を省略したり、矢印を順方向にしたり、グラフ全体の向き を変更する等のオプションも用意しています。

      - uses: asannou/tfmermaid-action@v1
        with:
          file: README.md
          include: provider
          exclude: var,local,output,data
          arrow-direction: forward
          orientation: RL

その他の例は https://github.com/asannou/tfmermaid-action/blob/v1/README.md#examples で確認してください。