背景

複数拠点(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 デバイスを通じて通信します。

  1. Linuxカーネルはtunデバイスにパケットを出す
  2. そこから先は**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 で把握する。
    irouteCCD(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 は別宇宙」


🔹 WireGuard を推す根拠(カーネル空間・FRR と親和)

  • WireGuard は Linux カーネル内で動作する L3 VPN(=暗号化された仮想インターフェイス)。
    カーネルルーティングと直結するため、FRR/OSPF の経路がそのまま実効経路になる。
    WireGuard Official Paper (PDF)

  • 公式も「Linux カーネル実装(最大性能)」を明言。必要に応じてユーザ空間実装もあるが、基本はカーネル側で完結。
    WireGuard Official Website


🔹 記事内で使える “引用しやすい一文” テンプレ