プロキシ・DNSを実際に構築して理解する

# はじめに
この記事は [NRI OpenStandia Advent Calendar 2024](https://qiita.com/advent-calendar/2024/nri-openstandia) の15日目の記事です。

最近オンプレやAWSで構築された社内基盤を触れる業務が多くなってきました。
今まではふわふわした理解でproxyやDNSと付き合ってきましたが、一度腰を据えて勉強してみるかと考えこの記事を作成するに至りました。
閉域ネットワークをAWS上で再現して、proxy, DNSサーバを構築、閉域ネットワーク内のclient端末から外部ネットワークへの通信を試みます。

# 目標の構成
下記に示すアーキテクチャを構築します。
なお、proxyにはsquid, DNSにはbindを利用しており、private subnetのcilentは閉域ネットワーク内にあるclient端末として想定しています。
![architecture.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412378/eee41812-c8ca-8a27-0b9f-651c50077c81.png)


# サービス構築
今回CFnを使用して、各サービスの構築を実施しました。
また、今回それぞれの挙動の理解を深めることを目的としているので、全てのサービスを立ち上げた後にDNS, proxyの設定を行うこととします。
使用したスクリプトに関しては以下のgithubをご覧ください。
https://github.com/Shiba-You/shibaProxy
基本的には以下のような構成としています。

```
└── <リソース名>
    ├── deploy.<リソース名>.sh         // デプロイのスクリプト
    ├── parameter.<リソース名>.json    // template.<*>.yamlで利用するパラメータ定義（gitには上げていないので注意）
    ├── (setup.<リソース名>.sh)        // ec2内での設定のスクリプト
    └── template.<リソース名>.yaml     // スタック定義
```

## 1. VPC作成
まずは必要となるVPC, IGW, Nat Gateway周りを作成します。
上記リポジトリの `/vpc/template.vpc.yaml` にてスタックを設定していますので、気になる方はご確認ください。
また、 `parameter.vpc.json` を新規に作成すれば `deploy.vpc.sh` からスタック作成可能です。
`parameter.vpc.json` に記載する項目は `parameter.vpc.json` からご確認ください。

## 2. bastionサーバ作成
続いて、bastion（踏み台）サーバを作成します。
VPCと同様に `/bastion/template.bastion.yaml` にてスタックを設定しています。
なお、インバウンドにadminとなる自宅のIPアドレスを、アウトバウンドにはVPC内へのssh接続を許可しています。

## 3. DNSサーバ作成
次に、DNSサーバを作成します。
DNSサーバでは、インバウンド方向に Public Subnet(10.0.10.0/24) からのポート53(tcp/udp), 953を許可、アウトバウンド方向に nat gatewayへのポート53(tcp/udp), 80, 443を許可しています。
ここで、一度bastionサーバからDNSサーバにアクセスできるか確認してみます。

```sh
# ssh-agent にローカルの秘密鍵を追加する
eval "$(ssh-agent -s)"
ssh-add <秘密鍵へのパス>
ssh -A ec2-user@<bastionサーバのパブリックIPv4 DNS> -t ssh ec2-user@<DNSサーバのプライベートIP DNS名>
> 
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
Last login: aaa MMM DD hh:mm:ss YYYY from nn.nn.nn.nn
```

上記をローカルPCで実行すると、無事にDNSサーバへssh接続できていることがわかるかと思います。

## 4. proxyサーバ作成
次に、proxyサーバを作成します。
proxyサーバでは、インバウンド方向にVPC内からのポート80, 443, 3128を許可、アウトバウンドに外部ネットワークへポート80, 443を、DNSサーバへポート53を許可しています。

## 5. clientサーバ作成
最後にclientサーバを作成します。
clientのインバウンド方向では、proxyサーバ向きであればどのような通信であっても許可するようにしています。
DNSサーバと同様に、bastionサーバからclientサーバにアクセスできるか確認してみます。

```sh
ssh -A ec2-user@<bastionサーバのパブリックIPv4 DNS> -t ssh ec2-user@<clientサーバのプライベートIP DNS名>
```

DNSと同様なコンソール画面になれば問題ないです。
これで役者が全て揃ったので、ようやく各種サーバの設定を行いたいと思います。

# 各種サーバ設定
現状ではまだproxyの設定やDNSの設定ができておらず、適切にclientサーバから外部ネットワークへアクセスすることができません。
設定しつつ、各段階での疎通状況などを見ていきます。

## client設定
まずは、clientサーバの設定を行います。
以下でclientサーバへssh接続します。
```sh
ssh -A ec2-user@<bastionサーバのパブリックIPv4 DNS> -t ssh ec2-user@<DNSのプライベートIP DNS名>
```

clientサーバに環境変数を設定し、現在の疎通状況を確認してみます。
（このときの3128は後ほど設定するsquidのデフォルトのportです。）
```sh
echo export http_proxy=http://<proxyサーバのIP>:3128 >> ~/.bashrc
echo export https_proxy=http://<proxyサーバのIP>:3128 >> ~/.bashrc
source ~/.bashrc
```

疎通確認のため、google.comにgetリクエストを送信してみます。

```sh
curl -v www.google.com
>
* connect to <proxyサーバのIP> port 3128 from <clientサーバのIP> port 42474 failed: Connection timed out
* * Failed to connect to <proxyサーバのIP> port 3128 after 132406 ms: Couldn't connect to server
* Closing connection
curl: (28) Failed to connect to <proxyサーバのIP> port 3128 after 132406 ms: Couldn't connect to server
```

実行結果から、proxyサーバで通信が止まってしまいタイムアウトされることが分かるかと思います。
これはproxyサーバが未設定のため、clientのリクエストを適切に処理できていないことが原因ですので、続いてproxyサーバのセットアップを実施します。


## proxy設定
続いてproxyサーバのセットアップです。
今回proxyサーバにはOSSであるsquidを使用します。

```sh
sudo yum update -y
sudo yum install -y squid
```

squidの設定ファイルには、デフォルトで `localnet` という名前でACLが既に定義されています。
今回proxyを利用したいclientは `10.0.0.0/16` のVPC内に立っているため、 `localnet` 内で既に定義済みの `10.0.0.0/8` に包含されています。
今回はそこまで厳しく制限しないため、 この `localnet` をそのまま許可するように修正します。
なお、squidの設定に関しては、[こちら](https://www.novell.com/ja-jp/documentation/sles11/book_sle_admin/data/sec_squid_configfile.html)に詳細がまとまっております。

```sh
sudo vi /etc/squid/squid.conf
> （追記）
http_access allow localnet                  # ACL lcoalnet の許可
```
squidを再起動して、設定ファイルの読み込みを行います。
```sh
sudo systemctl restart squid
```

上記の設定で `10.0.0.0/16` の端末はこのsquidを経由して外部のネットワークに出ることができるようになりました。
実際にclientから外部へhttp通信できるか試してみます。

```sh
curl -v www.google.com
> 
...
< HTTP/1.1 200 OK
...
```

上記のようにアクセスできていることが確認できるかと思います。
また、自身のIPアドレスを表示させる `ifconfig.me` にgetリクエストを行うことで、proxy経由で外部ネットワークに通信が行われていることがわかります。

```sh
curl ifconfig.me
> 
<proxyサーバのグローバルIP>
```

proxy側では、以下のコマンドで通信状況のログを確認することができます。

```sh
sudo vi /var/log/squid/access.log
> 
1733558149.812    167 <clientサーバのIP> TCP_MISS/200 21199 GET http://www.google.com/ - HIER_DIRECT/172.217.175.68 text/html
```
なお、このログのフォーマットはsquidで制御可能です。
詳細は、[こちら](https://www.novell.com/ja-jp/documentation/sles11/book_sle_admin/data/sec_squid_configfile.html)をご覧ください。

## DNS設定
前章まででproxyを用いた通信自体は可能となりましたが、現状では外部のDNSサーバを利用しています。
自前のDNSサーバを利用することで、セキュリティの向上や名前解決の高速化、閉域ネットワーク内でのみの名前解決など様々なメリットを享受することができます。（運用コストとはトレードオフになりますが...）
折角なのでclientサーバからのDNSクエリを禁止し、proxyサーバから自前のDNSサーバへDNS問い合わせするように設定していきます。

まず、現在clientサーバのDNSクエリの送信先を確認します。

```sh
cat /etc/resolv.conf
>
...
nameserver 10.0.0.2
...
```

clientサーバのAMI（Amazon Linux 2023）ではsystemd-resolvedが標準搭載（[公式](https://repost.aws/ja/knowledge-center/ec2-static-dns-ubuntu-debian)）されています。 
今回は、自前のDNSサーバを利用していることを確認するために、あえてsystemd-resolvedを停止させてから `/etc/resolv.conf` を修正していきます。

```sh
# systemd-resolved の停止
sudo systemctl status systemd-resolved
sudo systemctl disable systemd-resolved 

# clientでの名前解決禁止
sudo vi /etc/resolv.conf
> (修正)
...
# nameserver 10.0.0.2                       # コメントアウトする
# search ap-northeast-1.compute.internal    # コメントアウトする
```

続いて、proxyサーバでDNSリクエストを自前のDNSサーバへ送るように設定します。

```sh
sudo vi /etc/squid/squid.conf
> （追記）
...
dns_nameservers <DNSサーバのIPアドレス>

...
```

上記の追記のみでproxyを経由する通信のDNSリクエストをDNSサーバへ送信してくれます。

続いて、DNSサーバとしてのセットアップを行います。
なお、セットアップに関しては[こちらの記事](https://qiita.com/koinunopochi/items/091dd3fe111dd37c328c)を参考にさせていただきました。
今回DNSサーバにはOSSであるbindを使用します。
```sh
sudo yum update -y
sudo yum install bind -y
```
設定ファイルを修正します。
今回は、ゾーンファイルを用意せずに単純にDNSクエリを受け取り、上位DNSサーバにフォワーディングするだけの設定としています。
```sh
sudo vi /etc/named.conf
> （修正）
...
options {
    ...
    listen-on port 53 { 127.0.0.1; <DNSサーバのIPアドレス>};
    allow-query     { localhost; 10.0.0.0/8; };
    forwarders { 8.8.8.8; 8.8.4.4; };
    forward only;
    ...
}
...
```

namedを再起動して、設定ファイルの読み込みを行います。
```sh
sudo systemctl restart named
```

以上でDNSサーバの設定は完了です。
では、clientサーバからproxyサーバ経由で名前解決できていることを確認します。
で
```sh
dig google.com
> 
...
;; SERVER: <DNSサーバのIPアドレス>#53(<DNSサーバのIPアドレス>) (UDP)
...
```
上記のように `<DNSサーバのIPアドレス>` で名前解決できていることが確認できると思います。

# まとめ

以上で目標として構成が完成しました。
今まで雰囲気でしか理解できていなかった領域が「少しわかった」くらいにはなったと思います。
やはり座学だけでなく、実際に手を動かすことは重要ですね。