CentOS 7 で SSH VPN

いつか役にたつかもしれないので CentOS 7 で SSH VPN をしてみます。

SSH VPN とは OpenSSH で作る簡易な VPN です。ググると情報は色々ありますが、iproute2 や NetworkManager での設定方法はあまり見つかりませんでした。

## 構成

- hostB と hostC と hostX が同じネットワークに属しています
- hostA からはグローバルの 9876 ポートを指定して hostX に ssh できます
- hostA から hostX を経由して hostB に ssh できます
- hostA から hostB に ssh で VPN を張って hostA から hostC に透過的にアクセスします

例えば次のような IP アドレスだとします。

```
hostA 192.168.8.123 ※VPN の端点
hostB 192.168.0.100 ※VPN の端点
hostC 192.168.0.200 ※目的のサーバ
hostX 203.0.113.111 ※hostA から hostB に ssh するための中継機のグローバルアドレス
```

hostA の .ssh/config で次のように記述しているので、hostA から hostB へは `ssh hostB` だけで接続できます。

```text:.ssh/config
Host hostB
  HostName 192.168.0.100
  ProxyCommand ssh 203.0.113.111 -l ore -p 9876 -W %h:%p
```

## L3(point-to-point)

L3(point-to-point) で VPN を構成します。
この方法は hostA と hostB に NIC を追加してクロスケーブルで接続し、hostB にルータのように振る舞わせるイメージです。

まず、hostA と hostB の `/etc/ssh/sshd_config` に次を追記します。

```text:/etc/ssh/sshd_config
PermitTunnel point-to-point
```

設定を反映します。

```console:hostA/hostB
# systemctl reload sshd
```

hostA から hostB に `-w0:0` を付けて接続します。

```console:hostA
# ssh -N -f -w0:0 root@hostB
```

hostA と hostB でそれぞれ tun0 というネットワークデバイスができているはずです。

```console:hostA/hostB
# ip link show dev tun0
3: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 500
    link/none
```

hostA で tun0 に IP アドレスを設定します。このとき peer で hostB の tun0 に設定する IP アドレスも指定します。

```console:hostA
# ip link set tun0 up
# ip addr add 192.168.254.2 peer 192.168.254.1 dev tun0
```

さらに、hostA に hostB が属するネットワークへのルーティングを追加します。

```console:hostA
# ip route add 192.168.0.0/24 via 192.168.254.1 dev tun0
```

hostB で tun0 に IP アドレスを設定します。peer も指定するのは hostA と同様です。

```console:hostB
# ip link set tun0 up
# ip addr add 192.168.254.1 peer 192.168.254.2 dev tun0
```

この時点で hostB に付与した IP アドレスに hostA から ping が通ります。

```console:hostA
# ping 192.168.254.1
PING 192.168.254.1 (192.168.254.1) 56(84) bytes of data.
64 bytes from 192.168.254.1: icmp_seq=1 ttl=64 time=20.7 ms
64 bytes from 192.168.254.1: icmp_seq=2 ttl=64 time=26.2 ms
```

しかし、まだ hostA から hostC へは接続できません。

hostC に hostA へのルーティングを設定すれば良いのですが、めんどうなので hostB で IP マスカレードします。

firewall-cmd のデフォルトだと NIC のゾーンを external にすればマスカレードが有効になります（ens32 は hostB が元々持っていた NIC です）。

```console:hostB
# firewall-cmd --zone=external --change-interface=ens32
```

もしくは、hostB の NIC が元々属していたゾーンにマスカレードを追加しても大丈夫です。

```console:hostB
# firewall-cmd --zone=public --add-masquerade
```

IP マスカレードをする場合（というか hostB をルータのように使う場合）sysctl で IP フォワーディングを有効にする必要がありますが、firewall で IP マスカレードを設定すると IP フォワーディングも自動的に有効になるようです。

これで hostA から hostC に VPN 経由でアクセスできるようになります。

```console:hostA
# ping 192.168.0.200
PING 192.168.0.200 (192.168.0.200) 56(84) bytes of data.
64 bytes from 192.168.0.200: icmp_seq=1 ttl=63 time=16.9 ms
64 bytes from 192.168.0.200: icmp_seq=2 ttl=63 time=17.8 ms
```

traceroute すれば VPN 経由であることがわかります。

```console:hostA
# traceroute 192.168.0.200
traceroute to 192.168.0.200 (192.168.0.200), 30 hops max, 60 byte packets
 1  192.168.254.1 (192.168.254.1)  25.189 ms  43.994 ms  44.008 ms
 2  192.168.0.200 (192.168.0.200)  44.008 ms  44.006 ms  44.003 ms
```

いらなくなったら hostA で ssh を殺します。

```console:hostA
# pkill -x ssh
```

tun0 に設定した IP アドレスやルーティングは tun0 デバイスが消えると一緒に消えます。

hostB の firewall は自動的には元に戻らないので、戻すのであれば restart で戻します。

```console:hostB
# systemctl restart firewalld
```

## L2(ethernet)

次は L2(ethernet) で VPN を構成します。
この方法は hostB を L2SW のように振る舞わせるイメージです。

なお、hostB が仮想環境の場合は仮想化のホスト側で hostB に割り当てられている仮想 NIC のプロミスキャスモードを有効にしておく必要があります（これに気づかずに 1 時間ぐらい無駄にしました・・・）。

プロミスキャスモードを有効にする方法は仮想化の製品によって異なるので詳細は割愛しますが、

 - ESXi ならホストの構成のネットワークの仮想スイッチのプロパティのセキュリティで「無差別モード」を「承諾」にします
 - VirtualBox ならゲストのネットワークの設定で「プロミスキャスモード」を「すべて許可」にします

プロミスキャスモードが設定できたら、hostA と hostB の `/etc/ssh/sshd_config` に次を追記します。

```text:/etc/ssh/sshd_config
PermitTunnel ethernet
```

設定を反映します。

```console:hostA/hostB
# systemctl reload sshd
```

hostB でブリッジを作っておきます。

失敗すると hostB に繋げられなくなるので、この作業はあらかじめ安全に作業できる状況でやっておくことをオススメします。

```console:hostB
# nmcli con add type bridge ifname br0 stp no ;\
> nmcli con modify bridge-br0 ipv4.method auto ;\
> nmcli con del ens32 ;\
> nmcli con add type bridge-slave ifname ens32 master br0
```

hostA から hostB に `-w0:0` と `-o Tunnel=ethernet` を付けて接続します。

```console:hostA
# ssh -N -f -w0:0 -o Tunnel=ethernet hostB
```

hostA と hostB でそれぞれ tap0 というネットワークデバイスができているはずです。

```console:hostA/hostB
# ip link show dev tap0
7: tap0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 500
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
```

hostB で tap0 をブリッジに追加します。

```console:hostB
# ip link set tap0 master br0
# ip link set tap0 up
```

hostA で tap0 をアクティブにします。

```console:hostA
# ip link set tap0 up
```

この時点で hostA から hostC に arping が通るはずです。

```console:hostA
# arping -I tap0 192.168.0.200
ARPING 192.168.0.200 from 192.168.8.123 tap0
Unicast reply from 192.168.0.200 [xx:xx:xx:xx:xx:xx]  16.882ms
Unicast reply from 192.168.0.200 [xx:xx:xx:xx:xx:xx]  13.632ms
```

hostA で tap0 に **hostB が属するネットワーク** の IP アドレスを設定します。

```console:hostA
# ip addr add tap0 192.168.0.101/24 dev tap0
```

もしくは hostA の NetworkManager で tap0 を dhcp にします。NetworkManager を restart しているのは `nmcli con` に tap0 を表示させるためです。なお、この dhcp に応えるのは **hostB が属するネットワーク上の dhcp サーバ** です。

```console:hostA
# systemctl restart NetworkManager
# nmcli con modify tap0 ipv4.method auto
# nmcli con up tap0
```

hostA から hostC に ping が通るようになります。

```console:hostA
# ping 192.168.0.200
PING 192.168.0.200 (192.168.0.200) 56(84) bytes of data.
64 bytes from 192.168.0.200: icmp_seq=1 ttl=64 time=16.1 ms
64 bytes from 192.168.0.200: icmp_seq=2 ttl=64 time=15.8 ms
```

この VPN はイーサネットフレームをカプセル化して通しているのでブロードキャストも通ります。

```console:hostA
# ping -I tap0 -b 255.255.255.255
WARNING: pinging broadcast address
PING 255.255.255.255 (255.255.255.255) from 192.168.0.101 tap0: 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=37.3 ms
64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=89.7 ms (DUP!)
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=89.7 ms (DUP!)
```

いらなくなったら hostA で ssh を殺します。

```console:hostA
# pkill -x ssh
```

hostA で tap0 を NetworkManager で dhcp にしているとゴミが残るのでこれも削除します。 

```console:hostA
# nmcli con del tap0
```

## さいごに

ssh 一本繋がるだけで VPN ができるのでお手軽です。が、普通は `-L` のポートフォワードや `-D` の SOCKS プロキシの方が簡単ですね。

また、SSH VPN は TCP Over TCP な VPN になります。あまり好ましくないので常用すべきではありません。

 - [Why TCP Over TCP Is A Bad Idea](http://sites.inka.de/~W1011/devel/tcp-tcp.html "Why TCP Over TCP Is A Bad Idea")
 - [なぜTCP Over TCPは悪いアイデアか](http://shugo.net/docs/tcp-tcp.html "なぜTCP Over TCPは悪いアイデアか")