EC2から自宅経由でIPv4インターネット接続 (WireGuardトンネル編)

目次

はじめに

前回のブログにて、自宅と Amazon EC2 インスタンスとの間で単純な IPv4 over IPv6 トンネルを張り、EC2 から自宅経由で IPv4 インターネットと通信する方法について書きました。

しかし、記載した方法では送信元アドレスを詐称したパケットが自宅ルータに来るとセキュリティ上の問題があるため、本格的に利用するには改良が必要です。

今回は改良方法の1つとして、IPv6 で WireGuard トンネルを張り、そのうえで IPv4 インターネットと通信する方法について試してみました。


WireGuard とは

WireGuard は UDP ベースの VPN プロトコルの1つで、ハンドシェイクの部分が IPsec や TLS 系のプロトコルよりもかなりシンプルです。 また、Linux Kernel 5.6 以降ではカーネルに組み込まれているので手軽に利用でき、カーネルランドで動作するので余計なオーバーヘッドも少ないです。 詳細は 公式サイトホワイトペーパー を参照ください。


環境説明

ネットワーク構成

次の図のようなネットワーク構成で試しました。

ルータとその設定は 「NEC UNIVERGE IX2105 で OCN 光の IPoE 接続を行う」 に書いたような内容です。

目標

ルータ (UNIVERGE IX2105) では WireGuard を扱えないので、ルータの内側で動作している Raspberry PI と EC2 インスタンスの間で IPv6 で WireGuard トンネルを張り、そのうえで IPv4 通信を行えるようにします。

EC2 インスタンスから IPv4 インターネットに送信するパケットは「EC2 インスタンス ⇒ Raspberry PI ⇒ ルータ ⇒ インターネット」のように流れる想定で、それぞれ次のような動作を行わせます。

  • EC2 インスタンス : IPv4 パケットを WireGuard トンネルで Raspberry PI に転送する
  • Raspberry PI : WireGuard トンネルから出てきた IPv4 パケットをそのままルータに転送する
  • ルータ : IPv4 パケットの送信元アドレスとポートを NAPT してインターネットに転送する

また、インターネットからの戻りのパケットは逆に流れて、それぞれ次のような動作を行わせます。

  • ルータ : IPv4 パケットの宛先アドレスとポートを元に戻して Raspberry PI に転送する
  • Raspberry PI : ルータから来たパケットを WireGuard トンネルで EC2 インスタンスに転送する
  • EC2 インスタンス : WireGuard トンネルから出てきたパケットの受信処理を行う

NAT は最小限にしたいので、ルータで IPv4 の NAPT を行うだけにします。

AWS 側

検証用の EC2 インスタンスや AWS 側のネットワークは次のようにしました。 今回は慣れている Ubuntu 22.04 を利用しています。

  • VPC 関連
    • VPC アドレス : 172.31.0.0/16
    • サブネット : 172.31.0.0/20
    • Internet Gateway : あり
  • EC2 インスタンス関連
    • パブリック IPv4 アドレス : なし
    • プライベート IPv4 アドレス : 172.31.0.4
    • IPv6 アドレス : 2406:Y:Y:Y::4
    • OS : Ubuntu 22.04.3 LTS (kernel 6.2.0-1017-aws)

自宅側

常時起動している Raspberry PI があるのでそれを用いました。

  • Raspberry PI
    • プライベート IPv4 アドレス : 192.168.1.10
    • IPv6 アドレス : 2400:X:X:X::10
    • OS : Ubuntu 22.04.3 LTS (kernel 5.15.0-1044-raspi)

各種設定

ファイアウォールで通信を許可する

WireGuard トンネルを張るのに先立ち、自宅と EC2 の両方のファイアウォールでインバウンド方向の通信を許可しておきます。

WireGuard は IPsec と異なり任意の UDP ポートで利用できるので、今回はともに 30000 番ポートを利用することとしました。 また、KeepAlive を使えば片方だけ許可するに留めることも可能ですが、KeepAlive を使いたくない (=無駄なトラフィックを発生させたくない) ので双方向で許可します。

セキュリティグループの設定

EC2 インスタンスのセキュリティグループでは最小限のインバウンド許可を追加しました。

  • タイプ : カスタム UDP
  • プロトコル : UDP
  • ポート範囲 : 30000
  • ソース : 2400:X:X:X::10/128 (Raspberry PI のIPアドレス)

ルータの設定

ルータにも最小限のアクセスリストとフィルタのルールを追加しました。

1ipv6 access-list permit-wg-i permit udp src 2406:Y:Y:Y::4/128 sport eq 30000 dest 2400:X:X:X::10/128 dport eq 30000
2ipv6 access-list permit-wg-o permit udp src 2400:X:X:X::10/128 sport eq 30000 dest 2406:Y:Y:Y::4/128 dport eq 30000
3
4interface GigaEthernet0.0
5  ipv6 filter permit-wg-i 11 in
6  ipv6 filter permit-wg-o 11 out

WireGuard 接続設定

WireGuard で利用する秘密鍵の作成、公開鍵の作成、ネットワークインターフェースの設定、ピアとの接続設定を行っていきます。 wg コマンド (wireguard-tools パッケージ) を使えば簡単にできます。

秘密鍵・公開鍵の作成

まず、EC2 インスタンス、Raspberry PI のそれぞれで秘密鍵と公開鍵を作成します。

1$ wg genkey > wg0.key    # 秘密鍵の作成
2$ chmod 600 wg0.key
3$ wg pubkey < wg0.key > wg0.pub    # 公開鍵の作成

どこで作成しても良いですが、最終的には root ユーザだけが秘密鍵ファイルにアクセスできるようにすべきです。

ネットワークインターフェースの設定

次に、EC2 インスタンス、Raspberry PI のそれぞれでネットワークインターフェースを設定します。

1$ sudo ip link add dev wg0 type wireguard
2$ sudo wg set wg0 listen-port 30000 private-key wg0.key
3$ sudo ip link set dev wg0 up

この時点で UDP 30000 番ポートの Listen が開始されていますが、通信は何も発生していません。

ピアとの接続設定

そして、EC2 インスタンス、Raspberry PI のそれぞれでピアとの接続設定を行っていきます。 その際、ピアの公開鍵の文字列が必要となります。

EC2 インスタンス側

1$ sudo wg set wg0 peer ${Raspberry PI側の公開鍵} allowed-ips 0.0.0.0/0 endpoint [2400:X:X:X::10]:30000

Raspberry PI 側

1$ sudo wg set wg0 peer ${EC2インスタンス側の公開鍵} allowed-ips 172.31.0.4/32 endpoint [2406:Y:Y:Y::4]:30000

allowed-ips には自分自身からピアに転送できる宛先 IP アドレスの範囲を指定するのですが、EC2 インスタンスからはインターネット向きの通信を行うため、任意の IPv4 アドレス宛に転送できるようにしています。

KeepAlive (persistent-keepalive オプション) を指定していないため、まだ通信は発生しておらず、WireGuard のハンドシェイクも行われていません。

IPv4 ルーティング設定

IPv4 通信の流れに合わせてルーティング設定も行っていきます。

EC2 インスタンスのルーティング設定

インターネットへのデフォルト経路を WireGuard のネットワークインターフェースに向けつつ、内部で行うべき通信は既存のネットワークインターフェースに向けるようにします。

 1$ sudo ip route add default dev wg0
 2$ sudo ip route add 169.254.0.0/16 via 172.31.0.1 dev ens5
 3$ sudo ip route add 172.31.0.0/16 via 172.31.0.1 dev ens5
 4
 5$ ip route    # 設定後のルーティングテーブルの内容
 6default dev wg0 scope link
 7default via 172.31.0.1 dev ens5 proto dhcp src 172.31.0.4 metric 100
 8169.254.0.0/16 via 172.31.0.1 dev ens5
 9172.31.0.0/20 dev ens5 proto kernel scope link src 172.31.0.4 metric 100
10172.31.0.0/16 via 172.31.0.1 dev ens5
11172.31.0.1 dev ens5 proto dhcp scope link src 172.31.0.4 metric 100
12172.31.0.2 dev ens5 proto dhcp scope link src 172.31.0.4 metric 100

Raspberry PI のルーティング設定

デフォルト経路はルータに向いているのでそのままで良いですが、EC2 インスタンスへの経路を WireGuard のネットワークインターフェースに向ける必要があります。 また、IPv4 パケットを転送する必要があるのでカーネルパラメータで有効にしてあげます。

1$ sudo ip route add 172.31.0.4/32 dev wg0
2$ sudo sysctl net.ipv4.ip_forward=1

ルータのルーティング設定

NAPT してインターネットに転送する設定は既に行ってあるので新たな設定は不要でしたが、インターネットからの戻りのパケットを Raspberry PI に転送するための経路の追加は必要でした。

1$ ip route 172.31.0.4/32 192.168.1.10

正常系の動作確認

アプリケーションレベルの疎通確認

EC2 インスタンスからインターネットに対する通信は当然成功します。

※このとき、EC2 インスタンスの別ターミナルを起動して tcpdump でパケットキャプチャしておくと、この後の確認が捗ります。

 1$ ping 8.8.8.8 -c 4
 2PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
 364 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=27.2 ms
 464 bytes from 8.8.8.8: icmp_seq=2 ttl=116 time=11.3 ms
 564 bytes from 8.8.8.8: icmp_seq=3 ttl=116 time=11.6 ms
 664 bytes from 8.8.8.8: icmp_seq=4 ttl=116 time=11.5 ms
 7
 8--- 8.8.8.8 ping statistics ---
 94 packets transmitted, 4 received, 0% packet loss, time 3005ms
10rtt min/avg/max/mdev = 11.347/15.421/27.151/6.772 ms

初っ端は WireGuard のハンドシェイクが行われたため RTT が少し大きいですが、それ以降は 11~13ms の間で安定していました。

WireGuard のパケットの確認

前項で ping を実行したときの EC2 インスタンスでは次のパケットが流れていました。

※tcpdump では WireGuard のデータを解析してくれなかったので tshark を使っています。

 1$ tshark -r any.cap "udp.port==30000 or icmp"
 2   10   3.686502   172.31.0.4 → 8.8.8.8         ICMP 104 Echo (ping) request  id=0x0002, seq=1/256, ttl=64
 3   12   3.686846 2406:Y:Y:Y::4 → 2400:X:X:X::10 WireGuard 216 Handshake Initiation, sender=0x77483376
 4   14   3.701249 2400:X:X:X::10 → 2406:Y:Y:Y::4 WireGuard 160 Handshake Response, sender=0x066C94D4, receiver=0x77483376
 5   15   3.701605 2406:Y:Y:Y::4 → 2400:X:X:X::10 WireGuard 196 Transport Data, receiver=0x066C94D4, counter=0, datalen=96
 6   16   3.713575 2400:X:X:X::10 → 2406:Y:Y:Y::4 WireGuard 196 Transport Data, receiver=0x77483376, counter=0, datalen=96
 7   17   3.713635      8.8.8.8 → 172.31.0.4      ICMP 104 Echo (ping) reply    id=0x0002, seq=1/256, ttl=116 (request in 10)
 8   20   4.687874   172.31.0.4 → 8.8.8.8         ICMP 104 Echo (ping) request  id=0x0002, seq=2/512, ttl=64
 9   21   4.687919 2406:Y:Y:Y::4 → 2400:X:X:X::10 WireGuard 196 Transport Data, receiver=0x066C94D4, counter=1, datalen=96
10   22   4.699097 2400:X:X:X::10 → 2406:Y:Y:Y::4 WireGuard 196 Transport Data, receiver=0x77483376, counter=1, datalen=96
11   23   4.699198      8.8.8.8 → 172.31.0.4      ICMP 104 Echo (ping) reply    id=0x0002, seq=2/512, ttl=116 (request in 20)
12   26   5.689439   172.31.0.4 → 8.8.8.8         ICMP 104 Echo (ping) request  id=0x0002, seq=3/768, ttl=64
13   27   5.689535 2406:Y:Y:Y::4 → 2400:X:X:X::10 WireGuard 196 Transport Data, receiver=0x066C94D4, counter=2, datalen=96
14   28   5.701021 2400:X:X:X::10 → 2406:Y:Y:Y::4 WireGuard 196 Transport Data, receiver=0x77483376, counter=2, datalen=96
15   29   5.701065      8.8.8.8 → 172.31.0.4      ICMP 104 Echo (ping) reply    id=0x0002, seq=3/768, ttl=116 (request in 26)
16   32   6.691286   172.31.0.4 → 8.8.8.8         ICMP 104 Echo (ping) request  id=0x0002, seq=4/1024, ttl=64
17   33   6.691324 2406:Y:Y:Y::4 → 2400:X:X:X::10 WireGuard 196 Transport Data, receiver=0x066C94D4, counter=3, datalen=96
18   34   6.702727 2400:X:X:X::10 → 2406:Y:Y:Y::4 WireGuard 196 Transport Data, receiver=0x77483376, counter=3, datalen=96
19   35   6.702803      8.8.8.8 → 172.31.0.4      ICMP 104 Echo (ping) reply    id=0x0002, seq=4/1024, ttl=116 (request in 32)

1回目の ICMP echo request / reply のときに1往復だけ WireGuard のハンドシェイクメッセージが流れています。 ホワイトペーパー の 5.4.1 にフローの図が書かれていますが、事前に公開鍵をお互いに知っているとはいえ1往復で完結するというのはシンプルで良いですね。

tshark で -V オプションも付ければ WireGuard のパケットの中身も確認できますが、シンプルだなぁという印象を抱いた程度でした。


異常系の動作確認

Raspberry PI から EC2 インスタンスに対してトンネル経由で ping を打ちつつ、EC2 インスタンスの再起動を試してみました。 結果としては、EC2 インスタンス側で WireGuard を再設定すれば、トンネルは正常状態に復帰することが確認できました。

このとき、Raspberry PI 側の挙動としては、ICMP echo request を WireGuard の Transport Data パケットとして送り続けていたのですが、ピアからのパケットが来なくなってから約15秒後に Handshake Initiation メッセージを5秒間隔で送り始めました。 EC2 インスタンス側で WireGuard を再設定したら Raspberry PI 側に Handshake Response が返り、新たな counter で Transport Data が送受信されるという動作となっていました。

Handshake Initiation メッセージを送信する挙動については ホワイトペーパー の 6.5 の記載の通りでした。

他にもいくつかのケースを試しましたが、ネットワークインターフェースと WireGuard の設定を行えばトンネルは復帰するという結果でした。


OS起動時の自動設定

OS を再起動すると WireGuard の設定が消えてしまうので、起動時に自動設定されるようにしておきます。 Ubuntu だと netplan で実現できました。

EC2 インスタンス側

 1$ sudo vim /etc/netplan/99-wireguard.yaml
 2
 3$ cat /etc/netplan/99-wireguard.yaml
 4network:
 5  version: 2
 6  tunnels:
 7    wg0:
 8      mode: wireguard
 9      port: 30000
10      keys:
11        private: EC2インスタンス側の秘密鍵
12      peers:
13      - allowed-ips: [0.0.0.0/0]
14        endpoint: "[2400:X:X:X::10]:30000"
15        keys:
16          public: Raspberry PI側の公開鍵
17      routes:
18      - to: 0.0.0.0/0
19  ethernets:
20    ens5:
21      routes:
22      - to: 169.254.0.0/16
23        via: 172.31.0.1
24      - to: 172.31.0.0/16
25        via: 172.31.0.1
26
27$ sudo chmod 600 /etc/netplan/99-wireguard.yaml

Raspberry PI 側

 1$ sudo vim /etc/netplan/99-wireguard.yaml
 2
 3$ cat /etc/netplan/99-wireguard.yaml
 4network:
 5  version: 2
 6  tunnels:
 7    wg0:
 8      mode: wireguard
 9      port: 30000
10      keys:
11        private: Raspberry PI側の秘密鍵
12      peers:
13      - allowed-ips: [172.31.0.4/32]
14        endpoint: "[2406:Y:Y:Y::4]:30000"
15        keys:
16          public: EC2インスタンス側の公開鍵
17      routes:
18      - to: 172.31.0.4/32
19
20$ sudo chmod 600 /etc/netplan/99-wireguard.yaml

netplan の設定ファイルのオーナーは root とし、パーミッションを 600 または 400 にちゃんと変更しましょう。


その他メモ

今回の方法ではルータには単に静的経路を追加しただけですので、前回の方法と違って一般的な家庭用ルータでも実現できます。 Raspberry PI からルータに IPv4 を転送するパケットを転送する際に送信元アドレスを Rasberry PI のものに NAPT すれば、ルータに静的経路を追加する必要もなくなります。

また、EC2 インスタンスに対してインターネットから IPv4 でアクセスしたい場合、IPv4 インターネット接続の方式しだいですが、ルータで静的 NAPT を行えば可能です。


まとめ

WireGuard を用いて EC2 インスタンスから自宅経由で IPv4 インターネットにアクセスできるようにし、前回の記事 に記載したセキュリティ上の問題を解消しつつ、引き続き料金を節約できるようになりました。 自宅側に Linux マシンが必要となるためその分のランニングコストは必要となりますが、常時起動しているマシンがあるならコストは大きく増えることはないでしょう。

一方で、ルータと Linux マシンとの間でトラフィックが余分に流れていますし、Linux マシンの余分なパワーを使うことにもなります。 暗号化されたトンネルを張るといったことはルータで行いたいものですので、IPsec を用いる方式についても後日検証するつもりです。