AWS EC2インスタンスを L2TP/IPSec クライアントに苦労しながら設定した話

# はじめに

現在、VPNサーバーに Windows 10 ビルドイン の設定を使って接続しているが、これを AWS 上の Linux インスタンスから実施したかった。
正直、WindowsではGUIで簡単にできていたし簡単にできるだろうと高を括っていたら丸一日ぐらい調査と検証に時間をかけることになりましたので、メモします。

今回の結果はAWSの東京リージョン (ap-northeast-1) 上で検証したものになります。


## 使用したOS、ツールなど

- CentOS Linux release 7.6.1810 (Core) 
    - ami-045f38c93733dd48d (AWS Merketplace でサブスクリプション必要、無料)
    - 最初はAmazon Linux (ami-0064e711cbc7a825e) でやろうとしたけれど、当時は libreswan をインストールしても ipsec pluto がうまく入ってこなかったので今回はあきらめた
        - （追記） 少し手順を変えることで amzn2-ami-hvm-2.0.20191217.0-x86_64-gp2 (ami-011facbea5ec0363b) では VPN 接続できたのでこの点を追記
    - **Public Subnet に存在していて、EIPを eth0 に付与しているインスタンス**
- libreswan (libreswan-3.25-8.1.el7_7.x86_64)
- xl2tpd (xl2tpd-1.3.14-1.el7.x86_64)                                                                                                                              


## VPNサーバーについて

Windows 10 のVPN接続設定で以下の設定で接続できたVPNサーバー。

- VPNプロバイダー: Windows (ビルドイン)
- VPNの種類: 事前共有キーを使った L2TP/IPSec
- サインイン情報の種類: ユーザー名とパスワード

## 各種ツールのインストール

ローンチした直後の CentOS 7 で以下のコマンドを実行し、libreswan および xl2tpd をインストールします。 
初期ログインユーザーは centos ですが、以下の全てのコマンドは `sudo su - ` 後の root で実行しています。 sudo で実行する場合は適時読み替えてください。

```bash
yum -y update 
yum -y install epel-release      # xl2tpd に必要
yum -y install libreswan xl2tpd
yum -y install bind-utils        # dig が必要な場合
```

# ミドルウェアの設定

ここから各ミドルウェアの設定をしていきます。
ただ、いくつかの説明サイトではlibreswanとxl2tpdの設定がまとめて書かれていますが、実際には/で分離されている通り、IPSec と L2TP は別の目的があり、これらを組み合わせてVPN接続を実現しています。 
言い換えると（実用性は別として）IPSec, L2TP は個別に設定を確認できるということです。
そこで、本記事ではこれらを分けて設定していきます。

## IPSec の設定

### 1. `ipsec verify` 

まずは何も記載していない状態で `ipsec verify` を確認します。
ここでエラーとなっている部分を直していきます。

```console
$ systemctl restart ipsec
$ ipsec verify
Verifying installed system and configuration files

Version check and ipsec on-path                         [OK]
Libreswan 3.25 (netkey) on 3.10.0-957.1.3.el7.x86_64
Checking for IPsec support in kernel                    [OK]
 NETKEY: Testing XFRM related proc values
         ICMP default/send_redirects                    [NOT DISABLED]

  Disable /proc/sys/net/ipv4/conf/*/send_redirects or NETKEY will act on or cause sending of bogus ICMP redirects!

         ICMP default/accept_redirects                  [NOT DISABLED]

  Disable /proc/sys/net/ipv4/conf/*/accept_redirects or NETKEY will act on or cause sending of bogus ICMP redirects!

         XFRM larval drop                               [OK]
Pluto ipsec.conf syntax                                 [OK]
Two or more interfaces found, checking IP forwarding    [FAILED]
Checking rp_filter                                      [ENABLED]
 /proc/sys/net/ipv4/conf/all/rp_filter                  [ENABLED]
 /proc/sys/net/ipv4/conf/default/rp_filter              [ENABLED]
 /proc/sys/net/ipv4/conf/eth0/rp_filter                 [ENABLED]
 /proc/sys/net/ipv4/conf/ip_vti0/rp_filter              [ENABLED]
  rp_filter is not fully aware of IPsec and should be disabled
Checking that pluto is running                          [OK]
 Pluto listening for IKE on udp 500                     [OK]
 Pluto listening for IKE/NAT-T on udp 4500              [OK]
 Pluto ipsec.secret syntax                              [OK]
Checking 'ip' command                                   [OK]
Checking 'iptables' command                             [OK]
Checking 'prelink' command does not interfere with FIPS [OK]
Checking for obsolete ipsec.conf options                [OBSOLETE KEYWORD]
warning: could not open include filename: '/etc/ipsec.d/*.conf'
```

ほとんどのエラーは Linux のカーネルパラメータの設定によるものなので、`/etc/sysctl.conf` に値を設定し、反映します。

```ini:/etc/sysctl.conf
# Disable /proc/sys/net/ipv4/conf/*/send_redirects
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0
net.ipv4.conf.eth0.send_redirects=0
net.ipv4.conf.ip_vti0.send_redirects=0
net.ipv4.conf.lo.send_redirects=0 

# Disable /proc/sys/net/ipv4/conf/*/accept_redirects
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.eth0.accept_redirects=0
net.ipv4.conf.ip_vti0.accept_redirects=0
net.ipv4.conf.lo.accept_redirects=0

# Two or more interfaces found, checking IP forwarding    [FAILED]
net.ipv4.ip_forward=1

# Checking rp_filter
net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.default.rp_filter=0
net.ipv4.conf.eth0.rp_filter=0
net.ipv4.conf.ip_vti0.rp_filter=0
```

以下のように結果が全て `[OK]` となることを確認。

```bash:ipsec&nbsp;verifyの修正確認
$ vi /etc/sysctl.conf          # 上記の項目を追加
$ sysctl -p
$ touch /etc/ipsec.d/vpn.conf  # 最後の include するファイルが1つもない部分に対応
$ ipsec verify
Verifying installed system and configuration files

Version check and ipsec on-path                         [OK]
Libreswan 3.25 (netkey) on 3.10.0-957.1.3.el7.x86_64
Checking for IPsec support in kernel                    [OK]
 NETKEY: Testing XFRM related proc values
         ICMP default/send_redirects                    [OK]
         ICMP default/accept_redirects                  [OK]
         XFRM larval drop                               [OK]
Pluto ipsec.conf syntax                                 [OK]
Two or more interfaces found, checking IP forwarding    [OK]
Checking rp_filter                                      [OK]
Checking that pluto is running                          [OK]
 Pluto listening for IKE on udp 500                     [OK]
 Pluto listening for IKE/NAT-T on udp 4500              [OK]
 Pluto ipsec.secret syntax                              [OK]
Checking 'ip' command                                   [OK]
Checking 'iptables' command                             [OK]
Checking 'prelink' command does not interfere with FIPS [OK]
Checking for obsolete ipsec.conf options                [OK]
```

### 2. `/etc/ipsec.d/vpn.conf` の記述

先ほど touch コマンドで作成したところに書き込んでいきます。
ここでは `vpn.conf` としましたが、 `*.conf` にマッチする名称であれば何でも構いません。 ルート設定の `/etc/ipsec.conf` 内で `/etc/ipsec.d/*.conf` が include されるためです。

今回は　EIP: `AAA.BBB.CCC.DDD` から VPNサーバー `WWW.XXX.YYY.ZZZ` へと VPN 接続を行うこととします。 以下の2つのファイルを作成・変更してください。

```text:/etc/ipsec.d/vpn.conf
conn L2TP
     authby=secret
     pfs=no
     auto=add
     keyingtries=3
     dpddelay=30
     dpdtimeout=120
     dpdaction=clear
     keyexchange=ike
     phase2=esp
     encapsulation=yes
     rekey=yes
     ikelifetime=8h
     keylife=1h
     type=transport
     left=%defaultroute
     leftid=AAA.BBB.CCC.DDD
     right=WWW.XXX.YYY.ZZZ
     # rightid=SSS.TTT.UUU.VVV
```

```text:/etc/ipsec.d/vpn.secrets
AAA.BBB.CCC.DDD WWW.XXX.YYY.ZZZ : PSK "[事前共有キー]"
```

なお、この設定では `auto=add` なので ipsec デーモンを立ち上げたとしても、ユーザーが明示的にupしない限り ipsec 交換は行いません。 up するコマンドは `ipsec auto --up [connの後に書いたコネクションの名前]` です。

以上の設定が終了した場合、以下のコマンドを実行してください。
`ipsec auto --up L2TP` で色々とメッセージが出ますが、すぐに終わった場合は大体成功です。

```bash:IPSecによる通信の暗号化を開始
$ systemctl restart ipsec
$ ipsec auto --up L2TP
...(ここにたくさん出てくるが、エラーがない場合はすぐ終わる)...
...(エラーがある場合は下記の備考1を参照にして修正)...
$ ipsec status
...(中略)...
000 Total IPsec connections: loaded 1, active 1 # 設定は1つしか書いていないので active 1 となっていれば OK
...(中略)...
```

うまくいけば IPSec については終了です。 設定だけを行いたい場合は L2TP まで進んでください。

#### 備考1: 失敗する場合

私の試行中には `ipsec auto --up L2TP` 実行後に終了しない場合、画面に以下のようなエラーが出力されましたので、修正方法を記載しておきます。
修正後は `systemctl restart ipsec && ipsec auto --up L2TP` で再試行してください。

- ERR1: 事前共有キーがうまく読み取れていません
    - vpn.secretsの値と一致しているかを再確認してください

```text:ERR1
Can't authenticate: no preshared key found for `AAA.BBB.CCC.DDD' and `WWW.XXX.YYY.ZZZ'.  Attribute OAKLEY_AUTHENTICATION_METHOD
```

- ERR2: VPNサーバーIPの裏にある別のIPをIDに指定する必要があります
    - `/etc/ipsec.d/vpn.conf` の rightidの行の#を外す
    - `/etc/ipsec.d/vpn.secrets` の値を `WWW.XXX.YYY.ZZZ` を `SSS.TTT.UUU.VVV` に置き換える
 
```text:ERR2
we require IKEv1 peer to have ID 'WWW.XXX.YYY.ZZZ', but peer declares 'SSS.TTT.UUU.VVV'
```

#### 備考2: Amazon EC2 インスタンスの制約

設定にはさらっと書いていますが、EC2インスタンスから libreswan で IPSec を貼る場合、EC2のネットワークを見越した設定が必要になります。

具体的には以下の３つです。 より詳しくは[libreswanの公式Wiki(英語)](https://libreswan.org/wiki/Interoperability)から。

- left=%defaultroute
- leftid=AAA.BBB.CCC.DDD
- encapsulation=yes

### 追記: さくらの VPC ルータに接続した場合

**2023/12/27 追記** : さくらの VPC ルータに接続しようとすると、上記設定では NG だった。

https://manual.sakura.ad.jp/cloud/network/vpc-router/vpc-remoteaccess.html

以下のように設定した。

- 暗号化アルゴリズムが AES256, 3DES、ハッシュアルゴリズムは SHA (=SHA1)、DHグループが MODP1024 のため、`ike` パラメータをそれに準じたものに明示的に指定。

```
conn L2TP
    authby=secret
    pfs=no
    auto=add
    dpddelay=30
    dpdtimeout=120
    dpdaction=clear
    keyexchange=ike
    ike=3des-sha;modp1024,aes256-sha;modp1024
    phase2=esp
    ikelifetime=28800s
    keylife=1800s
    type=transport
    encapsulation=yes
    left=%defaultroute
    leftid=AAA.BBB.CCC.DDD
    leftprotoport=17/1701
    right=WWW.XXX.YYY.ZZZ
    rightprotoport=17/1701

```

## L2TP (xl2tpd) の設定

こちらが L2TP を行ってくれるミドルウェアです。
ここで使われる設定ファイル3つを以下の通り修正します。
なお、xl2tpd.conf の `[lns default]` セクションが初めから記載されていますが、ここは L2TP サーバーとしての機能の設定なので、クライアントとしてのみ使う今回の場合はコメントアウト or 消去してください。

```text:/etc/xl2tpd/xl2tpd.conf
[lac L2TP]
lns = WWW.XXX.YYY.ZZZ
ppp debug = yes
pppoptfile = /etc/ppp/options.xl2tpd.client
length bit = yes
autodial = yes
redial = yes
redial timeout = 10
max redials = 6
```

```text:/etc/ppp/options.xl2tpd.client
user [VPNの接続ユーザー名]
debug
noauth
mtu 1400
mru 1400
; ipv6cp-use-ipaddr
; +ipv6
```

```text:/etc/ppp/chap-secrets
# Secrets for authentication using CHAP
# client        server  secret                  IP addresses
"[VPNの接続ユーザー名]"    *       "[VPNの接続パスワード(※事前共有キーではない)]"      *
```

設定後、以下のコマンドを実行して IPSec + L2TP の同時実行が行われます。

```bash
$ systemctl stop xl2tpd && systemctl stop ipsec  # 1度止める
$ systemctl start ipsec && systemctl start xl2tpd && ipsec auto --up L2TP
...(IPSecの起動メッセージが表示)...
$ ifconfig
...(ppp0というI/Fが作られていれば成功)...
```

上記メッセージ中にもありますが、ppp0というインタフェースが表示されていれば成功です。
問題がない場合は以下の備考は飛ばして次に進んでください。

ppp0 が表示されない場合はどこかに問題があることになります。 今回の設定だと、xl2tpd および pppd のメッセージログは `/var/log/messages` に記入されますので、ここを見て問題解決をしていきます。 以下の備考も参考にしてください。

なお、ここで示している設定では debug モードを on にしているため、表示されるメッセージは多めになっています。 正常に接続でき、以後詳細メッセージが不要であればこの部分は取り除いてください。

#### 備考1: 検証中に出てきたエラー

うまく ppp0 が作成されない場合、 `/var/log/messages` の中身を確認してみてください。 以下に私が遭遇したエラーを紹介します。

- ERR1: Config File が間違えている場合
    - xl2tpd, pppd の起動時に設定が間違えている場合、以下のようなメッセージが表示されるので、マニュアルや `man` などを参考にして修正してください
    - ちなみに、以下は ipv6 を有効にしようとして失敗したケースです。 詳しくは備考3で。

```text:ERR1
xl2tpd: /usr/sbin/pppd: In file /etc/ppp/options.xl2tpd.client: too few parameters for option 'ipv6'
pppd[16075]: In file /etc/ppp/options.xl2tpd.client: too few parameters for option 'ipv6'
```

- ERR2: [lac VPN]のセクションに local ip として自分の Private IPを書いてしまった場合
    - 今までの設定を見ていると書きたくなった
    - ここにlocal ip を書く場合はVPN側ネットワークで割り振られる local ip を書く必要がある
    - local ip は書かずに、素直にVPNサーバーが割り振ってくれるものを使おう

```text:ERR2
Peer refused to agree to our IP address
```

#### 備考2: l2tp-control への書き込み

参考にした記事では以下のようにして ppp0 を有効にしているものもありましたが、今回検証した環境ではこれを実行しなくても ppp0 は有効になりました。

```bash:xl2tpdを有効にする
echo "c [xl2tpd.confで設定したlac名称]" > /var/run/xl2tpd/l2tp-control
```

もし `/var/log/messages` にエラーが表示されないにも関わらず、ppp0 が作られない場合は `echo "c VPN" > /var/run/xl2tpd/l2tp-control` を1度実行してみてください。

ちなみに、既に有効になっている場合に上記を再度実行すると、

```text
xl2tpd: xl2tpd[15058]: Session 'VPN' already active!
```

のように表示されます（特に影響はありません）。


### 備考3: `Unsupported protocol 'IPv6 Control Protocol' (0x8057) received`

接続に時間がかかった理由の１つが `/var/log/messages` の中に以下のようなメッセージが出ていたためでした。

```text:こんなメッセージを見たらVPNサーバーがIPv6のみしか対応していないように見える
pppd[18449]: CHAP authentication succeeded
pppd[18449]: Unsupported protocol 'IPv6 Control Protocol' (0x8057) received
pppd[18449]: Peer refused to agree to our IP address
```

結果的に、2行目と3行目は別物でした。 3行目は備考1:ERR2として紹介した方法で解消するものです。 
しかし、これが問題と思っていたので解消方法を色々と探しました。
結果として、上の `/etc/ppp/options.xl2tpd.client` でコメントアウトされている部分を復活させれば IPv6 Control Protocol にも対応可能です。

```text:/etc/ppp/options.xl2tpd.client
user [VPNの接続ユーザー名]
debug
noauth
mtu 1400
mru 1400
ipv6cp-use-ipaddr
+ipv6
```

最後の `+ipv6` ですが + は間違いではありませんので忘れないでください。
なお、このオプションは [Enable the IPv6CP and IPv6 protocolsと説明されている](https://manpages.debian.org/stretch/ppp/pppd.8.en.html) ページがあったり、[廃止されたオプションとして無効化されていると説明されている](https://docs.oracle.com/cd/E56342_01/html/E54076/pppd-1m.html) ところがあったり、そもそもマニュアルに載ってなかったりするのですが、今回の環境では利用可能でした。

これを記述するとIPv6のローカルIPが払い出されます。

```text:/var/log/messages
pppd[18920]: CHAP authentication succeeded
pppd[18920]: local  LL address XXXX::XXXX:XXXX:XXXX:XXXX
pppd[18920]: remote LL address YYYY::YYYY:YYYY:YYYY:YYYY
```

## route の設定

これで L2TP/IPSec の接続はできましたが、VPN先から通信をしようと思う場合、通信として ppp0 から出力されるようにしなくてはいけません。

その他の手順を見ると `0.0.0.0/0` を ppp0 に流せば全ての通信が VPN 経由になる、という説明があるところもありますが、**今回は絶対にこの方法を採るべきではありません** 。
なぜなら、**EC2インスタンスにはあなたのローカルPCから何らかの方法（おそらくSSH）で接続をしていますので、全ての通信を VPN の口に向けてしまうと、接続のレスポンスが一切返ってこなくなります** 。 
万が一やってしまった場合はAWS Management Consoleなどからインスタンスを再起動しましょう。 永続追加してしまった場合はやりようが難しいので、基本は一時的なものにするとよいでしょう。

基本、LinuxをL2TP/IPSecクライアントとして利用する場合、宛先が決まっていることがほとんどだと思いますので、宛先を指定するには以下のコマンドを使います。

```bash:routeの設定
# 特定IPに対してVPN先のIPでアクセスする場合
$ route add -net [接続先IPアドレス] netmask 255.255.255.255 dev ppp0
# 上記設定を削除する場合
$ route del -net [接続先IPアドレス] netmask 255.255.255.255 dev ppp0
```

### ネットワークの範囲指定例

http://ifconfig.me/ というサイトで接続元IPをチェックできるので、ここへの接続をVPN越しに実施することにします。

```bash
$ dig ifconfig.me

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-9.P2.el7 <<>> ifconfig.me
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46330
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ifconfig.me.                   IN      A

;; ANSWER SECTION:
ifconfig.me.            60      IN      A       216.239.38.21
ifconfig.me.            60      IN      A       216.239.32.21
ifconfig.me.            60      IN      A       216.239.34.21
ifconfig.me.            60      IN      A       216.239.36.21

;; Query time: 1 msec
;; SERVER: 172.31.0.2#53(172.31.0.2)
;; WHEN: Thu Nov 14 13:48:38 UTC 2019
;; MSG SIZE  rcvd: 104
```

これら全てを個別に `route add` しても良いのですが、以下のように `216.239.32.0/19` として設定することでも対処できます。

```bash
$ curl http://ifconfig.me
# (EC2インスタンスのEIP AAA.BBB.CCC.DDD が出力)
$ route add -net 216.239.32.0 netmask 255.255.224.0 dev ppp0
$ curl http://ifconfig.me
# (VPNのIPアドレス WWW.XXX.YYY.ZZZ が出力)
```

これでVPNのIPアドレスが出力されていれば、VPN接続先から正常に通信が行えています。
これは永続化は設定していないので、再起動をすればルートは消えてしまいますが、もし永続化が必要な場合 `range` や `ip` コマンドを別途調べて下さい。

なお、ルートテーブルのメトリクスが同じ場合、複数のルールにマッチした場合はレンジの小さい方 (/24よりも/32の方が小さい) が優先されます。
これも踏まえてルールを設定してください。

## VPNの切断

L2TP/IPSec のサービスを中断すれば止まります。

```bash:VPNの切断
$ systemctl stop xl2tpd && systemctl stop ipsec
```
あるいは、L2TPのコネクションのみを切りたいのであれば、

```bash
echo "d VPN" > /var/run/xl2tpd/l2tp-control
```

のようにすればコネクションを切断できます。


# 終わりに

調べてみたら予想よりも L2TP/IPSec クライアントとして使うことを説明する記事が少なかったです。 サーバーは多かったのですが。。。

あとはこの情報をもとにスクリプト化すれば、色々と自動化が捗ります。


# 参考資料

- https://qiita.com/ryouma_nagare/items/a57fd8241f6b39f71676
- http://network.station.ez-net.jp/client/remote/vpn/l2tp-linux.asp
- https://qiita.com/__cooper/items/4b5ab7ae40b0e95f0d19
- https://qiita.com/kure/items/ad5b91d62d23b9928997
- https://mo.kerosoft.com/0215
- https://libreswan.org/wiki/Interoperability


# 2020/01/30 追記: Amazon Linux 2 での接続

CentOS 8 の Free AMI がなかなか出てこないので、Amazon Linux 2 でもう一度検証した。
結果、少し変更は必要だったが、概ね上記の手順で実行できた。

- 検証AMI: amzn2-ami-hvm-2.0.20191217.0-x86_64-gp2 (ami-011facbea5ec0363b)

## 変更点

- epel のインストールは `amazon-linux-extras install -y epel` となる
    - xl2tpd のインストールは epel がないと No Available になるので順序に注意
- そのままだと xl2tpd が起動しないので対処が必要。

```bash:xl2tpd.serviceをコピー
cp /usr/lib/systemd/system/xl2tpd.service /etc/systemd/system/
```

```ini:/etc/systemd/system/xl2tpd.serviceを編集
[Unit]
Description=Level 2 Tunnel Protocol Daemon (L2TP)
Wants=network-online.target
After=network-online.target
After=ipsec.service
# Some ISPs in Russia use l2tp without IPsec, so don't insist anymore
#Wants=ipsec.service

[Service]
Type=simple
PIDFile=/var/run/xl2tpd/xl2tpd.pid
# ここをコメントアウト: ExecStartPre=/sbin/modprobe -q l2tp_ppp
ExecStart=/usr/sbin/xl2tpd -D
Restart=always

[Install]
WantedBy=multi-user.target
```

原因は ExecStartPre でロードしようとするモジュール l2tp_ppp が Amazon Linux 2 のカーネル内に含まれないこと。
推測になるが、 xl2tpd の Github の書き込みの中に、以下の通り Amazon Linux 2 (と思う、AWS の　AMI) では `l2tp_ppp` がサポートされておらず、カーネルの再コンパイルが必要らしい。

> No, I contacted aws and they said it's not supported on their kernels.
> They claimed it may be possible by compiling a custom kernel but I took a different route. So this can be closed.
> 引用元 - https://github.com/xelerance/xl2tpd/issues/150

幸い、これがなくても動くので、systemctl 用の service ファイルから ExecStartPre 部分を除いてやれば動くようになる。
同様の解決をしたという例を AWS Forum でも見つけた。

https://forums.aws.amazon.com/thread.jspa?threadID=309762

とりあえず、これで気軽にAmazon Linux 2でも使えるようになりました。