この記事は ConoHa Advent Calendar 2023 の11日目の記事です。10日目はAyutsukiさんでした。12日目はめんどい 太郎さんです。
本記事では,ConoHa VPS 上に Docker で OpenVPN サーバを構築する手順を解説します。
前提
- ConoHa VPS 上に Linux サーバが構築されており,SSHでログインできるようになっている
- Docker がインストールされており,
sudo
なしで Docker が実行できるようになっている
この準備がまだの方は,次の記事でその手順を詳しく解説していますので,必要に応じてこちらを設定してから,続きをお読みください。(下記記事では Ubuntu でサーバ構築していますが,他の Linux でも大差ありませんので大丈夫です。)
VPNサーバを構築する目的
旅が好きな自分は,海外を含む空港やホテルが提供する公衆WiFiを使ってネット接続する場面が多々あります。それにあたり,ConoHa VPS上にVPNサーバを構築し,踏み台サーバとして用いると便利です。その目的は大きく4つあります。
目的①. 公衆WiFi接続時のセキュリティ確保
公衆WiFiのセキュリティに不安を持つ人も多いでしょう。そこで,ConoHa上のVPNサーバを踏み台として,全通信を暗号化されたVPNトンネルを通して行うことで,公衆WiFiを通す通信を暗号化し,セキュリティを確保できます。
目的②. 国内からしかアクセスできないサイトに海外から接続する
動画配信サイトや金融機関のサイトなどで,日本国外からのアクセスを,IPアドレスで判定して遮断しているサイトがあります。そこで,国内にある ConoHa VPS を踏み台としてアクセスすることで,サイト側からは日本国内からのアクセスと判定させて,海外からでも日本国内にいるときのようなアクセスを確保できるようになります。
なお,これを目的として,有料で提供されている既存の大手VPNサービスを使う手もありますが,サイトによっては,有名な大手VPN提供業者のIPアドレスがNGリストに登録されており,そのVPN業者からのアクセスを遮断しているケースもあります。自分でVPNサーバを構築しておけば,そのようなIPアドレス規制にも引っかからず確実です。
目的③. ファイアウォールの回避
空港やホテルのファイアウォール設定によっては,HTTP(S)やDNSなどの必要最低限のプロトコル以外のアウトバウンド接続を規制していたり,あるいは帯域制限等の目的で動画配信サイトへのアクセスが遮断されていたりすることがあります。VPS上に構成されたVPNサーバを踏み台とし,全ての通信をそのVPNトンネルを経由して接続することで,そのような制限をかいくぐることができます。特に,HTTPS通信と同じ 443/tcp でVPNサーバを待ち受けさせ,そこに向けてTLSで暗号化したVPN通信を通せば,通常のウェブブラウザによるHTTPS通信と区別を付けることは困難なので,ブロックされる可能性は極めて小さいと言えるでしょう。
目的④. 自宅LAN内へのアクセス確保
自宅LAN内の端末と出先の端末を,ともに ConoHa VPS 上のVPNサーバに接続しておきます。すると,その仮想ネットワークにおいては,自宅の端末と出先の端末が「同一LAN内」に存在することとなります。よって,SSH, SMB, VNCをはじめ,自宅LAN内の端末に自由に接続できるようになります。さらに,その自宅端末を踏み台にすることで,自宅LAN内の他の端末への接続も可能となるわけです。
この方法で自宅LANへの接続を確保する長所は次の通りです。
- 自宅のルータのポートフォワーディング設定などを変えなくてよい。
- インバウンド接続が全て拒否されるようなNAT内に対しても外からの接続を確保できる。
- ケーブルTVネットワークなどで,そもそも契約者にグローバルIPアドレスが与えられず,外部ネットワークから unreachable な自宅LAN環境であっても,外部からの接続が可能となる。
VPNサーバの種類の選択
PPTP
古いVPNプロトコルの一つで,設定が比較的簡単ですが,セキュリティが弱いので,現在では非推奨となっています。
L2TP/IPsec
定番のVPNプロトコルで,各OSでも標準でクライアント機能が用意されているので便利です。ただしポート番号が固定(UDP 500/1701/4500,IP 50 (ESP))であるため,公衆WiFiのファイアウォールでアウトバウンドポートが制限されている場合は使用不可となってしまいます。
OpenVPN
TLSを利用したオープンソースで提供されるVPNソリューションです。デフォルトでは 1194/udp を利用しますが,ポート番号を変更したり,UDPではなくTCPを使用することも可能です。各OSにはデフォルトでクライアント機能は用意されていないので,クライアントには自前で OpenVPN クライアントをソフトウェアをインストールする必要があります。
結論:OpenVPN サーバを Docker で構築する
これらを比較検討した結果,上述の目的①~④を全て満たすことができる,OpenVPN を選ぶことにしましょう。OpenVPN サーバの構築は本来それほど簡単ではありませんが,Docker を利用してイメージを展開することで,かなり楽に OpenVPN サーバ構築ができるようになります。
プロトコル・ポートの選択:UDP vs. TCP
OpenVPN の場合,デフォルトの待ち受けポートは 1194/udp です。多くの場合このままで問題はないでしょう。UDPはTCPに比べオーバーヘッドが少ないので,高速になると期待されます。ですが,次のようなケースで問題になり得ます。
- 航空機内WiFiなど,特に回線が貧弱でパケットロスが起こりやすい場合,UDPの信頼性の低さが問題となる。オーバーヘッドを許容してでも,TCPの信頼性が欲しいケースもある。
- 目的③に関連して,厳しいファイアウォールによって HTTP(S) 以外のアウトバウンド接続が制限されているようなケースだと接続できない。
- また,1194/udp に対して接続していると,「OpenVPN サーバへ接続している」ということが検知されやすく,ブロックされる可能性がある。(中国の金盾とか?)
このようなケースに対処するためには,OpenVPN サーバを 1194/udp ではなく 443/tcp で待ち受けさせるという手が考えられます。443/tcp はHTTPSのポートなので,これがファイアウォールでブロックされている可能性は限りなく小さいです。443/tcp で待ち受ける OpenVPN サーバに対してTLSで暗号化したVPNトンネルを張れば,ウェブブラウザの利用による通常のHTTPS通信と区別が付かないため,ブロックすることは困難でありましょう。
結論:UDPとTCPでデュアルスタンバイとする
一方,通常の利用であれば,オーバーヘッドの少ない 1194/udp で十分です。そこで,Dockerの強みを活かして,
- 同じ設定ファイル一式を元に,OpenVPNサーバを二重に起動させる(Dockerコンテナを2つ起動させる)。
- 片方はUDP,もう片方はTCPで待ち受けさせる。
- 通常利用時はUDPの方を使い,特殊なケースならTCPの方に接続する
というデュアルスタンバイ体制を整えてみましょう。
OpenVPNにおける仮想ネットワークトポロジーの選択:net30
vs. subnet
結論:subnet
を選んでおけばよい
詳細説明:net30 と subnet の違い
デフォルトでは,OpenVPN サーバが構築する仮想ネットワークのトポロジー設定は,net30
というものになっています。これは,IPv4 の 32bit のIPアドレスにおいて,上位 30bit をネットワークアドレスとし,下位 2bit のみをホストアドレスとして,接続クライアントごとに別々のネットワーク空間に属するように設定するものです。
net30
トポロジーの例
クライアントAとOpenVPNサーバとの接続
- ネットワークアドレス:
192.168.255.0
(2進法で書けば11000000.10101000.11111111.00000000
) - サブネットマスク:
255.255.255.252
(2進法で書けば11111111.11111111.11111111.11111100
)
➡ サブネット内のIPアドレスは4個しかないことになります。しかも,うち2つはネットワークアドレスとブロードキャストアドレスになるため,ホストに使えるIPアドレスは2個のみ。その2つを,サーバ側とクライアント側で分け合います。
-
192.168.255.0
(下位2bitが00
):ネットワークアドレス -
192.168.255.1
(下位2bitが01
):OpenVPNサーバ側のIPアドレス -
192.168.255.2
(下位2bitが10
):クライアントAのIPアドレス -
192.168.255.3
(下位2bitが11
):ブロードキャストアドレス
クライアントBとOpenVPNサーバとの接続
- ネットワークアドレス:
192.168.255.4
(2進法で書けば11000000.10101000.11111111.00000100
) - サブネットマスク:
255.255.255.252
(2進法で書けば11111111.11111111.11111111.11111100
)
同様に,サブネット内の4個のIPアドレスは,ネットワークアドレス・サーバのIPアドレス・クライアントのIPアドレス・ブロードキャストアドレスで使用します。
-
192.168.255.4
(下位2bitが00
):ネットワークアドレス -
192.168.255.5
(下位2bitが01
):OpenVPNサーバ側のIPアドレス -
192.168.255.6
(下位2bitが10
):クライアントBのIPアドレス -
192.168.255.7
(下位2bitが11
):ブロードキャストアドレス
このように,net30
トポロジーの場合,1クライアントごとにIPアドレス空間を4個ずつ消費してゆくので,アドレス空間の使い方として効率が低いです。また何より,クライアントごとに別々のサブネットに所属しているのは,目的④(外部から自宅LAN内の端末と同一ネットワーク内として接続したい)のためには分かりづらいです。そこで,もっとシンプルに,OpenVPNサーバと,接続中の各クライアントとが,全て同一ネットワーク内に所属するような仮想ネットワーク構成としたいです。それを実現するのが subnet
というネットワークトポロジー設定です。これであれば,
- ネットワークアドレス:
192.168.255.0
- サブネットマスク:
255.255.255.0
- OpenVPNサーバ側のIPアドレス:
192.168.255.1
- クライアントAのIPアドレス:
192.168.255.2
- クライアントBのIPアドレス:
192.168.255.3
⋮ - ブロードキャストアドレス:
192.168.255.255
という,ごく普通の 192.168.255.0/24
の仮想ネットワークが実現でき,アドレス空間が有効活用できますし,何よりシンプルで分かりやすいです。
古いOpenVPNクライアントだと subnet
トポロジーに対応していないという理由で net30
トポロジーがデフォルト設定となっているようですが,今から新規にOpenVPNサーバを構築するなら,subnet
トポロジーで構築しておくのがよいでしょう。
作業工程
前提
VPSのドメイン名
ConoHa VPS のサーバに与えられたドメイン名が VPS.SERVERNAME.COM
であるとします。独自ドメイン名を取得していない場合も,ConoHa VPS のコントロールパネルを見ると,vXXX-XXX-XXX-XXX.fxcu.static.cnode.jp
といったドメインが与えられていることが分かりますので,それを使えばOKです。
VPSのSSHサーバへの接続法
ConoHa VPS へのSSH接続ができるようになっているとします。前記事で設定したように,
- パスワード認証を無効化して
~/.ssh/id_ed25519
による公開鍵認証に -
~/.ssh/config
において__conoha
という名前でエイリアス設定
としておくと,安全かつ便利です。
Host __conoha
User USERNAME
Hostname VPS.SERVERNAME.COM
Port 12345
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
OpenVPNサーバの設定ファイルの保存場所
VPSのUbuntu上において,$HOME/ovpn-data
というディレクトリを設定ファイルの保存場所とします。
工程1:OpenVPN の Docker イメージを pull して初期設定
$ mkdir $HOME/ovpn-data
$ cd $HOME/ovpn-data
OpenVPN サーバの Docker イメージとして,kylemanna/openvpn
というイメージを使います。
これを pull しましょう。
$ docker pull kylemanna/openvpn
まずはデフォルト通り 1194/udp で待ち受ける設定,かつ subnet
トポロジーで,設定ファイルを生成します。
$ docker run --rm -v $HOME/ovpn-data:/etc/openvpn \
kylemanna/openvpn ovpn_genconfig \
-u udp://VPS.SERVERNAME.COM:1194 \
-e 'topology subnet'
これで ~/ovpn-data/openvpn.conf
という設定ファイルが生成されます。必要に応じてこのファイルを次のように手動編集しましょう。
$ sudo vim ~/ovpn-data/openvpn.conf
デフォルトで生成される openvpn.conf
において,
route 192.168.254.0 255.255.255.0
という行は,subnet
トポロジを採用する結果不要となりますので,#
でコメントアウトしておくとよいでしょう。
また,他には,
duplicate-cn
という行を加えておけば,同一証明書を持つ複数の端末からの同時接続を許可できます。ただし,本来,セキュリティ上は端末ごとに別々の証明書を発行して分離すべきです。同一端末扱いの複数のクライアントが同時に接続すると,ログ上での追跡が難しくなりますし,目的④のために端末ごとにIPアドレスを固定する運用をする場合には障害になりますので,避けておいた方がよいでしょう。ここでは duplicate-cn
は指定しないものとします。
工程2:PKI初期設定
次に,PKI(公開鍵基盤)の初期設定を行います。CA(認証局)のパスフレーズを決めてください。これは忘れぬよう記録しておきましょう。Common Name の質問はそのままリターンを押せばよいです。
$ docker run --rm -it -v $HOME/ovpn-data:/etc/openvpn \
kylemanna/openvpn ovpn_initpki
init-pki complete; you may now create a CA or requests.
(中略)
Enter New CA Key Passphrase:(ここでCAのパスフレーズを入力)
Re-Enter New CA Key Passphrase: (確認のためもう一度入力)
(中略)
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:(リターン入力)
(中略)
Enter pass phrase for /etc/openvpn/pki/private/ca.key:(CAのパスフレーズを再入力)
(中略)
Enter pass phrase for /etc/openvpn/pki/private/ca.key:(CAのパスフレーズを再入力)
(後略)
工程3:クライアントごとの鍵ペアと設定ファイルを生成
VPN接続したいクライアント(Mac,Windows,iPhoneなど)ごとに,鍵ペアと設定ファイルを生成します。
まずは,OpenVPNサーバが認識する固有のクライアント名を決めましょう。半角英数字のみ,記号は不可のようです。そして,そのクライアントに対応するパスフレーズも決めます。
$ export CLIENTNAME="ClientA"
$ docker run --rm -it -v $HOME/ovpn-data:/etc/openvpn \
kylemanna/openvpn easyrsa build-client-full $CLIENTNAME
(中略)
Enter PEM pass phrase:(クライアントAのパスフレーズを入力)
Verifying - Enter PEM pass phrase:(クライアントAのパスフレーズを再入力)
(中略)
Enter pass phrase for /etc/openvpn/pki/private/ca.key:(CAのパスフレーズを入力)
(中略)
Write out database with 1 new entries
Data Base Updated
次に,ここで設定した ClientA
というクライアントにインストールすべき設定ファイル(証明書を含む)を生成します。
$ docker run --rm -v $HOME/ovpn-data:/etc/openvpn \
kylemanna/openvpn ovpn_getclient $CLIENTNAME > ~/$CLIENTNAME.ovpn
すると,この例ならば ~/ClientA.ovpn
というファイルが生成されているはずです。
以下,他に接続したいクライアント ClientB
, ClientC
, …… があれば,その分だけこの工程を繰り返します。
工程4:ファイアウォールの許可設定
ConoHa VPS 内のLinuxのファイアウォール設定において,1194/udp
と 443/tcp
を許可します。Ubuntu の ufw
であれば次のようにして許可します。
$ sudo ufw allow 1194/udp
$ sudo ufw allow 443/tcp
ConoHa の側でもファイアウォール設定がある場合は,そちらにも許可設定を加えることを忘れてはいけません。ConoHa VPS Ver.3.0 の「セキュリティグループ」であれば,次のようにカスタムしたセキュリティグループを適用します。
工程5:デュアルスタンバイのための Docker compose ファイルを用意
次のファイルをVPS上の ~/docker-compose.yml
として用意します。
version: '3.8'
services:
openvpn-udp:
image: kylemanna/openvpn
container_name: openvpn-udp
ports:
- "1194:1194/udp"
volumes:
- "./ovpn-data:/etc/openvpn"
cap_add:
- NET_ADMIN
restart: unless-stopped
openvpn-tcp:
image: kylemanna/openvpn
container_name: OpenVPN-TCP
ports:
- "443:1194/tcp"
volumes:
- "./ovpn-data:/etc/openvpn"
cap_add:
- NET_ADMIN
command: ovpn_run --proto tcp
restart: unless-stopped
これにより,openvpn-udp
と openvpn-tcp
という2つのコンテナが起動します。後者は,起動オプションとして --proto tcp
が指定されています。これにより,UDPでの待ち受けを,強制的に(ポート番号は同じ1194のままで)TCPでの待ち受けに変更して起動します。そして,
- VPS上の 1194/udp ↔
openvpn-udp
コンテナ内の 1194/udp - VPS上の 443/tcp ↔
openvpn-tcp
コンテナ内の 1194/tcp
へとマッピングすることにより,外部からVPSへの 1194/udp,443/tcp への接続のそれぞれが openvpn-udp
, openvpn-tcp
へとマッピングされる,というわけです。
では,Docker compose を使って,これら2つのコンテナを起動してみましょう。
$ cd ~
$ docker compose up -d
docker ps -a
で起動中のコンテナを確認し,openvpn-udp
, openvpn-tcp
という名称のコンテナが起動していることを確認しておきましょう。
工程6:クライアントに設定ファイルをダウンロードして編集
次に,VPS上の ~/ClientA.ovpn
を,実際に使用するクライアントAへと安全にダウンロードします。クライアントAの ~/.ssh/config
で __conoha
という名前のエイリアス設定が済んでいれば,次のように scp
コマンドによって安全・簡単にダウンロードできます。
$ scp __conoha:ClientA.ovpn .
次に,UDP用とTCP用にファイルを分けます。
$ mv ClientA.ovpn ClientA-udp.ovpn
$ cp ClientA-udp.ovpn ClientA-tcp.ovpn
ClientA-tcp.ovpn
のファイルを開き,
remote VPS.SERVERNAME.COM 1194 udp
の行を
remote VPS.SERVERNAME.COM 443 tcp
に変更しておきます。
工程7:クライアントに OpenVPN Connect をインストールして接続
VPN接続したいクライアント(Mac,Windows,iPhoneなど)に対し,クライアントソフトウェアである OpenVPN Connect をインストールしましょう。
そして,新規プロファイル設定画面において,ClientA-udp.ovpn
を投入します。
これで接続できれば成功です!
同様に,ClientA-tcp.ovpn
用のプロファイルも作っておきましょう。
確認:接続先から見た自分のIPアドレスが ConoHa VPS のものに変更されているか?
WhatIsMyIPAddress.com のようなIPアドレス確認サイトを利用して,接続先サイト側から見た自分のIPアドレスが ConoHa VPS のものに変化したことを確認しましょう。
発展:どのようにして全通信をVPNトンネルに通しているか?
ClientA-udp.ovpn
を開いてみると,次のような行があります。
redirect-gateway def1
この設定があることで,(LAN内向けの通信を除く)全通信がVPNトンネル経由で送られるようになります。しかし,OSのデフォルトゲートウェイ設定は変更されていません。OSのデフォルトゲートウェイ設定を変えることなく,LAN外向けの全通信をどのようにしてVPNトンネルへと差し向けているのでしょうか。
まずは ifconfig
で設定を確認してみます。
$ ifconfig
(中略)
utun14: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
inet 192.168.255.2 --> 192.168.255.1 netmask 0xffffff00
この場合,utun14
がVPN用の仮想ネットワークインターフェースです。そこで,utun14
が絡むルーティングテーブルを確認してみます。
$ netstat -rn | grep utun14
0/1 192.168.255.1 UGScg utun14
128.0/1 192.168.255.1 UGSc utun14
192.168.255 192.168.255.2 UGSc utun14
192.168.255.1 192.168.255.2 UHr utun14
ここに,0/1
および 128.0/1
というルート情報があることに注目します。ルーティングテーブルにはロンゲストマッチという原則があります。宛先IPアドレスにマッチするルート情報が複数ある場合,その中で最もプレフィックス長が長いルート情報が優先されます。
2進法で書くと,0/1
と 128.0/1
はそれぞれ
00000000.00000000.00000000.00000000(先頭1bitがプレフィックス)
10000000.00000000.00000000.00000000(先頭1bitがプレフィックス)
となります。つまり,先頭1bitが0
であるようなIPアドレスは前者に,1
であるようなIPアドレスは後者にマッチします。
これらはプレフィックス長が1なので,ロンゲストマッチ原則における優先順位としては最下位となります。しかし,どのIPアドレスも,先頭1bitは0
か1
のどちらかなので,他のルート情報にマッチしなかったどのIPアドレスも,必ずこのどちらかのルート情報にマッチして拾われることになります。その結果,
- 他のどのルート情報にもマッチしなかった宛先への通信は,全て
0/1
,128.0/1
いずれかのルート情報にマッチする結果,utun14
に回される - OSのデフォルトゲートウェイ設定まで落ちてくることはなく,デフォルトゲートウェイ設定が結果的に無効化されることになる
となるわけです。
仮想ネットワーク内でのクライアントのIPアドレスを固定するには
目的④(外部から自宅LAN内の端末と同一ネットワーク内として接続したい)のためには,仮想ネットワーク内でのクライアントのIPアドレスが固定されるようにした方が都合が良いです。
そこで,OpenVPNサーバの側に,クライアントIPアドレス固定設定を加えましょう。
$ cd ~/ovpn-data/ccd
$ sudo echo ifconfig-push 192.168.255.2 255.255.255.0 > ClientA
この ClientA
というのは,工程3で定めたクライアント名と一致させてください。192.168.255.1
はコンテナ内のサーバ側のIPアドレスとして使うので,クライアントのIPアドレスとしては 192.168.255.2
~192.168.255.254
のいずれかを指定してください。
その他のメンテナンス作業
稼働中のコンテナ内に入ってネットワークの状況を調査するには
$ docker exec -it openvpn-udp /bin/bash
こうしてコンテナ内に入れば,ifconfig
や netstat
で仮想ネットワークの状況調査ができます。
クライアント証明書を失効させるには
使わなくなったクライアントがあれば,その証明書ではVPN接続できないようにしましょう。「登録を削除する」ということはできず,「証明書を CRL (Certificate Revocation List,証明書失効リスト) に登録する」という手を取ります。
$ export CLIENTNAME="ClientA"
# revokeしてCRLに登録
$ docker run --rm -it -v $HOME/ovpn-data:/etc/openvpn \
kylemanna/openvpn easyrsa revoke $CLIENTNAME
$ docker run --rm -it -v $HOME/ovpn-data:/etc/openvpn \
kylemanna/openvpn easyrsa gen-crl
# その上でOpenVPNサーバを再起動 → CRLが有効化
$ docker compose down
$ docker compose up -d