広告ブロックができるDNS Over HTTPS(DoH) サーバーを構築する

## はじめに
本稿はDNS Over HTTPS(DoH) サーバーを構築したログを記載するものです。
Cloudflare, Tailscale, dns-over-https, Pi-holeで構成しています。

このDoHサーバーの主な目的はDNSリクエストをコントロールすることで、ブラウザに表示される不要な広告を除去（ブロック）することです。
マシン全体ではなく特定のブラウザのみに構築したDoHサーバーを設定できるため、システムや他アプリケーションの安定性を損なうことなくDNSを使用した広告の除去が可能です。

メインのソリューションとなるPi-holeはDNSアクセスをコントロールするためソフトウェアで、十分な機能を揃えたWeb 管理画面からステータスやログを確認したり設定の変更を行うことができます。
![100.89.62.104_8080_admin_(Surface Pro 7)_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47014/84cf8fc6-29ec-ac75-5dcc-64c04201a3c5.png)

ブラウザなどのソフトウェアに設定を行うことで、それらのDNSリクエストについてPi-holeを経由させ、Pi-holeはフィルタ条件に基づき不適切なリクエストを遮断します。この仕組みにより、DNS単位での広告のブロックが可能になります。
デフォルトで事前定義されたブロックリストが読み込まれるため、インストール直後からすぐに広告ブロックのために使うことができます。この手のソフトウェアの例に漏れず定義はやや強めの基準で作られていますが、ブロックの解除も管理画面から簡単に行うことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47014/75564ebd-d54e-3722-7ee9-61cf47641af6.png)

## 注記
- この用途で同様のスペックのものとしては例えばADGUARD DNSというマネージドのソリューションが存在しますが、月額$20程度とそれなりによいお値段がします。
- サーバーにはグローバルIPを割り当てる必要があります。
- HTTPS化のためにCloudflare DNS Proxyを使用しているため、Cloudflare上でドメインのAレコードとしてサブドメインを1つサーバーに割り当てる必要があります。
- 検証/運用環境としてOracle Cloud Infrastructure(OCI) のUbuntu 22.04 LTSを使用しています。

## 想定運用コスト
- ドメイン購入/維持費（サブドメインを1つ使えればよいので既にあれば実質無料）
- Cloudflare - Free Plan（無料）
- Tailscale - Free Plan（無料）
- Oracle Cloud Infrastructure(OCI) 
    - AMD Compute VM - Free Tier - Always Free（無料）
    - Outbound Data Transfer - First 10 TB / Month（無料）


## 構成図
![graphviz.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47014/79c1f1f0-3f9d-c18d-db0b-cda27cb0f25b.png)

<!--
digraph Solution {
  rankdir=LR; // Left to Right layout

  // Client Subgraph
  subgraph cluster_Client {
    label = "Client";

    // Internet Subgraph
    subgraph cluster_Internet {
      label = "Internet";
      BrowserA [label="Browser A"];
      SSHClient [label="SSH Client"];  // SSH Client
    }

    // Tailscale (Tailnet) Subgraph
    subgraph cluster_Tailnet {
      label = "Tailnet";
      BrowserB [label="Browser B"];
    }
  }

  // Internal Nodes
  DoH [label="dns-over-https"];
  GoogleDNS [label="Google DNS\n8.8.8.8\n8.8.4.4"];
  SSHServer [label="SSH Server"];  // Added SSH inside VM

  // CloudFlare Proxy
  subgraph cluster_CloudFlare {
    label = "doh.example.com";
    CloudFlare [label="CloudFlare DNS Proxy"];
  }

  // Edges for the main path (DNS over HTTPS) from Browser A
  BrowserA -> CloudFlare [label="DNS Over HTTPS\n(TCP 443)"];
  
  // Edges for the admin path from Browser B through Tailscale
  BrowserB -> Tailscale [label="HTTP\n(TCP 8080)"];

  // Edge for SSH from SSH Client
  SSHClient -> SecurityList [label="SSH\n(TCP 22)"];  // SSH Connection

  // Internal configuration
  subgraph cluster_OCI {
    label = "OCI";
    
    subgraph cluster_VCN {
      label="VCN";
      
      // Security List inside VCN
      SecurityList [label="Security List\nAllows only SSH, HTTP"];
    }
    
    subgraph cluster_AppServer {
      label = "VM";

      subgraph cluster_PiHole {
        label = "Pi-Hole";
        PiHoleDNS [label="DNS Server"];
        PiHoleConsole [label="Admin Console"];
      }

      DoH -> PiHoleDNS [label="DNS\n(UDP 53)"];
      Tailscale -> PiHoleConsole [label="HTTP\n(TCP 8080)"];
      SecurityList -> SSHServer [label="SSH\n(TCP 22)"];  // Added SecurityList to SSHServer route
    }
    
    // Edges from CloudFlare to VCN and from VCN to AppServer
    CloudFlare -> SecurityList [label="HTTP\n(TCP 80)"];
    SecurityList -> DoH [label="HTTP\n(TCP 80)"];
  }
  
  
      PiHoleDNS -> GoogleDNS [label="DNS\n(UDP 53)"];
}

-->

## Pi-hole インストール

```
curl -sSL https://install.pi-hole.net | bash
```

インタラクティブなインストールを実施する。
必要があればパスワードをより長いものにする。
```
pihole -a -p new_passward
```

全てのインターフェイスからの通信を許可する。
```
pihole -a -i all
```

### lighttpd 設定
```
sudo vi /etc/lighttpd/conf-enabled/15-pihole-admin.conf
```
次の行を追加しポートを8080に変更する。
```
server.port := 8080
```

```
sudo vi /etc/lighttpd/lighttpd.conf
```
デフォルトのポートが80となっていて、後述するdns-over-httpsでの設定とコンフリクトするため次の行のように適当なポートに変更する。
```
server.port = 8000
```

サービスを再起動する。
```
sudo systemctl restart lighttpd
```

## ファイアウォール 設定
iptablesでDNS(UDP 53), HTTP(TCP 80, 8080)の通信を許可する。

```
-A INPUT -p udp --dport 53 -j ACCEPT
-A OUTPUT -p udp --sport 53 -j ACCEPT

-A INPUT -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT

-A INPUT -p tcp --dport 8080 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 8080 -m state --state ESTABLISHED -j ACCEPT
```

注記: 53, 8080ポートはVMの外側のクラウドサービス側のファイアーウォールで遮断され、SSH以外ではTCP 80ポートのみがインターネットからアクセス可能となる想定です。

### 現在の設定の保存
```
sudo netfilter-persistent save
```

### 設定の編集
```
sudo vi /etc/iptables/rules.v4
```

### 変更の反映

```
sudo iptables -F
sudo netfilter-persistent reload
```

DNSサーバーと8080ポートの管理画面にローカルからアクセスできることを確認する。

## Tailscale インストール
```
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
```
表示された次のようなURLから認証を行う。
``` 
https://login.tailscale.com/a/xxxxxxxxxx
```

設定後、Tailnet内の他端末から管理画面にアクセスできることを確認する。

## dns-over-https インストール
```
sudo apt install make git golang
git clone https://github.com/m13253/dns-over-https.git
cd dns-over-https
make
sudo make install
```

```
sudo vi /etc/dns-over-https/doh-server.conf
```

次の2箇所の設定を変更し、セキュアではないHTTP 80ポートで問い合わせを受けるサーバーを立て、上流のDNSサーバーとしてローカルのPi-holeを使うようにする。
```
# HTTP listen port
listen = [
    ":80",

    ## To listen on both 0.0.0.0:8053 and [::]:8053, use the following line
    # ":8053",
]
```

```
upstream = [
    "udp:127.0.0.1:53",
]
```

サービスを開始する。
```
sudo systemctl start doh-server
sudo systemctl status doh-server
```

サービスが正常に動作することを確認したら常に動作するように有効化する。
```
sudo systemctl enable doh-server
```

## クラウドサービス側 セキュリティ設定
必要があれば80ポートを開ける作業を実施する。

## Cloudflare DNS Proxy設定
Aレコードとして適当なサブドメイン名と対象のサーバーのグローバルIPを指定する。
HTTPS化のため、DNS ProxyがProxiedになっていることを確認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47014/920794bd-5ddc-0311-083d-0b82677ce6a3.png)

## DoH(DNS Over HTTPS)サーバー 動作確認
例えば次のようなURLへのGETで200 OKが返ることを確認する。

```
https://your-domain-name/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB
```

レスポンスのバイナリの例。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47014/2f1d21a8-b7f2-f056-9b8e-d2c7c13d69b7.png)

参考:
「4.1.1. HTTP Request Examples」
「4.2.2. HTTP Response Example」

https://tex2e.github.io/rfc-translater/html/rfc8484.html

## ブラウザへのDoH(DNS Over HTTPS)サーバー 設定

参考:

https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/encrypted-dns-browsers/