MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

strongSwanとFRRでオンプレとGCPをVPN接続

はじめに

こんにちは。マイクロアドでインフラエンジニアをしているハダです。
3回目のブログ投稿です。

今回のブログは、オンプレの環境とGCPの接続にVPNを使用し、
オンプレ側で使用するVPNゲートウェイをstrongSwanFRRで構築した内容です。

www.strongswan.org frrouting.org
一般的に、オンプレ側ではVPN対応RouterやVyOSなどのネットワークOSを使ってVPN接続することが多いです。
今回OSはUbuntu20.04を使いstrongSwanとFRRでGCPとVPN接続しました。
GCP側はHA VPN Gatewayを使うため、動的ルーティング(BGP)が必須となっています。1
そのため、オンプレ側ではFRRを使いBGP接続できるようにします。

なお、この記事の中ではGCP側のVPN設定の方法及びオンプレ側でのNAT設定等については書いていません。

オンプレとクラウドの接続方法

本題の前に、オンプレとクラウドを接続する方法を振り返っておきます。
クラウド事業者によりそれぞれのサービス名は異なりますが、主に以下2つの方法があります。
それぞれの接続方法については、メリット・デメリットありますがこの記事では省略します。

  1. 専用線による接続:オンプレ環境のあるデータセンターからクラウド事業者のアクセスポイントまで専用線を引いて接続する方法
  2. 拠点間VPNによる接続:オンプレ環境にVPNゲートウェイを準備し、VPNで接続する方法

専用線による接続

主なパブリッククラウドの接続サービス名称は、以下のようなものです。

  • GCP Dedicated Interconnect
  • AWS Direct Connect
  • Azure Express Route
    などです。

こちらのサービスは、クラウド事業者と直接回線を引いて接続する方法と
クラウド接続サービスを提供するプロバイダーを利用し、接続する方法があります。
どちらの場合も、オンプレ側に接続用の回線を準備する(引き込む)必要があります。

拠点間VPNによる接続

VPN接続サービスのサービス名称は、以下のようなものです。

  • GCP Cloud VPN
  • AWS VPN
  • Azure VPN Gateway
    などです。

今回はこちらの方法で接続し、その時オンプレ側で構築したVPNゲートウェイの構築方法についてです。

構成

今回のVPNゲートウェイは、Ubuntu20.04.4を使って構築しています。
VPN接続に必要なソフトウェアはstrongSwanです。

strongSwanとは

Linuxで使えるオープンソースのIPSecベースのVPNソフトウェアです。
IKEv1とv2に対応しています。
メジャーなLinuxディストリビューションには、バイナリパッケージが提供されています。

strongSwan公式サイト

Ubuntuにインストールする場合は、標準リポジトリでパッケージが提供されていますので、 aptでインストールが可能です。

sudo apt install strongswan

接続イメージ

接続イメージ図

左側がオンプレ側、右側がクラウド側です。
後述するipsecの設定でleft,rightという項目が出てきますが、
この図の通りleftがオンプレ(設定している自分)側・rightがクラウド(相手)側として設定しています。

strongSwanがGCP側のHA VPN GatewayとIPsecでVPN Tunnelを構成します。
このTunnelを使って、オンプレ側のFRRとGCP側のCloud RouterがBGPセッションを張ってPeeringします。

設定方法

IPsecの設定

strongSwanのIPSec設定します。

ipsec.confの設定

設定ファイルは、/etc/ipsec.confです(使用するディストリビューションによりPathが異なります)

以下、ipsec.confの設定例です。
各パラメータについては、環境により適切に変更してください。

ipsec.confの例を見る

# ipsec.conf - strongSwan IPsec configuration file

# basic configuration
config setup
        # strictcrlpolicy=yes
        # uniqueids = no

# Add connections here.
## default
conn %default
        ikelifetime=60m
        keylife=20m
        rekeymargin=3m
        keyingtries=1
        authby=secret
        keyexchange=ikev2
        mobike=no
        type=tunnel
        leftsubnet=0.0.0.0/0,::/0
        rightsubnet=0.0.0.0/0,::/0

## GCP-VPN
conn gcp-vpn-tunnel01
        esp=aes128-sha1-modp1024,3des-sha1-modp1024
        ikelifetime=600m
        keylife=180m
        rekeymargin=1m
        keyingtries=5
        keyexchange=ikev2
        left=[自HostIP]
        leftid=[自Site側のPublicIP]
        leftupdown=/etc/ipsec-vti.sh
        right=[対向のIPアドレス]
        auto=start
        mark=100

conn gcp-vpn-tunnel02
        esp=aes128-sha1-modp1024,3des-sha1-modp1024
        ikelifetime=600m
        keylife=180m
        rekeymargin=1m
        keyingtries=5
        keyexchange=ikev2
        left=[自HostIP]
        leftid=[自Site側のPublicIP]
        leftupdown=/etc/ipsec-vti.sh
        right=[対向のIPアドレス]
        auto=start
        mark=200

ipsec.secretsの作成

/etc/ipsec.secretsファイルを作成します。
このファイルには、自サイト側のPublic IP、対向側のPublic IP、事前共有キーを1行に記載するだけです。

ipsec.secretsの書き方を見る

[自サイト側public ip] [対向側IP1] : PSK "hogehogehogehogehogehogehogehoge"
[自サイト側public ip] [対向側IP2] : PSK "hehehehehehehehehehehehehehehehe"

トンネルインタフェース作成用のスクリプト

IPSecコネクションが張れた場合にトンネルインタフェースを作成するスクリプトを設置します。
前述のipsec.conf内で、leftupdownのパラメータで指定している /etc/ipsec-vti.sh というファイル名で、 スクリプト設置します。(ファイル名は任意です)
作成したファイルには、実行権限を付与しておきます。

ipsec-vti.shのスクリプト例を見る

#!/bin/bash

#
# /etc/ipsec-vti.sh
#

IP=$(which ip)
IPTABLES=$(which iptables)

PLUTO_MARK_OUT_ARR=(${PLUTO_MARK_OUT//// })
PLUTO_MARK_IN_ARR=(${PLUTO_MARK_IN//// })

case "$PLUTO_CONNECTION" in
    gcp-vpn-tunnel01)         # ipsec.conf内でconnに設定しているコネクション名を設定
        VTI_INTERFACE=vti01
        VTI_LOCALADDR=169.254.0.2/30
        VTI_REMOTEADDR=169.254.0.1/30
        ;;
    gcp-vpn-tunnel02)         # ipsec.conf内でconnに設定しているコネクション名を設定
        VTI_INTERFACE=vti02
        VTI_LOCALADDR=169.254.0.6/30
        VTI_REMOTEADDR=169.254.0.5/30
        ;;
esac

# output parameters to /var/log/messages for debug
    logger "ipsec-vti.sh: ================================================="
    logger "ipsec-vti.sh: PLUTO_CONNECTION = ${PLUTO_CONNECTION}"
    logger "ipsec-vti.sh: PLUTO_VERB = ${PLUTO_VERB}"
    logger "ipsec-vti.sh: VTI_INTERFACE = ${VTI_INTERFACE}"
    logger "ipsec-vti.sh: PLUTO_ME = ${PLUTO_ME}"
    logger "ipsec-vti.sh: PLUTO_PEER = ${PLUTO_PEER}"
    logger "ipsec-vti.sh: PLUTO_MARK_IN_ARR[0] = ${PLUTO_MARK_IN_ARR[0]}"
    logger "ipsec-vti.sh: PLUTO_MARK_OUT_ARR[0] = ${PLUTO_MARK_OUT_ARR[0]}"
    logger "ipsec-vti.sh: PLUTO_MARK_IN = ${PLUTO_MARK_IN}"
    logger "ipsec-vti.sh: ================================================="


case "${PLUTO_VERB}" in
    up-client)
        $IP link add ${VTI_INTERFACE} type vti local ${PLUTO_ME} remote ${PLUTO_PEER} okey ${PLUTO_MARK_OUT_ARR[0]} ikey ${PLUTO_MARK_IN_ARR[0]}
        sysctl -w net.ipv4.conf.${VTI_INTERFACE}.disable_policy=1
        sysctl -w net.ipv4.conf.${VTI_INTERFACE}.rp_filter=2 || sysctl -w net.ipv4.conf.${VTI_INTERFACE}.rp_filter=0
        $IP addr add ${VTI_LOCALADDR} remote ${VTI_REMOTEADDR} dev ${VTI_INTERFACE}
        $IP link set ${VTI_INTERFACE} up mtu 1436
        $IPTABLES -t mangle -I FORWARD -o ${VTI_INTERFACE} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
        $IPTABLES -t mangle -I INPUT -p esp -s ${PLUTO_PEER} -d ${PLUTO_ME} -j MARK --set-xmark ${PLUTO_MARK_IN}
        $IP route flush table 220
        ;;
    down-client)
        $IP link del ${VTI_INTERFACE}
        $IPTABLES -t mangle -D FORWARD -o ${VTI_INTERFACE} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
        $IPTABLES -t mangle -D INPUT -p esp -s ${PLUTO_PEER} -d ${PLUTO_ME} -j MARK --set-xmark ${PLUTO_MARK_IN}
        ;;
esac

# Enable IPv4 forwarding
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.eth0.disable_xfrm=1
sysctl -w net.ipv4.conf.eth0.disable_policy=1

スクリプト中の VTI_LOCALADDRVTI_REMOTEADDR については169.254.0.0/16から/30で切り出したものを設定します。
ここで設定しているIPアドレスが、トンネルの両端に設定されるアドレスです。
後述のBGPの設定内で、このIPアドレスを使ってBGPのpeer設定します。

補足:AWSでは、この範囲のうち以下については予約されていて設定できません。

169.254.0.0/30
169.254.1.0/30
169.254.2.0/30
169.254.3.0/30
169.254.4.0/30
169.254.5.0/30
169.254.169.252/30

ipsec設定の反映

ここまでの設定ファイルの作成で、ipsecによるトンネルが作成できる状態になっています。
設定反映を反映し、トンネルが作成できるか確認します。

systemctl restart strongswan-starter.service  

もしくは、以下のようにipsecコマンドを使います。

ipsec stop
ipsec start

トンネル状態の確認

トンネルの状態を確認するには、ipsec statusallで確認できます。
Connections及びSecurity Associationsで設定したトンネルが作成されていればOKです。

ubuntu@vpngateway01:~$ sudo ipsec statusall
Status of IKE charon daemon (strongSwan 5.8.2, Linux 5.4.0-107-generic, x86_64):
  uptime: 25 days, since Apr 21 10:54:49 2022
  malloc: sbrk 3690496, mmap 0, used 1899216, free 1791280
  worker threads: 11 of 16 idle, 5/0/0/0 working, job queue: 0/0/0/0, scheduled: 8
  loaded plugins: charon aesni aes rc2 sha2 sha1 md5 mgf1 random nonce x509 revocation constraints pubkey pkcs1 pkcs7 pkcs8 pkcs12 pgp dnskey sshkey pem openssl fips-prf gmp agent xcbc hmac gcm drbg attr kernel-netlink resolve socket-default connmark stroke updown eap-mschapv2 xauth-generic counters
Listening IP addresses:
  <自HostIPアドレス>
  169.254.0.2
  169.254.0.6
Connections:
gcp-vpn-tunnel01:  <自HostIPアドレス>...<対向PublicIP1>  IKEv2
gcp-vpn-tunnel01:   local:  [<自SitePublicIP>] uses pre-shared key authentication
gcp-vpn-tunnel01:   remote: [<対向PublicIP1>] uses pre-shared key authentication
gcp-vpn-tunnel01:   child:  0.0.0.0/0 ::/0 === 0.0.0.0/0 ::/0 TUNNEL
gcp-vpn-tunnel02:  <自HostIPアドレス>...<対向PublicIP2>  IKEv2
gcp-vpn-tunnel02:   local:  [<自SitePublicIP>] uses pre-shared key authentication
gcp-vpn-tunnel02:   remote: [<対向PublicIP2>] uses pre-shared key authentication
gcp-vpn-tunnel02:   child:  0.0.0.0/0 ::/0 === 0.0.0.0/0 ::/0 TUNNEL
Security Associations (2 up, 0 connecting):
gcp-vpn-tunnel02[308]: ESTABLISHED 21 seconds ago, <自HostIPアドレス>[<自SitePublicIP>]...<対向PublicIP2>[<対向PublicIP2>]
gcp-vpn-tunnel02[308]: IKEv2 SPIs: b5c9ef394a38844e_i* 06cf833b057ad6c8_r, pre-shared key reauthentication in 9 hours
gcp-vpn-tunnel02[308]: IKE proposal: AES_GCM_16_128/PRF_AES128_XCBC/MODP_2048
gcp-vpn-tunnel02{823}:  INSTALLED, TUNNEL, reqid 119, ESP in UDP SPIs: c83c018d_i e93650b4_o
gcp-vpn-tunnel02{823}:  AES_CBC_128/HMAC_SHA1_96, 1180 bytes_i (17 pkts, 1s ago), 1490 bytes_o (21 pkts, 1s ago), rekeying in 2 hours
gcp-vpn-tunnel02{823}:   0.0.0.0/0 === 0.0.0.0/0
gcp-vpn-tunnel01[307]: ESTABLISHED 4 hours ago, <自HostIPアドレス>[<自SitePublicIP>]...<対向PublicIP1>[<対向PublicIP1>]
gcp-vpn-tunnel01[307]: IKEv2 SPIs: 0bc3c2c69af5a567_i* 66b5d8b2940de6a2_r, pre-shared key reauthentication in 5 hours
gcp-vpn-tunnel01[307]: IKE proposal: AES_GCM_16_128/PRF_AES128_XCBC/MODP_2048
gcp-vpn-tunnel01{822}:  INSTALLED, TUNNEL, reqid 118, ESP in UDP SPIs: c848ec62_i 13a9586e_o
gcp-vpn-tunnel01{822}:  AES_CBC_128/HMAC_SHA1_96/MODP_1024, 261905 bytes_i (4258 pkts, 0s ago), 260137 bytes_o (4224 pkts, 0s ago), rekeying in 71 minutes
gcp-vpn-tunnel01{822}:   0.0.0.0/0 === 0.0.0.0/0

トンネルインタフェースの確認

ipコマンドを使って、トンネルインタフェースが作成されていることを確認します。
vti01とvti02のインタフェースが作成されていれば問題ありません。

ubuntu@vpngateway01:~$ ip a
〜〜〜省略〜〜〜
9: ip_vti0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
135: vti01@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1436 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip <自HostIPアドレス> peer <対向PublicIP1>
    inet 169.254.0.2 peer 169.254.0.1/30 scope global vti01
       valid_lft forever preferred_lft forever
    inet6 fe80::5efe:ac1e:272f/64 scope link 
       valid_lft forever preferred_lft forever
136: vti02@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1436 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip <自HostIPアドレス> peer <対向PublicIP2>
    inet 169.254.0.6 peer 169.254.0.5/30 scope global vti02
       valid_lft forever preferred_lft forever
    inet6 fe80::5efe:ac1e:272f/64 scope link 
       valid_lft forever preferred_lft forever

通信状況も確認します
PacketsやBytesが増えていてエラーが増え続けていなければ問題ありません。

ubuntu@vpngateway01:~$ ip -s tunnel show
ip_vti0: ip/ip remote any local any ttl inherit nopmtudisc key 0
RX: Packets    Bytes        Errors CsumErrs OutOfSeq Mcasts
    0          0            0      0        0        0       
TX: Packets    Bytes        Errors DeadLoop NoRoute  NoBufs
    0          0            0      0        0        0     
vti01: ip/ip remote <対向PublicIP1> local <自HostIPアドレス> ttl inherit nopmtudisc key 100
RX: Packets    Bytes        Errors CsumErrs OutOfSeq Mcasts
    11719      720872       0      0        0        0       
TX: Packets    Bytes        Errors DeadLoop NoRoute  NoBufs
    11648      717244       0      0        0        0     
vti02: ip/ip remote <対向PublicIP2> local <自HostIPアドレス> ttl inherit nopmtudisc key 200
RX: Packets    Bytes        Errors CsumErrs OutOfSeq Mcasts
    1351       83221        0      0        0        0       
TX: Packets    Bytes        Errors DeadLoop NoRoute  NoBufs
    1313       81347        0      0        0        0     

FRRの設定

FRRを使ってGCPのCloud RouterとBGP接続します。
マイクロアドではオンプレの環境はIP Closネットワークで構築しており、各サーバにFRRがインストールされBGPでToRと接続しています。
そのため、今回のVPNゲートウェイもすでにFRRがインストールされているため
BGP接続に関してはFRRにNeighborを追加し、必要な経路を広報するように修正して対応しました。

Neighborの設定

vtyshを使ってFRRの設定を変更します。

ubuntu@vpngateway01:~$ sudo vtysh

vpngateway01# conf t
vpngateway01(config)# router bgp [自AS番号]
vpngateway01(config-router)# neighbor 169.254.0.1 remote-as [GCPに設定したAS番号]
vpngateway01(config-router)# neighbor 169.254.0.5 remote-as [GCPに設定したAS番号]
vpngateway01(config-router)# address-family ipv4 unicast
vpngateway01(config-router-af)# neighbor 169.254.0.1 soft-reconfiguration inbound
vpngateway01(config-router-af)# neighbor 169.254.0.5 soft-reconfiguration inbound

これだけで、Cloud RouterとBGP接続できます。
ただし、この状態だとオンプレ側にある経路をそのままCloud Routerに広報してしまうため、 必要な経路かつAggregateしてCloud Router側に広報するように設定します。

ip prefix-listとroute-mapを追加して、経路フィルターができるように追加します。
ここでは、192.168.[0|1|2].0/24のPREFIXを広報するようにします。

vpngateway01# conf t
vpngateway01(config)# ip prefix-list SERVER_RANGE seq 10 permit 192.168.0.0/24
vpngateway01(config)# ip prefix-list SERVER_RANGE seq 11 permit 192.168.1.0/24
vpngateway01(config)# ip prefix-list SERVER_RANGE seq 12 permit 192.168.2.0/24
vpngateway01(config)# route-map AGGREGATE_ROUTES permit 10
vpngateway01(config-route-map)# match ip address prefix-list SERVER_RANGE

このあと、作成したrote-mapを今回接続しているNeighborに対してout方向でフィルタリングし、 Aggregateする設定も追加します。

vpngateway01# conf t
vpngateway01(config)# router bgp [自AS番号]
vpngateway01(config-router)# address-family ipv4 unicast
vpngateway01(config-router-af)# aggregate-address 192.168.0.0/24
vpngateway01(config-router-af)# aggregate-address 192.168.1.0/24
vpngateway01(config-router-af)# aggregate-address 192.168.2.0/24
vpngateway01(config-router-af)# neighbor 169.254.0.1 route-map AGGREGATE_ROUTES out
vpngateway01(config-router-af)# neighbor 169.254.0.5 route-map AGGREGATE_ROUTES out

ここまで設定すると、Cloud Router側に広報する経路が192.168.0.0/24〜192.168.2.0/24の3経路のみになります。

vpngateway01# show ip bgp summary 
〜〜〜省略〜〜〜
Neighbor                 V                  AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt
169.254.0.1              4 [GCPに設定したAS番号]    748692    752463        0    0    0 07:45:32            1        3
169.254.0.5              4 [GCPに設定したAS番号]    706792    709111        0    0    0 03:26:22            1        3
〜〜〜省略〜〜〜

設定が完了したら、write memoryで設定ファイルに書きが出して保存しておきます。

vpngateway01# write memory 
Note: this version of vtysh never writes vtysh.conf
Building Configuration...
Integrated configuration saved to /etc/frr/frr.conf
[OK]

まとめ

このあとGCP側にVMを作成して通信状況を確認することでVPNでの接続設定は完了です。

個人的には過去にVyattaと言われていた頃のVyOSでVPN接続したことはありましたが、
strongSwanでの接続もポイントさえ押さえれば意外と簡単に設定できました。

ただ、strongSwanではトンネルインタフェースを作るところまでしてくれないので、
スクリプトを別途用意して対応する必要があるなど注意点があることも事実です。

AWSでも同じ方法で接続が可能です。
ただしAWSで設定するカスタマーゲートウェイに設定できるAS番号が
4byteのプライベートAS番号が含まれていない点に注意が必要です。 2

参考にしたサイト

今回の構築にあたって、以下のサイトを参考にさせていただきました。

AWS Site-to-Site VPN with IPSec VPN (StrongSwan) and BGP (FRRouting)