bindのRPZを使ってDNSレコードを偽装する

アプリ開発などで、本番用ドメインを使用したまま開発環境へアクセスしたいケースがある。
今回は、bindのRPZという機能を使い特定のレコードの応答を偽装してみる。

# RPZについて
JPNICの「[RPZとは](https://www.nic.ad.jp/ja/basics/terms/rpz.html)」より引用。
> RPZ (Response Policy Zones)とは、フィッシングサイトやマルウェア配布サイトといった、 特定のノードへの接続防止などを目的とした、DNSによるフィルタリング機能の一つです。

元々はフィルタリング機能であり、近年では児童ポルノ対策のブロッキング方式として採用されているケースもある。
今回はこの機能を利用する。

# 用意するもの
ざっくり以下の３つを用意。

- DNSサーバ
- named.conf
- rpzゾーンファイル

なお、DNSサーバについては、「[検証用にDockerでbindを動かす](https://qiita.com/motani/items/12ff01ab8190b43b0d7e)」をベースに利用するため割愛。

## named.conf
named.confの例

```conf
logging {
    channel "query-log" {
        file "/var/log/query.log" versions 6 size 1024M;
        severity dynamic;
        print-time yes;
        print-category yes;
    };
    category queries { "query-log"; };
    category lame-servers { null; };
    category resolver { null; };
};

include "/etc/rndc.key";
controls {
    inet 127.0.0.1 allow { 127.0.0.1; } keys { "rndc-key"; };
};

acl "access-list" {
    127.0.0.1;
    172.17.0.0/16;
};

options {
    directory "/var/named/";
    pid-file "/run/named/";
    dump-file "/var/named/named_dump.db";
    statistics-file "/var/named/named.stats.log";

    zone-statistics yes;
    version "";
    masterfile-format text;
    recursive-clients 10000;
    tcp-clients 10000;

    allow-recursion { access-list; };
    allow-query { access-list; };
    allow-query-cache { access-list; };

    response-policy { zone "rpz.zone"; };
};

view "internal" {
    recursion yes;

    zone "." {
        type hint;
        file "named.root";
    };

    zone "0.0.127.in-addr.arpa" {
        type master;
        file "localhost.rev";
    };

    zone "rpz.zone" {
        type master;
        file "rpz.zone";
    };

    zone "sample.com" {
        type master;
        file "sample.com.zone";
    };
};
```

### 解説
#### response-policyオプションの設定
optionsにresponse-policyを定義する。
zoneは、後々定義するzone名と合わせる必要がある。

```
    response-policy { zone "rpz.zone"; };
```

#### rpz.zoneの定義
通常zone名はゾーンそのものを指すが、rpzの場合は特に意味はない。
fileにはrpzの設定ファイル名を指定する。

```
    zone "rpz.zone" {
        type master;
        file "rpz.zone";
    };
```

## rpzゾーンファイル

```conf
$TTL 900
@   IN  SOA localhost. postmaster.localhost. (
        2020062101      ; Serial Number
        1800            ; Refresh
        900             ; Retry
        1209600         ; expire
        900             ; minimum
        )
@   IN  NS  localhost.
www.sample.com    IN  A   127.0.0.1
google.com        IN  A   127.0.0.1
hogefugamoge.yahoo.co.jp IN  A   127.0.0.1
```

重要なところは、下三行のレコード部分。
上書きしたいドメイン名をFQDNで記載し、期待する応答をレコードとして記載する(終端のピリオドはなくてもOK)。
存在する・しないに関わらず記載することができるため、柔軟にDNSの偽装が可能である。

# 検証
上記の設定ファイルを元にDockerを起動する。
今回は以前起動したDNSのimageがあったので、buildして再作成。

```
$ sudo docker build . -t bind
$ sudo docker stop bind
$ sudo docker rm bind
$ sudo docker run -it -d -p 53:53 -p 53:53/udp --name bind bind:latest
```

・google.comを引いてみる

```
$ dig @localhost google.com +nostat

; <<>> DiG 9.9.4-RedHat-9.9.4-38.el7_3.3 <<>> @localhost google.com +nostat
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62337
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;google.com.			IN	A

;; ANSWER SECTION:
google.com.		5	IN	A	127.0.0.1

;; AUTHORITY SECTION:
rpz.zone.		900	IN	NS	localhost.
```

・存在しないドメイン(hogefugamoge.yahoo.co.jp)を引いてみる

```shell
$ dig @localhost hogefugamoge.yahoo.co.jp +nostat

; <<>> DiG 9.9.4-RedHat-9.9.4-38.el7_3.3 <<>> @localhost hogefugamoge.yahoo.co.jp +nostat
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17667
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;hogefugamoge.yahoo.co.jp.	IN	A

;; ANSWER SECTION:
hogefugamoge.yahoo.co.jp. 5	IN	A	127.0.0.1

;; AUTHORITY SECTION:
rpz.zone.		900	IN	NS	localhost.
```

うまく機能している！

# 注意点
RPZはDNS応答を「上書き」する機能であるため、実際の名前解決フローは実施される。
そのためbindが名前解決をできる状態でないと機能しない。
例えば、外部疎通性のない環境では下記のようになる。

```shell
$ dig @localhost google.com +nostat

; <<>> DiG 9.9.4-RedHat-9.9.4-38.el7_3.3 <<>> @localhost google.com +nostat
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 37778
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;google.com.			IN	A
```
google.comが解決できないため、SERVFAILとなる。。。

また、権威を持っているゾーンについても上書きできなかった。
・sample.comゾーンの場合

```shell
$ dig @localhost www.sample.com +nostat

; <<>> DiG 9.9.4-RedHat-9.9.4-38.el7_3.3 <<>> @localhost www.sample.com +nostat
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57804
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.sample.com.			IN	A

;; ANSWER SECTION:
www.sample.com.		900	IN	A	127.0.0.1

;; AUTHORITY SECTION:
sample.com.		900	IN	NS	sample.com.

;; ADDITIONAL SECTION:
sample.com.		900	IN	A	127.0.0.1
```

この辺りはdnsmasqの方が柔軟かもしれない。