Cloud NAT の Endpoint-independent Mapping とは?

Seiji Ariga
google-cloud-jp
Published in
18 min readFeb 10, 2021

去年 2020 年の 10 月 6 日にプレビューとしてリリースされ、12 月 16 日に GA (一般提供)になった Cloud NAT の機能に、Endpoint-independent Mapping の有効化/無効化という機能があります。

これが何なのかよく分からなかったので調べてみました。(なお、結局は Cloud NAT 特有の話ではなく、NAT 一般における「Endpoint-independent Mapping」の話なので、ご存知の方は以降読まなくても大丈夫です。)

Cloud NAT のアイコン

Cloud NAT

Endpoint-independent Mapping の話に入る前に、前提となる Cloud NAT の紹介をします。

Cloud NAT は Google Cloud の仮想マシン (VM) に外部 IP アドレスをつけなくても外部(インターネット)と通信できるように NAT (Network Address Translation) の機能をマネージドで提供するサービスになります。(マネージドの Kubernetes サービスである Google Kubernetes Engine からももちろん使えます)

Cloud NAT を使うことで VM に外部 IP アドレスを付与せずにインターネットとの通信ができ、外部からの VM への直接のアクセスを防ぐことができるため、VM がある VPC を外部のネットワークと切り離すことができます。

Cloud NAT の特徴

Cloud NAT の特徴は NAT をおこなうゲートウェイ的なものが VM からインターネットへの通信経路上に存在せず、各 VM が自律分散的に NAT をおこなっている点にあります。

よくある NAT のモデルと Cloud NAT の違い

これは Google Cloud の VPC の基盤である Andromeda で実現されており、そのため中間ノード(ゲートウェイなど)によって NAT を実現している場合に発生し得る中間ノードのパフォーマンス上の制限(たとえば限りある中間ノードのパフォーマンスを一部の VM で使い切ってしまうなど)や、中間ノードの可用性の問題などは原理的に発生しません。

(なお、そのためスループットは各 VM のスループットに依存します)

一方、各 VM が自律分散的に NAT をおこなっており、外部 IP アドレスを共用しているため、上図のように通信に使う送信元ポート番号が重複しないようにうまく割り当てる必要があります。(複数の VM が同じ NAT IP アドレスと送信元ポート番号を使ってしまった場合、戻りのパケットをどの VM へ送ればいいかわからなくなるためで。)デフォルトでは NAT に使われる外部 IP アドレスやポートの管理は Cloud NAT が動的におこないますので、ユーザーとしてはあまり気にすることはありません。

Cloud NAT の動作の基本

それでは実際に Cloud NAT が動作している様子を見てみます。動作が分かりやすくなるよう、わざと以下のように設定します。

この設定において各 VM からは、同じ宛先アドレス + 宛先ポート番号に対して、同時に 8 セッションまで接続することができます

例として、Cloud NAT 配下の VM 192.168.2.2 から 8 つのプロセスがすべて同時に 203.0.113.19:80 に接続する場合、NAT の変換テーブルは以下の通りとなります。

    内部IP:送信元ポート    外部IP:送信元ポート   宛先アドレス:ポート
(1) 192.168.2.2:10000 - 34.84.146.5:1028 - 203.0.113.19:80
(2) 192.168.2.2:10001 - 34.84.146.5:1029 - 203.0.113.19:80
(3) 192.168.2.2:10002 - 34.84.146.5:1030 - 203.0.113.19:80
(4) 192.168.2.2:10003 - 34.84.146.5:1031 - 203.0.113.19:80
(5) 192.168.2.2:10004 - 34.84.146.5:1032 - 203.0.113.19:80
(6) 192.168.2.2:10005 - 34.84.146.5:1033 - 203.0.113.19:80
(7) 192.168.2.2:10006 - 34.84.146.5:1034 - 203.0.113.19:80
(8) 192.168.2.2:10007 - 34.84.146.5:1035 - 203.0.113.19:80

これで、この VM に割り当てられた 8 つのポート番号はすべて使い切ってしまうため、これ以上 203.0.113.19:80 へは接続できません。(ただし、後述する通り、203.0.113.19:80 以外へはもちろん接続できます。)

実際に VM からわざと 9 セッション同時に接続しようとしてみます。

$ for n in $(seq 0 8); do echo "GET / HTTP/1.0" | nc -w5 -p 1000${n} 203.0.113.19 80 & done

すると [203.0.113.19] 80 (?) : Connection timed out のようなエラーが出て、同時に接続できたのは 8 セッションまでになります。(2 行目〜9 行目)

接続先での TCP セッションの様子

(なお、実行結果を見て分かるように、当該に VM に実際に割り当てられた送信元ポートは 1028〜1031、32768〜32771 の 8 つです)

これは、同一の宛先アドレス + 宛先ポートに接続するためには送信元ポートを異なるものにする and/or 送信元アドレスを異なるものにする必要があるためです。(そして、この記事では送信元アドレスを 1 つに固定しているため、送信元ポートを使い切るとそれ以上は接続できなくなります。デフォルトでは送信元アドレスが動的に増え、通信を継続できます。)

一方、同一の宛先アドレス + 宛先ポートへの接続ではない場合はその制約が無いため、同時 8 セッションまでといった制限はありません。

たとえば、同一宛先アドレスの複数のポートへ同時に合計 4 セッション接続してみます。

$ for p in 80 443 3128 8080; do echo "GET / HTTP/1.0" | nc -w30 -p 10000 203.0.113.19 ${p} & done

すると、実際のセッションは以下の通りとなり、たとえば 34.84.146.5:1031 という 1 つの送信元アドレス + 送信元ポートから 203.0.113.19:80,443,3128,8080 の 4 つの宛先ポートへ接続できてることが分かります。(10 行目〜13 行目)

異なる宛先ポートへの接続の様子

このように、送信元ポートの数の制限は、同一の宛先(アドレス+ポート)に同時に多くのセッションを張るようなユースケース以外では問題になりません。

NAT 時の外部アドレス:ポートの割当

さて、ようやく本題の Endpoint-Independent Mapping の話に入っていきます。

Endpoint-Independent Mapping 自体は Cloud NAT とは関係なく NAT の定義の話なので、まずはそちらから確認していきたいと思います。

言葉の定義

「Endpoint-Independent Mapping」は RFC 5128 2.3. Endpoint-Independent Mapping にありますが、内容自体は RFC 4787 4.1. Address and Port Mapping で定義されてます。

The NAT reuses the port mapping for subsequent packets sent from the same internal IP address and port (X:x) to any external IP address and port.  Specifically, X1':x1' equals X2':x2' for all values of Y2:y2.(私訳:同じ内部 IP アドレスとポートの組み合わせ(X:x)から、なんらかの外部 IP アドレスとポートへ送られる一連のパケットは、NAT の際に同じポートマッピングを利用します。具体的には、すべての Y2:y2 に対して X1':x1' と X2':x2' が同じになります。(Y2:y2 は接続先のアドレス:ポート、X1':x1'、X2':x2' は NAT 後の外部アドレス:ポート をそれぞれ表す)

図にするとこんな感じです。

Endpoint-Independent Mapping

逆に「Endpoint-Dependent Mapping」(RFC 5128 2.4.)は「Address-Dependent Mapping」と「Address and Port-Dependent Mapping」の組み合わせで、定義(RFC 4787 4.1.)はこんな感じです。

Address-Dependent Mapping

The NAT reuses the port mapping for subsequent packets sent from the same internal IP address and port (X:x) to the same external IP address, regardless of the external port. Specifically, X1':x1' equals X2':x2' if and only if, Y2 equals Y1.(私訳:同じ内部 IP アドレスとポートの組み合わせ(X:x)から、同じ外部 IP アドレスに対して送られる一連のパケットは、外部ポート番号によらず、NAT の際に同じポートマッピングを利用します。具体的には、Y2 と Y1 が同じ場合のみ、X1':x1' と X2':x2' が同じになります。)
Address-Dependent Mapping

Address and Port-Dependent Mapping

The NAT reuses the port mapping for subsequent packets sent from the same internal IP address and port (X:x) to the same external IP address and port while the mapping is still active. Specifically, X1':x1' equals X2':x2' if and only if, Y2:y2 equals Y1:y1.(私訳:上記にさらに宛先ポートも条件に入れた場合)
Address and Port-Dependent Mapping

というわけで、もともと疑問だった「Endpoint-independent Mapping の有効化/無効化」という機能は、Cloud NAT における外部アドレス:ポートの割当方式を、Endpoint-independent Mapping にするか、Endpoint-dependent Mapping (Address-Dependent Mapping か Address and Port-Dependent Mapping のいずれか)にするかの選択だということは分かりました。

マッピング方式と Cloud NAT の挙動の確認

ちなみにマッピング方式の変更方法はこちらのドキュメントに記載しています。

  • 同じ宛先の異なるポートへの接続

Endpoint Independent Mapping の場合は送信元アドレス:ポートが同じですが、Endpoint Dependent Mapping の場合は、送信元ポートがばらばらになっていることが分かります。

Endpoint Independent
Endpoint Dependent

なお、同じ宛先アドレスなのに、送信元ポートが異なっていることから Cloud NAT では Address and Port-Dependent Mapping を採用していることが分かります。

  • 異なる宛先の同じポートへの接続

こちらの挙動も同じです。

Endpoint Independent
Endpoint Dependent

なお、この結果を見ると Endpoint Dependent Mapping の方がたくさん送信元ポートを使っており、送信元ポートが足りなくなるんじゃないか、と思われるかもしれませんが、必ずしもそういうことはありません。たとえば、割り当てられたポート数以上の異なる宛先に同時にセッションを張った場合は以下のようになります。

ご覧のように、同じ送信元ポートを使いまわしているので、Endpoint Independent Mapping と同様に、これで送信元ポートが枯渇しやすいということはありません。

NAT 時のエラー

Cloud NAT 利用時のエラーに、送信時の失敗があります。(メトリクス nat/dropped_sent_packets_count)このエラーが発生した場合、理由も同時に記録され、OUT_OF_RESOURCES もしくは ENDPOINT_INDEPENDENCE_CONFLICT となります。

OUT_OF_RESOURCES

OUT_OF_RESOURCES はそのまま、リソース(送信元 IP アドレスや送信元ポート)が不足していることを意味しています。

たとえば、上の方で VM からわざと同一宛先へ 9 セッション同時に接続 した際にはこのエラーが発生していました。

router.googleapis.com/nat/dropped_sent_packets_count のグラフ (Cloud Monitoring)

ログでも allocation_status が “DROPPED” になっています。

Cloud Logging のログの一部

ENDPOINT_INDEPENDENCE_CONFLICT

一方、ENDPOINT_INDEPENDENCE_CONFLICT の方は少しややこしいです。このエラーが発生する仕組みや、具体例などはドキュメントにありますので、ここでは簡単に説明したいと思います。

このエラーはマッピング方式が Endpoint Independent Mapping になっている時にのみ発生します。

ENDPOINT_INDEPENDENCE_CONFLICT が発生する例

まず初めに、VM から外部ホスト X と Y へ、緑の線で表される既存のセッション ① があるとします。

  1. この状態で内部送信元ポート :10000 から Y へ接続しようとします ②
  2. Endpoint Independent Mapping のルールに則り、X へのセッションと同じ外部送信元ポート 1024 を使おうとします
  3. しかしポート 1024 は使用中のため ENDPOINT_INDEPENDENCE_CONFLICT が発生し、エラーとなります。(なお、外部送信元ポート自体はまだ余ってる(1029〜1031)ので、OUT_OF_RESOURCES エラーにはなりません)
router.googleapis.com/nat/dropped_sent_packets_count のグラフ (Cloud Monitoring)

ドキュメントにもありますが、このエラーを減らすには以下のような方法があります。

  • Endpoint Independent Mapping をやめる(Endpoint Dependent Mapping にする)これが一番簡単で、そもそもこの記事を書くきっかけとなった「Endpoint-independent Mapping の有効化/無効化という機能」が実装された背景でもあります。
  • VM 当たりの送信元ポート番号の割り当てを増やす。この記事ではわざと割当ポート数を 8 と小さい値にすることによって問題が発生しやすくしていますが、割当ポート数が大きければ衝突する可能性も低くなります。

Endpoint Independent Mapping の必要性

こうなってくると、そもそも Endpoint Independent Mapping はなぜあるのか?全部 Endpoint Dependent Mapping にしておけばいいのではないか?という疑問が出てきます。

そこで出てくるのが NAT トラバーサルSTUNTURN)です。これらの NAT トラバーサルの技術との互換性のためには、Endpoint Independent Mapping を採用している必要があるのです。(RFC 4787 にも「REQ-1: A NAT MUST have an “Endpoint-Independent Mapping” behavior.」と書かれています)

STUN サーバーを介した P2P 通信の開始

では、STUN を使った通信を思い出してみましょう。初めに、A と B はお互いに直接通信をしたい、NAT 配下のホストとします。

  1. A と B はそれぞれ送信元アドレス:ポートを A:n、B:m として STUN サーバーに接続し、STUN サーバーを経由して相手のアドレスとポート番号(A:n、B:m)を取得する ①
  2. A と B はお互いに向かう(A:n→B:m と B:m→A:n)通信を開始する ②

この時、NAT が Endpoint Independent Mapping をしていれば ① の時と ② の時の送信元アドレス:ポートが同じであることが期待できるため、A と B は通信をできます。

しかし、もし NAT が Endpoint Dependent Mapping をしていると、STUN サーバーへの通信と、A/B お互いへの通信の際の送信元アドレス:ポートは異なるため、A と B は直接通信ができません。

歴史的にゲームなどのために NAT の裏側にいるホスト同士で P2P 通信をおこないたいという要求はずっとあり、上に書いた RFC 4787 における要件もそのような状況を反映したものです。

しかし、Cloud NAT は Google Compute Engine (GCE)や Google Kubernetes Engine と(GKE)いった、どちらかというと(クライアントではなく)サーバー用途で使われるホストに対して NAT のサービスを提供するものであり、GCE/GKE のホスト同士が NAT トラバーサルを使った直接の通信をしなければならない場面は非常に限定的であることを考えると、Cloud NAT においては Endpoint Dependent Mapping を利用することによるデメリットはほぼ無いと考えられます。

この記事を書いていたら、ちょうどタイミングよく “6 best practices for effective Cloud NAT monitoring” という記事も公開されたので、こちらも合わせてご覧いただければと思います。

  1. Best practice 1: Plan ahead for Cloud NAT capacity
  2. Best practice 2: Monitor port utilization
  3. Best practice 3: Monitor the reasons behind Cloud NAT drops
  4. Best practice 4: Enable Cloud NAT logging and leverage log-based metrics
  5. Best practice 5: Monitor top endpoints and their drops
  6. Best practice 6: Baseline a normalized error rate

参考文献

--

--

Seiji Ariga
google-cloud-jp

Customer Engineer, Network Specialist, Google Cloud (All views and opinions are my own.)