🚞

【AWS】アドレスが重複するVPC間でホスト同士を通信させる方法を考えてみた&ツールを作ってみた

2021/08/29に公開

※この記事書いた2日後に以下のアップデートがあったw
以下の機能を使って普通のトンネルプロトコルでもVPCのサブネット単位ならアドレス重複していても一部接続はできる気がする(未検証)。
https://aws.amazon.com/jp/blogs/aws/inspect-subnet-to-subnet-traffic-with-amazon-vpc-more-specific-routing/


アドレスが重複するAmazon VPC間でホスト同士を通信させる方法を考えてみました&ツールを作ってみました。かなりのオレオレツールになった感。。
※作ってみたツールは多分に実験的です。

https://github.com/kuredev/vpc_conn

背景

VPCは以下のような仕様があり、アドレスが重複するVPC同士やVPCとオンプレとの接続が大変難しいです。

  • VPCのCIDR宛のパケットはローカルルートとして最優先にVPC内で処理される
  • ブロードキャストをサポートしていない

既存のツールで実現しようとすると、各VPCで双方向NATを設定し、片方のホストからは通信先のホストのIPアドレスを、偽装したIPアドレスのものを認識することで、通信を実現する方法があります(実際、知り合いが構築しているところも見たことがある)。

https://dev.classmethod.jp/articles/vpn-cidr/

http://www.rtpro.yamaha.co.jp/RT/docs/nat-descriptor/twice-nat.html

似たような方法でNLBを利用した方法も紹介されていました。

https://dev.classmethod.jp/articles/aws-nw-architectures-net320/#toc-23

また、例えばVPNサーバを用意して各ホストがVPNサーバにVPN接続し、VPNの仮想IPアドレスを用いて互いに通信する、という方法もあると思います。
同じアドレス帯という意味ではこれが一番きれいな方法かもしれません。
試してないですが、SoftEtherの仮想ハブ等では出来ると思います。

https://ja.softether.org/4-docs/1-manual/3/3.4

今回は、これらの方法とは別に、VPCのアドレス帯をそのまま用いて、オンプレミスの時にL2VPNで接続した時のように同じサブネットにあたかもいるかのように別VPCのホストと通信出来ないか考えてみました。

考えてみた

VPCの仕様

VPC内の通信はMapping Serviceという仕組みでENIのIPアドレス/MACアドレスの紐付けが管理され、管理外のIPアドレスのパケットはドロップされると考えて良さそうです。

https://codezine.jp/article/detail/9790

「送信元先チェック」を無効にすると自分のIPアドレス宛ではない通信のパケットの受信も可能となります。これはインターネットゲートウェイの代わりのEC2インスタンスベースのゲートウェイのようなものを配置する時に主に使われますが、VPC外部の宛先のパケットをそのゲートウェイに処理させるような用途となります。今回はVPCのアドレス宛のパケットを送出するため、パケットはローカルルートにすべて取られてしまいますため、ルートテーブルで別のホストに無理やり届けるといったことは出来なそうです。

アイデア

やはり、VPCのアドレス帯が宛先となっている通信は、IPアドレス/MACアドレスとも実際にVPC内部に存在するホスト(ENI)に向けたものである必要がありそうです。
そこで、別VPCの宛先のホストと同じIPアドレスを持つホストを送信元のVPCに配置し、その宛先パケットを受け取ったホスト(ルータ的なもの。実際には「ルーティング」はしない)が、別VPCにパケットをトンネルで届ければいいのではと考えてみました。VPCは別なので、同じIPアドレスを付与することは出来ます。
ただし、この方法だとカプセル化されたパケットのMACアドレスが送信元VPCのホストのものとなり、宛先のVPCに届いた後ではそのMACアドレスはMapping Serviceの管理外のものと判定され正常に処理されないため、パケットがトンネルの終端に届き、VPCに流す時にMACアドレスを書き換える必要があります。

上記のアイデアでツールを作ってみた

上記の方法なら実現できるのではないかと実験的にツールを作ってみたら、一応動きました。イメージとしては↓こういうことが出来ます。

仕組みの概要

  • 宛先ホストと同じIPアドレスをトンネル用のルータに持たせます
  • トンネル用のルータはそのインタフェースでパケットを受信したらEtherIPトンネルで対向のトンネル用のルータにパケットを届けます。この時に宛先ホストのMACアドレスを解決して、書き換えておきます(EtherIPなのでトンネルの通信は暗号化はされません)。
  • 対向のルータはパケットを受け取ったら送信元MACアドレスを自分のインタフェースのものに書き換えて宛先ホストに届けます

実装のメモ

実装はRubyで行いました。

イメージとしてはブリッジでVPC同士を接続しているように通信できればと思っていますが、現在のところは以下の制約があります(作り込めば多分解消可能)。

  • 発信側となれるのはどちらかのVPCのホストのみ(レスポンスは帰るので通信は可能)
  • ARPキャッシュは無い
  • シングルスレッドでの処理

細かいシーケンスは以下の通りです。
外から受け取ったパケットを基本的にはVPCの中に流す時に、IPアドレスやMACアドレスをVPC内で中継するデバイス(図中のトンネルルータ)のものに都度書き換えることで、外から来たパケットの内容を中継できます。
IPアドレスの書き換えも発生するタイミングでは、IPヘッダのチェックサムを計算し直さないとVPC内でドロップされるようなので、その処理も必要です。

ツール単体での処理のフローは以下の通りです。

使い方とか

AWSの準備

  • 同じアドレス帯のVPCを2つ用意します
  • 4つのEC2インスタンスを用意します
    • 2つは疎通確認用
    • 2つはトンネルルータ用
      • ENIを3つ用意します
        • 自VPCとの通信用
        • トンネル用 -> グローバルIP付与
        • マネジメント用 -> グローバルIP付与
    • 受信側のEC2インスタンスのIPアドレスと同じIPアドレスをトンネルルータの「自VPCとの通信用」のENIのセカンダリIPに付与します

ツール

$ git clone git@github.com:kuredev/vpc_conn.git
  # 送信側のトンネルルータ
$ sudo ruby vpc_conn.rb -o eth1 -t eth2 -d [受信側のトンネルルータのトンネル用のIPアドレス] -s [自分のIF(自VPCとの通信用)のIPアドレス] -r [受信側の疎通確認用インスタンスのIPアドレスと同じIPアドレス]
  # 受信側のトンネルルータ
$ sudo ruby vpc_conn.rb -o eth1 -t eth2 -d [送信側のトンネルルータのトンネル用のIPアドレス] -s [自分のIF(自VPCとの通信用)のIPアドレス]

あとは、送信用の端末から受信用の端末のIPに向けてPingすれば相手先まで届いて返信されるはずです(確認したのはICMPだけですが、TCP/UDPでも多分動く、、はず)。

感想

軽い気持ちで遊びで作り始めてみたらどんどんオレオレツール沼にハマってしまいましたが、一応最低動くところまでは試せたので良かったかなと。
ネットワークプログラミングの勉強にもなりました。

L2VPNのイメージでEtherIPプロトコルを使いましたが、どちらにせよ元のMACアドレスは書き換えるので、IPIPとかでも良かった気もする 🤔

参考文献

https://amzn.to/3DpkNF6
→ネットワークプログラミング全般的に参考にさせてもらってます。

https://amzn.to/2WvQ0pq
→今回のようなオレオレツールをイチから作っていく過程やその楽しさが書かれていて面白いです。

Discussion