概要
VPCエンドポイントのトンネルエンドポイントがリリースされました。
これまでのリソースエンドポイントは、Resource Configurationをリソース単位で作成する方式でした。トンネルエンドポイントではResource ConfigurationにCIDR範囲を指定できるので、10.0.0.0/16と指定すれば、その範囲にあるリソースへまとめて到達できます。
その代わり、DNS名に接続するだけで済む従来の使い方ではなく、接続元が自分でパケットをGENEVEでカプセル化して、トンネルエンドポイントに投げる必要があります。
全体構成は下記のとおりです。コンソールからの設定は次の順番になります。
- Resource Gatewayを作成(接続先)
- Resource Configuration(CIDRタイプ)を作成(接続先)
- Resource Configuration(CIDRタイプ)をRAMで共有(接続先)
- RAMのリソース共有を承認(接続元)
- VPCエンドポイント(トンネルタイプ)を作成(接続元)
パケットが流れる仕組みは下記のとおりです。今回は接続元EC2から接続先EC2の22番へTCP接続して疎通確認を行います(①)。カーネルのルーティング判断によってトンネルデバイス(tun0)へ書き込まれ(②③)、リレーが8バイトのGENEVEヘッダを付与して(④)、ens5からVPCエンドポイントへ送出されます(⑤)。
ここからは簡単に接続元EC2での動きを見ていきます。大きくは下記の流れです。
-
tun0というL3のトンネルデバイスを作る - 接続先CIDR宛のパケットを
tun0に向けるルートを追加する -
tun0に出てきたパケットに8バイトのGENEVEヘッダを付けてトンネルエンドポイントのENIへUDP 6081で送り、返ってきたら8バイトを外してtun0に戻す
実際の設定
AWS側のリソース(Resource Configuration / Resource Gateway / RAM共有 / Tunnelエンドポイント)は作成済みの状態から始めます。
環境
| 役割 | 値 |
|---|---|
| 接続元VPC | 10.1.0.0/16 |
| 接続元EC2 | 10.1.139.57 |
| TunnelエンドポイントのENI | 10.1.132.45 |
| 接続先VPC | 10.0.0.0/16 |
| 接続先EC2 | 10.0.130.80 |
① リレーを配置する
cat > /tmp/relay.py <<'EOF'
import fcntl, os, select, socket, struct, sys
t = os.open("/dev/net/tun", os.O_RDWR)
fcntl.ioctl(t, 0x400454CA, struct.pack("16sH", b"tun0", 0x0001 | 0x1000))
os.system("ip link set tun0 mtu 1464 up")
h = struct.pack("!BBHI", 0, 0, 0x0800, 0)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 6081))
tep = (sys.argv[1], 6081)
while True:
r, _, _ = select.select([t, s], [], [])
if t in r:
s.sendto(h + os.read(t, 65535), tep)
if s in r:
d, _ = s.recvfrom(65535)
if len(d) > 8:
os.write(t, d[8:])
EOF
このスクリプトがやっていることは、「tun0を作ること」と「その両側をつなぐこと」の2つです。
まず/dev/net/tunを開きます。これはOSに最初から存在する特殊なファイルで、開いてioctlで名前とモードを渡すと、その名前のトンネルデバイスが作られます。ここではtun0という名前と、IPパケットがそのまま流れるL3モードを指定しています。作成後、ip link set tun0 mtu 1464 upで起動します。
tun0から読んだパケットには先頭に8バイトのGENEVEヘッダを付けて、トンネルエンドポイントへUDP 6081で送ります。UDP 6081で返ってきたパケットは先頭8バイトを捨ててtun0に書き戻します。
② リレーを起動する
nohup python3 /tmp/relay.py 10.1.132.45 > /tmp/relay.log 2>&1 &
上記スクリプトをバックグラウンドで起動し、出力は/tmp/relay.logに書きます。引数の10.1.132.45はTunnel VPCエンドポイントのENIのプライベートIPで、コード内のtep = (sys.argv[1], 6081)の送信先として渡されます。
ip link show tun0
4: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1464 qdisc fq_codel state UNKNOWN mode DEFAULT group default qlen 500
link/none
tun0が起動していることを確認します。tun0はこのプロセスが動いている間だけ存在します。
③ ルートを追加する
ip route add 10.0.0.0/16 dev tun0 src 10.1.139.57
接続先VPCのCIDR宛だけをtun0に向けます。またtun0にIPアドレスが付いていないため、srcで送信元IPを明示します。
④ 疎通確認
timeout 5 cat < /dev/tcp/10.0.130.80/22
SSH-2.0-OpenSSH_9.9
接続先EC2の22番にTCP接続し、sshdのバナーが表示されることを確認します。
まとめ
CIDR単位で共有できるのは、接続先のリソースが増えていく構成ではかなり効いてくると思っています。ただし現時点では、AWSコンソールからの設定に加えて、接続元でGENEVEのカプセル化を自分で用意する必要があります。この部分を運用できるかどうかが採用の分かれ目になりそうです。

