背景
複数拠点(A, B, Cなど)を OpenVPN 経由で接続し、 **2系統のVPNトンネル(10.8.0.0/24 と 10.10.0.0/24)**を経由して経路冗長化したい。(openvpnサーバは独立した2マシン) そんなニーズから検証を始めました。
各拠点はそれぞれローカルLAN(例: 192.168.1.0/24, 192.168.2.0/24, 192.168.3.0/24)を持ち、 それらを OSPF (FRRouting) で自動経路学習させて、 VPNトンネル越しに相互通信できるようにしたい、という構成です。
やりたかったこと
- OpenVPNで各拠点を接続(多拠点site-to-site VPN)
- FRR (OSPF) を使って動的ルーティング
- トンネルごとにコストを設定して経路選択
- できれば route-nopull で手動ルーティングを制御 目標は「OpenVPNトンネルを2本走らせて、FRRで経路選択を任せる」ことでした。 これを実現するにあたり、色々とハマったので知見をまとめておこうと思います。
OpenVPNのルーティングの構造
OpenVPNはユーザランドで動作し、/dev/net/tun デバイスを通じて通信します。
- Linuxカーネルはtunデバイスにパケットを出す
- そこから先は**OpenVPNプロセス(ユーザランド)**が受け取り、独自の内部ルーティングテーブルで転送を行う つまり、Linuxのルーティングテーブル(ip route)とは別世界で動いています。(ここが非常に大事です。openvpnサーバないのルーティングテーブルがちゃんと設定されていないと、クライアントから届いたパケットをどのクライアントに転送すればいいかがわかりません。ちなみに、openvpnのルーティングテーブルは/var/log/openvpn-status.logから確認可能です)
push と route-nopull の関係
OpenVPNサーバはクライアントに対して push “route …” を送ることで、 クライアントのOSにもユーザランドにもルート情報を注入します。
一方、クライアント設定で route-nopull を有効にすると、 その push 情報をすべて拒否します。 frrでルーティングをさせたい場合には、route-nopullにしておかないと、0.0.0.0/1と128.0.0.1/1がOSのルーティングテーブルに登録され、すべてのパケットが特定のnicから出るように設定されてしまうので注意が必要(server側で**push “redirect-gateway def1 bypass-dhcp”**が入っている場合)
FRR(OSPF)を組み合わせても解決しない理由
FRRでOSPFを走らせると、 VPNトンネル越しに相手拠点のLAN情報(例: 192.168.x.x/24)が学習されます。 これにより、OSレベルのルーティングテーブルは更新されます。
しかし!
OpenVPNサーバはそのOSルートを全く参照しません。 OpenVPNプロセスは独自に「このLANはどのクライアントの背後にあるか」を iroute で知っていなければ、パケットをどこに転送すべきか判断できません。
自分の場合は、openvpnサーバの/etc/openvpn/ccd以下に書くルーティングファイルの情報が間違っており、クライアント1 -> サーバはパケットが届いていたが、サーバがクライアント2宛のパケットをうまく転送できずdropしてしまっていたのが原因ですね。
実際にうまく行った設定
各クライアントの設定
log syslog informational
frr version 9.x
frr defaults traditional
hostname site-a
service integrated-vtysh-config
!
interface lan0
ip ospf passive
!
interface tun0
ip ospf network point-to-multipoint
ip ospf cost 50 ! プライマリ(ECMPなら削る or tun1と同コスト)
!
interface tun1
ip ospf network point-to-multipoint
ip ospf cost 10 ! バックアップ(ECMPならtun0と同コスト)
!
router ospf
router-id 3.3.3.3
passive-interface lan0
network 192.168.2.0/24 area 0
network 10.8.0.0/24 area 0
network 10.10.0.0/24 area 0
! maximum-paths 2 ! 負荷分散したいときに有効化
!
line vty
サーバの設定
root@hogehoge:~# cat /etc/openvpn/ccd/*
iroute 192.168.10.0 255.255.255.0
iroute 192.168.2.0 255.255.255.0
iroute 192.168.40.0 255.255.255.0
iroute 192.168.1.0 255.255.255.0
root@xhogehoge:~# cat /etc/openvpn/server.conf
port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh.pem
crl-verify crl.pem
# 既存と同じプールを継続(tun内は /24 で配布)
server 10.10.0.0 255.255.255.0
topology subnet
ifconfig-pool-persist ipp.txt
# ========= ここが重要:クライアントのルーティングは FRR に任せる =========
# ※ クライアントへルートやデフォルトGWを push しない(route-nopull 前提)
push "redirect-gateway def1 bypass-dhcp"
push "route 10.10.0.0 255.255.255.0"
push "dhcp-option DNS 8.8.8.8"
# クライアントごとの固定tun IPを使いたい時は ccd を活用(推奨)
client-config-dir /etc/openvpn/ccd
# ※ FRRでクライアントLANを学習させるため、ここでserver側から静的routeを足す必要は基本なし
# (段階移行で一時的に必要なら残す)
# クライアント側でroute-nopull入れれば大丈夫だということがわかった
route 192.168.1.0 255.255.255.0 # white house
route 192.168.40.0 255.255.255.0 # Tokiwa
route 192.168.2.0 255.255.255.0 # Kamino
route 192.168.10.0 255.255.255.0 # Hachiouji
# MTU周り(環境に合わせて調整)
tun-mtu 1400
#mssfix 1360
# トンネル内でクライアント間のL3を許可(データはサーバ経由でフォワードされる)
client-to-client
# キープアライブ
keepalive 10 120
explicit-exit-notify 1
# セキュリティ:圧縮は無効(VORACLE回避のため)
#comp-lzo
#compress lz4-v2
user nobody
group nogroup
persist-key
persist-tun
status /var/log/openvpn-status.log
log /var/log/openvpn.log
log-append /var/log/openvpn.log
verb 3
status-version 2
# duplicate-cn
おまけ
openvpn server1とserver2の二つがある時、server2が落ちる前と落ちた後のospfのルーティングテーブル
前
ray@kami:~$ sudo vtysh -c "show ip route ospf"
% Can't open configuration file /etc/frr/vtysh.conf due to 'No such file or directory'.
Codes: K - kernel route, C - connected, S - static, R - RIP,
O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR,
f - OpenFabric,
> - selected route, * - FIB route, q - queued, r - rejected, b - backup
t - trapped, o - offload failure
O>* 10.8.0.20/32 [110/10] via 10.8.0.20, tun0 inactive, weight 1, 00:04:48
* via 10.10.0.6, tun1, weight 1, 00:04:48
O>* 10.8.0.28/32 [110/0] is directly connected, tun0, weight 1, 02:57:51
O>* 10.8.0.32/32 [110/10] via 10.8.0.32, tun0 inactive, weight 1, 00:04:45
* via 10.10.0.3, tun1, weight 1, 00:04:45
O>* 10.10.0.2/32 [110/0] is directly connected, tun1, weight 1, 16:13:41
O 10.10.0.3/32 [110/10] via 10.8.0.32, tun0 inactive, weight 1, 00:04:45
via 10.10.0.3, tun1 inactive, weight 1, 00:04:45
O 10.10.0.6/32 [110/10] via 10.8.0.20, tun0 inactive, weight 1, 00:04:48
via 10.10.0.6, tun1 inactive, weight 1, 00:04:48
O 192.168.2.0/24 [110/100] is directly connected, enp6s0, weight 1, 16:13:41
O>* 192.168.10.0/24 [110/110] via 10.8.0.32, tun0 inactive, weight 1, 00:04:45
* via 10.10.0.3, tun1, weight 1, 00:04:45
O>* 192.168.40.0/24 [110/110] via 10.8.0.20, tun0 inactive, weight 1, 00:04:48
* via 10.10.0.6, tun1, weight 1, 00:04:48
後
ray@kami:~$ sudo vtysh -c "show ip route ospf"
% Can't open configuration file /etc/frr/vtysh.conf due to 'No such file or directory'.
Codes: K - kernel route, C - connected, S - static, R - RIP,
O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR,
f - OpenFabric,
> - selected route, * - FIB route, q - queued, r - rejected, b - backup
t - trapped, o - offload failure
O 10.8.0.20/32 [110/10] via 10.8.0.20, tun0 inactive, weight 1, 00:00:00
O>* 10.8.0.28/32 [110/0] is directly connected, tun0, weight 1, 02:59:42
O 10.8.0.32/32 [110/10] via 10.8.0.32, tun0 inactive, weight 1, 00:00:00
O>* 10.10.0.2/32 [110/0] is directly connected, tun1, weight 1, 16:15:32
O>* 10.10.0.3/32 [110/10] via 10.8.0.32, tun0, weight 1, 00:00:00
O>* 10.10.0.6/32 [110/10] via 10.8.0.20, tun0, weight 1, 00:00:00
O 192.168.2.0/24 [110/100] is directly connected, enp6s0, weight 1, 16:15:32
O>* 192.168.10.0/24 [110/110] via 10.8.0.32, tun0, weight 1, 00:00:00
O>* 192.168.40.0/24 [110/110] via 10.8.0.20, tun0, weight 1, 00:00:00
tun1の経路情報がなくなり、すべてtun0、つまりサーバ1を経由した経路に変わっていることがわかる。
しかし、これ切り替わるまでに数十秒かかるのですよね。それだと遅い!!もっと早く切り替えたいです。
ルーティング切り替えを早くする方法
interface tun0
ip ospf network point-to-multipoint
ip ospf cost 50 ! プライマリ(ECMPなら削る or tun1と同コスト)
ip ospf hello-interval 1
ip ospf dead-interval 3
!
interface tun1
ip ospf network point-to-multipoint
ip ospf cost 10 ! バックアップ(ECMPならtun0と同コスト)
ip ospf hello-interval 1
ip ospf dead-interval 3
!
こんな感じで、hello-intervalとdead-intervalを入れてください。 前者は1秒に一回helloを投げるということ。 後者は、3秒間 Hello が来なければネイバー切断扱いにする、という感じです。
参考文献
🔹 OpenVPN の内部ルーティング(iroute / CCD が必須)
サーバは「どのクライアントの背後にどのサブネットがあるか」を
irouteで把握する。irouteは CCD(client-config-dir) 内のクライアント別ファイルで指定し、OpenVPN の内部ルーティングを決める(OS ルーティングとは別)。
→ HowTo: explain CCDs and “iroute” (OpenVPN Forum)サーバ設定
routeと CCD のirouteはセットで機能し、LAN 間通信(クライアント背後サブネット到達)を成立させる一般手順として各種ドキュメント/ナレッジで示されている。
→ Routing through OpenVPN clients (UI Community)OpenVPN ステータスファイルの
ROUTING_TABLEは、どのネットワークがどのクライアントに紐づいたかを示す(=サーバ内部ルーティングの実体)。記事中でこれをスクショ/抜粋すると説得力が増す。
→ Mastering OpenVPN (O’Reilly Media)
🔹 client-to-client / topology の意味
--client-to-clientは、サーバが内部でクライアント同士のパケットをルーティングする挙動を有効化するオプション(TUN/TAP に全部押し出すのではなく、OpenVPN デーモン内で処理)。
→ Server Fault: How does client-to-client work?topology subnet(サブネット型トポロジ)では、TUN デバイスが「ブロードキャスト的な」アドレス/マスクを持ち、単一サブネット内で IP を配布する方式になる(/30 の net30 とは異なる)。
→ OpenVPN Manual: Topology subnet explained
🔹 route-push と route-nopull の挙動
サーバからの
push "route …"は、通常クライアント側の OS ルーティングにも OpenVPN 内部にも反映される。クライアントは--pullでそれを受け取る。
一方で--route-nopullを指定すると「プッシュされたルートを受け取らない」ため、クライアントのルーティングテーブルにも反映されない。
→ OpenVPN Manual (route-nopull Option)route-nopullはクライアント設定の末尾に置く等の実装注意点がコミュニティでも共有されている(オプションの上書き事故を防ぐ)。
→ OpenVPN Forum Discussion on route-nopull
🔹 「OS のルートは見えても OpenVPN は別宇宙」
TUN デバイスはカーネル側の仮想IFだが、OpenVPN はユーザ空間で動き、TUN/TAP を経由して L3 パケットを読み書きする。
つまり OpenVPN の転送判断はユーザランド内部のテーブルに依存する(だからirouteが必要)。
→ Medium: Understanding TUN/TAP and OpenVPN Routing実務でも OpenVPN×OSPF/FRR を組み合わせる場合、OpenVPN にも経路(
iroute)を教える必要があるという知見が共有されている。
OSPF は OpenVPN の内部ルーティングを更新しない。
→ Netgate Forum: OSPF with OpenVPN dynamic routing discussion
→ Open Maniak Blog: FRR OSPF over OpenVPN
🔹 WireGuard を推す根拠(カーネル空間・FRR と親和)
WireGuard は Linux カーネル内で動作する L3 VPN(=暗号化された仮想インターフェイス)。
カーネルルーティングと直結するため、FRR/OSPF の経路がそのまま実効経路になる。
→ WireGuard Official Paper (PDF)公式も「Linux カーネル実装(最大性能)」を明言。必要に応じてユーザ空間実装もあるが、基本はカーネル側で完結。
→ WireGuard Official Website
🔹 記事内で使える “引用しやすい一文” テンプレ
OpenVPN の
irouteは OS のルーティングではなく、OpenVPN デーモン内部のルーティングを教えるためのもの(CCD でクライアント別に設定)。
→ OpenVPN Forum Discussion on irouteroute-nopullを入れたクライアントは、サーバからのルート push を受け取らない(OS にも入らない)。
→ OpenVPN Manual: route-nopullclient-to-clientは OpenVPN サーバがクライアント間の転送を内部で処理するスイッチ。
→ Server Fault: client-to-client option explanationtopology subnetは単一サブネット配布の挙動(net30と違う)。
→ OpenVPN Topology ModesWireGuard はカーネル内実装の L3 トンネルで、ルーティングはそのままカーネル/FRR と整合する。
→ WireGuard Paper (kernel implementation)