AWS Gateway Load Balancer endpoint(GWLBe)を通じてGeneveプロキシで(ICMP Echoだけ)通す(のが精一杯だった)

# 概要

AWSのGateway Load Balancerのエンドポイントサービスを使えば隔離されたネットワークを作れるのでは？と思って、ルーティングをしようといろいろ試みましたが、私の知識ではICMP Echo(ping)を通すだけが精一杯だったということをまとめます。
技術的な検証なので、役に立つレベルまでは到達できませんでしたが、Geneveに関する記事はあまりないのでまとめておきます。

# 結論

まずGWLBで使われるGeneveプロトコルに関するドキュメントや解説記事などがうまく見つけらません…
試行錯誤してPythonによりプロキシ的なものを作り、無理矢理ICMP Echoを通すことができました。ICMPと同じような感じで、UDPは通すことができる気がします(未確認)が、TCPはいろいろと大変そう。
実際のpingはこんな感じ。

```shell-session
[ec2-user@ip-172-18-16-6 ~]$ ping 192.168.100.12
PING 192.168.100.12 (192.168.100.12) 56(84) bytes of data.
64 bytes from 192.168.100.12: icmp_seq=1 ttl=253 time=1.21 ms
64 bytes from 192.168.100.12: icmp_seq=2 ttl=253 time=0.798 ms
64 bytes from 192.168.100.12: icmp_seq=3 ttl=253 time=0.859 ms
64 bytes from 192.168.100.12: icmp_seq=4 ttl=253 time=0.685 ms
64 bytes from 192.168.100.12: icmp_seq=5 ttl=253 time=0.729 ms
64 bytes from 192.168.100.12: icmp_seq=6 ttl=253 time=0.763 ms
^C
--- 192.168.100.12 ping statistics ---
6 packets transmitted, 6 received, 0% packet loss, time 5079ms
rtt min/avg/max/mdev = 0.685/0.841/1.216/0.179 ms
```

Geneveドライバなどをうまく組み合わせれば簡単にルーティングできるのだろうか…？(仮想ネットワークに詳しくないので全然わからない)

# 構成

検証をしただけなので、図に示したように1-AZの簡単な構成です。
ゲートウェイエンドポイントサービス側(192.168.100.0/24)とサービスを使う側(172.18.16.0/24)は別々のAWSアカウントで作成しました。

![AWS-GWLBe.png.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111095/02fdd009-7f9c-ebd7-2788-3f9128566e6a.jpeg)

具体的な作成方法はここでは示しませんのでドキュメントなどを参照してください。主な手順は、EC2インスタンスを作成し、それに対してターゲットグループを設定、Gateway Load Balancerに割り当てて、サービスエンドポイントを作るだけです。今回のように異なるアカウントで作った場合はサービスエンドポイントのプリンシパルの許可を設定するのを忘れないようにします。

EC2インスタンスは全てAmazon Linux 2(Arm)のt4g.microをスポットインスタンスで作成しました。

## サービス側

- Gateway Load Balancer(GWLB)の設定
  - ターゲットグループ: インスタンスA(192.168.100.11)
- ICMP Echo到達先のインスタンスB(192.168.100.12)
- AからBにpingを通すようにしておく(共にディフォルトSGにすれば通信可能)
- SSHの疎通などは適宜

## 利用側

- 作ったサービスエンドポイントのURLをエンドポイント登録する
- ルートテーブルの192.168.100.0/24をGWLBeに向ける
- ping送信元となるインスタンスC(172.18.16.6)を作成する

# Geneveプロキシ

大げさなものではなく、非常に単純な力業です。

- GWLBから届くパケットをパースして、inner packetの送信元を自身のアドレスにして送る
- ICMP Echoなので、返ってきたICMP ReplyをGeneveパケットに組み直してGWLBに送る

それだけです。
以下、ソースです。ご自由にお使いください。

```python:geneve_echo_proxy.py
#!/usr/bin/env python3
# ICMP Echo proxy for AWS Gateway Load Balancer endpoint(GWLBe)
# 2022-06-30 nobrin
import sys
import socketserver
from socket import *

class GeneveHandler(socketserver.BaseRequestHandler):
    GENEVE_PORT = 6081

    def handle(self):
        # Handler for GENEVE
        data = self.request[0]

        gnv_optlen = (data[0] & 0b00111111) * 4
        gnv_header_len = 8 + gnv_optlen
        gnv_header = data[:gnv_header_len]

        ip_packet = data[gnv_header_len:]
        ip_header = ip_packet[:20]
        ip_body = ip_packet[20:]
        src = ip_header[-8:-4]
        dst = ip_header[-4:]

        # This can handle only ICMP Type 8(Echo request)
        if ip_header[9] != 1:
            sys.stderr.write(f"Packet must be ICMP(0x01), not 0x{ip_header[9]:02x}\n")
            return

        if ip_body[0] != 8:
            sys.stderr.write(f"ICMP Type must be Echo Request(0x08), not 0x{ip_body[0]:02x}\n")
            return

        # Rewrite source address to MY_IP
        MY_IP = self.server.server_address[0]
        if dst == inet_aton(MY_IP):
            sys.stderr.write("Can't send to server address.\n")
            return
        req_packet = ip_header[:-8] + inet_aton(MY_IP) + dst + ip_body

        # Send packet to inner destination
        with socket(AF_INET, SOCK_RAW, IPPROTO_RAW) as sock:
            sock.sendto(req_packet, (inet_ntoa(dst), 0))

        # Receive packet from the destination
        with socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) as sock:
            sock.setsockopt(SOL_IP, IP_HDRINCL, 1)
            resp, addr = sock.recvfrom(1500)

        # Respond GENEVE packet over UDP to GWLB
        # NOTE: Packet with header from the `resp` will not working...(why?)
        #resp_packet = gnv_header + resp[:12] + dst + src + resp[20:]
        resp_packet = gnv_header + ip_header[:12] + dst + src + resp[20:]
        with socket(AF_INET, SOCK_DGRAM) as sock:
            sock.sendto(resp_packet, (self.client_address[0], self.GENEVE_PORT))

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: sys.argv[0] SERVER_ADDR")
        sys.exit(1)
    HOST, PORT = sys.argv[1], 6081
    with socketserver.UDPServer((HOST, PORT), GeneveHandler) as server:
        sys.stderr.write(f"GENEVE proxy server started(ICMP Echo only) on {HOST} ...\n")
        server.serve_forever()
```

## 使い方

今回の構成ですとインスタンスA(192.168.100.11)でroot権限で実行します。
```
Usage: geneve_echo_proxy.py <自身のIPアドレス(GWLBのターゲットIPアドレス)>
```

```shell-session
[ec2-user@ip-192-168-100-11 ~]$ sudo ./geneve_echo_proxy.py 192.168.100.11
GENEVE proxy server started(ICMP Echo only) on 192.168.100.11 ...
```

サーバーを起動したらインスタンスC(172.18.16.6)からpingを送ってみます。
うまく構成できてたら届きます。192.168.100.1(ゲートウェイ)にも届きますが、届かないこともあります(よくわからない)。サーバー自身からうまく返ってきません(少し工夫がいるのかも)。

```shell-session
[ec2-user@ip-172-18-16-6 ~]$ ping 192.168.100.12
PING 192.168.100.12 (192.168.100.12) 56(84) bytes of data.
64 bytes from 192.168.100.12: icmp_seq=1 ttl=253 time=1.25 ms
64 bytes from 192.168.100.12: icmp_seq=2 ttl=253 time=0.733 ms
64 bytes from 192.168.100.12: icmp_seq=3 ttl=253 time=0.695 ms
64 bytes from 192.168.100.12: icmp_seq=4 ttl=253 time=0.775 ms
^C
--- 192.168.100.12 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3032ms
rtt min/avg/max/mdev = 0.695/0.865/1.258/0.229 ms
```

socketserverでポート6081で待ち受けて、パケットが届いたら上に書いたような処理をしています。

# 参考サイト

Geneve(ジュネーブ)という名前がよくないです(--;。とにかく検索しにくい…

## Geneveプロトコル

- [Geneve tunnels using netlink examples](https://gist.github.com/nerdalert/5276aeaf04cb730b21be1929ab6905a4)
    - 非常に基本的なトンネルの作り方。この通りに作れば簡単に接続できます。これで動作したパケットをキャプチャして眺めて考えてました。
- [How-to Setup Geneve tunnels with Linux bridge](https://medium.com/@veronica2831986/how-to-setup-geneve-tunnels-with-linux-bridge-f4b7c8216115)
    - ブリッジがうまく使えないかなーと。
- [RFC8926 Geneve: Generic Network Virtualization Encapsulation](https://datatracker.ietf.org/doc/html/rfc8926)
    - パケットの構造の確認
- [GeneveProxy - an AWS Gateway Load Balancer reference application](https://www.sentiatechblog.com/geneveproxy-an-aws-gateway-load-balancer-reference-application)
    - Proxyとなってますが、このスクリプトのmain.pyでは転送はしてくれません。概念をつかむにはいいかも(ちょこっとしか読んでないけど)
- [Setting up GENEVE tunnel in Linux with TC](https://darjchen.medium.com/setting-up-geneve-tunnel-with-linux-tc-571f891618a9)
    - Amazon Linuxでは`tc`が使えなかったのであまり深掘りしてない

## ソケットプログラミング

- [Python icmp socket server (not tcp\udp)](https://stackoverflow.com/questions/8245344/python-icmp-socket-server-not-tcp-udp)
- [linux/include/uapi/linux/in.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/in.h)