サービスメッシュ Kuma の Transparent Proxy を既存 DNS と共存させる

[日立グループ OSS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/hitachi-oss) 13 日目の記事です。

Kuma 紹介記事の第二弾です。

1. [サービスメッシュ Kuma の サンプルを Docker Compose で動かす](https://qiita.com/t-mogi/items/954e529a037c4e283954)
2. 本記事
3. [サービスメッシュ Kuma で iptables と resolv.conf の設定を取得する方法](https://qiita.com/enori/items/3dbd1153b9d6e7e8bc70#_reference-05a45b468dc0062664b6)

(株)日立製作所 研究開発グループ サービスコンピューティング研究部の井出です。
[Kuma](https://kuma.io/docs/latest/) の Transparent Proxy (透過プロキシ) 構成にて Kuma の DNS と既存の DNS 設定を共存させる方法を紹介します。なお、本記事は Kuma v1.3.1 が前提です。

## 要約

Kuma の Transparent Proxy 設定時、`--skip-resolv-conf`と`--redirect-dns`オプションを追加すると、`.mesh`のドメインのみ Kuma で名前解決し、それ以外は既存 DNS に問い合わせてくれます。

既存 DNS 設定に手を入れなくて良くなるので便利です。

```sh
sudo kumactl install transparent-proxy \
    --kuma-dp-user kuma-dp \
    --kuma-cp-ip $CONTROL_PLANE_IP \
    --skip-resolv-conf \
    --redirect-dns
```

## Kuma とは

[Kuma](https://kuma.io/docs/1.4.0/) は OSS のサービスメッシュツールです。
2021 年現在、CNCF の [Sandbox Project](https://www.cncf.io/sandbox-projects/) に所属しています。

Kuma ではトラフィック制御や、セキュリティ管理、監視をアプリから透過的に実施可能です。L7 レベルのアクセス制御など、細かい部分で未対応な機能もありますが、サービスメッシュの主要機能は大体実装されている印象です。Kuma と他ツールとの機能比較は [Service Mesh Comparison - INNOQ](http://servicemesh.es/) が詳しいです。

私的に、Kuma の一番の特徴は Kubernetes 以外でも Control Plane を稼働できる Universal Mode だと思います。Kubernetes がなくとも動作するため、VM ベースのシステムにサービスメッシュを導入する場合に便利です。

本記事も VM 上に透過プロキシ(Transparent Proxy)として Kuma を構築する際の Tips になります。

## [Transparent Proxy](https://kuma.io/docs/latest/networking/transparent-proxying/) 構成とは

Transparent Proxy 構成とは、Data Plane を透過プロキシとして用いる構成です。アプリケーションは Kuma を意識せずに通信できるため、ポートマッピングに比べて Kuma を管理しやすくなります。

Kuma の Transparent Proxy は、`iptables`を用いて inbound / outbound のトラフィックを Kuma の Data Plane である [Envoy Proxy](https://www.envoyproxy.io/) に転送することで実現しています。Kuma が`iptables`をどのように用いているかは[こちらの記事](https://qiita.com/enori/items/3dbd1153b9d6e7e8bc70#_reference-05a45b468dc0062664b6)を参照下さい。

Transparent Proxy は確かに便利なのですが、[公式ドキュメント](https://kuma.io/docs/latest/networking/transparent-proxying/)の手順は、サービスディスカバリのために Kuma 独自の DNS を使う必要があるため、VM 環境で利用すると以下のような問題が発生します。

- Control Plane への UDP 53 ポート割り当て
    - 要 sudo 権限
    - `systemd-resolved`との競合回避要
- Data Plane の DNS サーバを Kuma 専用に変更
    - Kuma 用のドメイン(`.mesh`)以外は名前解決できない
    - DHCP による DNS サーバのアドレスを配布と相性が悪い
    - `systemd-resolved`を用いている場合、自動インストール不可  
      (`kumactl install transparent proxy`は`/etc/hosts`を変更するのみ)

これら問題は`kumactl install transparent proxy`の`--redirect-dns`オプションを使うといい感じに回避できます。

## Redirect DNS 設定

Redirect DNS は Transparent Proxy 利用時に DNS トラフィック(TCP/UCP 53)を Envoy Proxy に飛ばし、Kuma 用ドメイン(`.mesh`)の場合は Envoy Proxy で名前解決し、それ以外のドメインは元々の DNS リゾルバに転送してくれる設定です。公式ドキュメントの [DNS](https://kuma.io/docs/latest/networking/dns/) に記載されています。

Redirect DNS の挙動を以下に示します(CoreDNS は Kuma が用いる DNS サーバ)。
リダイレクトにより 53 ポートの通信を 15053 ポートに飛ばし、CoreDNS にて名前解決します。
また、Transparent Proxy 構成であるため、inbond / outbound 通信は Envoy Proxy 経由でやり取りされます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542848/0d65cb4d-3d33-fd98-4181-a268509c4a8b.png" width="100%">

Redirect DNS を使うには、以下のように Transparent Proxy の設定時に`--skip-resolv-conf`と`--redirect-dns`を付与します。`kumactl install transparent-proxy`コマンドは`iptables`と`/etc/resolv.conf`を書き換えるコマンドです。他オプションの内容は[公式ページ](https://kuma.io/docs/latest/networking/transparent-proxying/#setting-up-the-service-host)や[`kumactl`のヘルプ](https://github.com/kumahq/kuma/blob/master/docs/cmd/kumactl/kumactl_install_transparent-proxy.md)を参照下さい。

```sh
sudo kumactl install transparent-proxy \
    --kuma-dp-user kuma-dp \
    --kuma-cp-ip $CONTROL_PLANE_IP \
    --skip-resolv-conf \
    --redirect-dns
```

Redirect DNS が有効化された場合、`iptables`に`tcp dpt:53 redir ports 15053`が付与されます。

```sh
sudo iptables -t nat -n -L | grep 53
# REDIRECT   tcp  --  0.0.0.0/0            127.0.0.53           tcp dpt:53 redir ports 15053
```

Redirect DNS により、外部のドメインと同じように Kuma のドメインを名前解決できるようになります。

```sh
nslookup www.hitachi.com
# Server:         127.0.0.53
# Address:        127.0.0.53#53

# Non-authoritative answer:
# www.hitachi.com canonical name = www.hitachi.com.edgekey.net.
# www.hitachi.com.edgekey.net     canonical name = www.hitachi.com.edgekey.net.globalredir.akadns.net.
# www.hitachi.com.edgekey.net.globalredir.akadns.net      canonical name = e7880.a.akamaiedge.net.
# Name:   e7880.a.akamaiedge.net
# Address: 104.71.147.221

nslookup server1.mesh
# Server:         127.0.0.53
# Address:        127.0.0.53#53

# Non-authoritative answer:
# Name:   server1.mesh
# Address: 240.0.0.1
# ** server can't find server1.mesh: NXDOMAIN
```

## (参考) Redirect DNS では何がおきているか

参考のため、Redirect DNS の挙動を調査した結果をまとめます。

Redirect DNS はおおよそ以下のような挙動をとります。
ただし、4 は裏付けが取れていないため、間違っている可能性があります。ご注意下さい

1. `iptables`設定にて DNS トラフィックを TCP/UDP 15053 ポートの CoreDNS に転送
2. CoreDNS が UDP 15054 ポートの Envoy Proxy に転送
3. Envoy Proxy にて`.mesh`ドメインを`240.0.0.0/2`の IP アドレスに名前解決
4. 名前解決できない場合(`.mesh`ドメイン以外)、CoreDNS が本来の DNS リゾルバ経由で名前解決(予想)
5. CoreDNS が DNS リゾルバを送信元にして、元々の問合せ元に名前解決の結果(A or AAAA レコード)を返却

以降、1、2、3、5 の挙動の確認方法を簡単に示します。

### 1. `iptables`設定にて DNS トラフィックを TCP/UDP 15053 ポートの CoreDNS に転送

`iptables`に関して Redirect DNS オプションを付与すると以下設定が追加されます(差分や関連項目のみ表示)。
差分から、53 番ポートへのアクセスを 15053 に転送しようとしていることが分かります。

```diff:iptables設定
-DNS_CAPTURE=false
+DNS_CAPTURE=true
 REDIRECT_ALL_DNS_TRAFFIC=false
-DNS_SERVERS=[],[]
+DNS_SERVERS=[127.0.0.53],[]
 AGENT_DNS_LISTENER_PORT=15053
 DNS_UPSTREAM_TARGET_CHAIN=RETURN

# IPv4設定
 * nat
--A MESH_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --uid-owner 1001 -j MESH_IN_REDIRECT
--A MESH_OUTPUT -o lo -m owner ! --uid-owner 1001 -j RETURN
+-A MESH_OUTPUT -o lo ! -d 127.0.0.1/32 -p tcp ! --dport 53 -m owner --uid-owner 1001 -j MESH_IN_REDIRECT
+-A MESH_OUTPUT -o lo -p tcp ! --dport 53 -m owner ! --uid-owner 1001 -j RETURN
--A MESH_OUTPUT -o lo -m owner ! --gid-owner 1001 -j RETURN
+-A MESH_OUTPUT -o lo -p tcp ! --dport 53 -m owner ! --gid-owner 1001 -j RETURN
+-A MESH_OUTPUT -p tcp --dport 53 -d 127.0.0.53/32 -j REDIRECT --to-ports 15053
+-I OUTPUT 1 -p udp --dport 53 -m owner --uid-owner 1001 -j RETURN
+-I OUTPUT 2 -p udp --dport 53 -m owner --gid-owner 1001 -j RETURN
+-I OUTPUT 3 -p udp --dport 53 -d 127.0.0.53/32 -j REDIRECT --to-port 15053

# IPv6設定
 * nat
+-I OUTPUT 1 -p udp --dport 53 -m owner --uid-owner 1001 -j RETURN
+-I OUTPUT 2 -p udp --dport 53 -m owner --gid-owner 1001 -j RETURN
```

そこで、`lsof`などで 15053 ポートを調べると、CoreDNS に使用されていることが分かります。

```sh
sudo lsof -i:15053
# COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
# coredns 1174 kuma-dp    7u  IPv6  27112      0t0  TCP *:15053 (LISTEN)
# coredns 1174 kuma-dp    8u  IPv6  27113      0t0  UDP *:15053
```

### 2. CoreDNS が UDP 15054 ポートの Envoy Proxy に転送

`/tmp/kuma-dp-<ID>/Corefile`の CoreDNS の設定を見ると、15053 ポート宛のトラフィックを 15054 ポートに転送していることが分かります。

```sh:Corefile
.:15053 {
    forward . 127.0.0.1:15054
    # We want all requests to be sent to the Envoy DNS Filter, unsuccessful responses should be forwarded to the original DNS server.
    # For example: requests other than A, AAAA and SRV will return NOTIMP when hitting the envoy filter and should be sent to the original DNS server.
    # Codes from: https://github.com/miekg/dns/blob/master/msg.go#L138
    alternate NOTIMP,FORMERR,NXDOMAIN,SERVFAIL,REFUSED . /etc/resolv.conf
    prometheus localhost:19153
    errors
}

.:15055 {
    template ANY ANY . {
      rcode NXDOMAIN
    }
}
```

Data Plane の起動コマンドである`kuma-dp run`のヘルプを見ると 15054 ポートは Envoy Proxy にてバーチャル IP を名前解決する用途と分かります。また、説明文から CoreDNS は最初に 15054 ポートへアクセスし、その後、実際の DNS リゾルバを試す仕様のようです。

```sh
kuma-dp run -h
# Launch Dataplane (Envoy).
# ~ 略 ~
# Flags:
# ~ 略 ~
# --dns-envoy-port uint32    A port that handles Virtual IP resolving by Envoy.
#                            CoreDNS should be configured that it first tries to
#                            use this DNS resolver and then the real one (default 15054)
```

### 3. Envoy Proxy にて`.mesh`ドメインを`240.0.0.0/2`の IP アドレスに名前解決

`.mesh`ドメインである`server1.mesh`にアクセスしたときの tcpdump および Envoy Proxy の strace 結果を以下に示します。15054 ポートでアクセスされた Envoy Proxy が CoreDNS に応答を返していることが分かります。

```sh
ubuntu@client:~$ sudo tcpdump -i lo not port ssh and not port 5678 and not port 30001 and not arp
# ~ 略 ~
04:21:55.814336 IP localhost.55218 > localhost.15053: UDP, length 30                # Application から CoreDNS へのリクエスト
04:21:55.814525 IP localhost.53117 > localhost.15054: UDP, length 30                # CoreDNS から Envoy Proxy へのリクエスト               
04:21:55.814643 IP localhost.15054 > localhost.53117: UDP, length 58                # Envoy Proxy から CoreDNS への応答
04:21:55.814709 IP localhost.domain > localhost.55218: 7928 1/0/0 A 240.0.0.1 (58)  # Application への応答
```

この応答を WireShark などで DNS として読み取ると`server1.mesh`を名前解決した A レコードであることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542848/8bfd6d13-9fb6-562b-1c98-27f78dc90c0b.png)

### 4. 名前解決できない場合(`.mesh`ドメイン以外)、CoreDNS が本来の DNS リゾルバ経由で名前解決(予想)

前述のように`kuma-dp`のヘルプメッセージを読む限り、CoreDNS は Envoy Proxy での名前解決に失敗した後は実際の DNS リゾルバでの名前解決を試す仕様と考えられます。しかし、tcpdump や strace で CoreDNS のリクエストの流れを追っても既存の DNS リゾルバへのアクセスは確認できませんでした。

### 5. CoreDNS が DNS リゾルバを送信元にして、元々の問合せ元に名前解決の結果(A or AAAA レコード)を返却

以下 tcpdump 結果を見ると、Application には 46291 ポート宛で名前解決結果が返されていることが分かります。

```sh
04:08:43.784631 IP localhost.46291 > localhost.15053: UDP, length 30                # Application から CoreDNS へのリクエスト
04:08:43.786176 IP localhost.43931 > localhost.15054: UDP, length 30                # CoreDNS から Envoy Proxy へのリクエスト
04:08:43.786837 IP localhost.15054 > localhost.43931: UDP, length 58                # Envoy Proxy から CoreDNS への応答
04:08:43.787368 IP localhost.domain > localhost.46291: 12290 1/0/0 A 240.0.0.1 (58) # Application への応答
```

strace を用いて CoreDNS のプロセスのシステムコールを見ていると、上と同タイミングで CoreDNS から 46291 ポートへのリクエストが送付されており、`localhost.domain`が実は CoreDNS であると分かります。

```sh
sudo strace -tt -ff -e trace=network -p 1174 # CoreDNS の PID
# ~ 略 ~
04:08:43.784798 recvmsg(8, {msg_name={sa_family=AF_INET6, sin6_port=htons(46291), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, msg_namelen=112->28, msg_iov=[{iov_base="0\2\1\0\0\1\0\0\0\0\0\0\7server1\4mesh\0\0\1\0\1", iov_len=512}], msg_iovlen=1, msg_control=[{cmsg_len=36, cmsg_level=SOL_IPV6, cmsg_type=0x32}], msg_controllen=40, msg_flags=MSG_CTRUNC}, 0) = 30
04:08:43.787281 sendmsg(8, {msg_name={sa_family=AF_INET6, sin6_port=htons(46291), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, msg_namelen=28, msg_iov=[{iov_base="0\2\201\200\0\1\0\1\0\0\0\0\7server1\4mesh\0\0\1\0\1\7s"..., iov_len=58}], msg_iovlen=1, msg_control=[{cmsg_len=28, cmsg_level=SOL_IP, cmsg_type=IP_PKTINFO, cmsg_data={ipi_ifindex=0, ipi_spec_dst=inet_addr("127.0.0.1"), ipi_addr=inet_addr("0.0.0.0")}}], msg_controllen=32, msg_flags=0}, 0) = 58 #これ
```

以上が、Redirect DNS のおおよその挙動となります。

## まとめ

本記事では、Kuma の Transparent Proxy にて既存 DNS をそのまま使用できる`--redirect-dns`オプションを解説しました。これにより、OS レベルでの設定変更がほぼ不要になるため、既存環境への Kuma 組み込みの敷居を低減可能です。

Kuma はサービスメッシュツールの中でも珍しく Kubernetes に依存しない構成を取れるため、VM で構築された環境への導入に適しています。皆様も一度試してみてはいかがでしょうか。