一覧NW-16-2 · WAN・トンネル・VPN

VPNのルーティング

full/splitの経路と、VPN利用者への返信・認可を確認します。

説明

VPNへ接続したことと、すべての通信がVPNを通ることは同じではありません。どの宛先をトンネルへ送るかは端末の経路で決まり、相手側には利用者への戻り経路も必要です。

IPv4のsplit tunnelとfull tunnelの表を比較し、VPN終端への例外経路を確認します。次に利用者アドレスへの返信と通信許可を調べ、IPv6の経路も別に確認します。

1split tunnelの経路表

VPN利用者のトンネルIPは10.99.0.10、業務宛先は10.20.0.0/16、VPN終端は203.0.113.5です。

import ipaddress宛先の所属を判定; split=宛先範囲と出力先[("203.0.113.5/32","wan0"),("10.20.0.0/16","vpn0"),("0.0.0.0/0","wan0")]; splitsplitの経路

業務宛先だけvpn0へ送り、一般宛先とVPN終端はwan0です。終端へ運ぶ外側通信と、VPN内へ入れる内側通信を別々に考えます。この演習は宛先だけで選ぶ単一表のモデルです。

2最長一致で出力先を求める

一致する経路のうちプレフィックス長が最も長い候補を選びます。

def route(address,table):宛先と表を受け取る address=IP形式へ変換ipaddress.ip_address(address) matches=一致する候補[r for r in table if address.version==ipaddress.ip_network(r[0]).version and address in ipaddress.ip_network(r[0])] return max最も長い範囲を優先(matches,key=lambda r:ipaddress.ip_network(r[0]).prefixlen)[1] if matches else None

同じ宛先に複数の候補があれば、/32は/16や/0より具体的です。VPNへのdefaultを設定しても、終端への/32例外を残せば外側通信は物理回線へ出せます。

3fullとsplitで同じ宛先を比べる

fullではIPv4のdefaultをvpn0へ変更し、終端の/32例外を残します。

full=defaultをVPNへ変更[("203.0.113.5/32","wan0"),("0.0.0.0/0","vpn0")]; targets=業務・一般・終端の宛先["10.20.1.20","198.51.100.20","203.0.113.5"]; [(a,route同じ宛先を二方式で検索(a,split),route(a,full)) for a in targets]

業務宛先はどちらもVPN、一般宛先はsplitでwan0・fullでvpn0です。VPN終端はどちらもwan0です。実際のfull tunnelにはローカルLAN等の例外を設ける構成もあり、名称だけで全経路を決め付けず表を確認します。

4VPN終端への例外を失った場合

fullの表から/32例外を取り除きます。

without_exception=例外を除いた表[("0.0.0.0/0","vpn0")]; routeVPN終端の出力先("203.0.113.5",without_exception)

終端までvpn0へ向きます。外側の配送経路がトンネル自身に依存するため、このモデルでは到達を成立させられません。実装は別の経路表やポリシーで外側通信を保護する場合もあります。

5サーバー側から利用者へ返信できるか

業務側ルーターの表に、利用者プール10.99.0.0/24への経路がない状態です。

server_routes=業務側の経路[("10.20.0.0/16","corp-lan")]; before=追加前の返信先検索route("10.99.0.10",server_routes); server_routes.append利用者プールへの経路(("10.99.0.0/24","vpn-gateway")); (before追加前後を比較,route("10.99.0.10",server_routes))

追加前はNone、追加後はvpn-gatewayです。端末が業務宛先へ送れるだけでなく、業務側もトンネル利用者へ返せる必要があります。VPN側で送信元変換する設計では、業務側から見える返信先も変わります。

6経路があっても許可は別に必要

利用者に許す対象を10.20.1.20のTCP443だけとした認可モデルです。

allowed=認可した宛先とポート{("10.20.1.20",443)}; [(dest,port具体的な通信を判定,(dest,port) in allowed) for dest,port in [("10.20.1.20",443),("10.20.1.20",22),("10.20.2.20",443)]]

許可は最初の通信だけです。トンネル確立、宛先への経路、利用者の通信権限は別の条件なので、VPN接続済みという表示だけで業務網の全サービスへ到達できるとは扱いません。

7IPv6のdefaultを別に確認する

IPv4のfull表へ、物理回線のIPv6 defaultが残っている例を追加します。

dual=IPv6の経路を追加full+[("::/0","wan0")]; (routeIPv4とIPv6を別々に検索("198.51.100.20",dual),route("2001:db8:20::20",dual))

IPv4はvpn0でもIPv6はwan0です。両方をVPN対象にする要件なら、IPv6の対応と経路・制御も必要です。接続名にfullと書かれていても、対象アドレスファミリーを確認します。

まとめ

VPNの内側宛先と外側終端の経路を分け、戻り経路も確認します。

full/splitの対象、利用者の認可、IPv4/IPv6を別々に照合します。

操作履歴と解説を振り返れます。

Python 3

1
import ipaddress; split=[("203.0.113.5/32","wan0"),("10.20.0.0/16","vpn0"),("0.0.0.0/0","wan0")]; split
[('203.0.113.5/32', 'wan0'), ('10.20.0.0/16', 'vpn0'), ('0.0.0.0/0', 'wan0')]
2
def route(address,table): address=ipaddress.ip_address(address) matches=[r for r in table if address.version==ipaddress.ip_network(r[0]).version and address in ipaddress.ip_network(r[0])] return max(matches,key=lambda r:ipaddress.ip_network(r[0]).prefixlen)[1] if matches else None
3
full=[("203.0.113.5/32","wan0"),("0.0.0.0/0","vpn0")]; targets=["10.20.1.20","198.51.100.20","203.0.113.5"]; [(a,route(a,split),route(a,full)) for a in targets]
[('10.20.1.20', 'vpn0', 'vpn0'), ('198.51.100.20', 'wan0', 'vpn0'), ('203.0.113.5', 'wan0', 'wan0')]
4
without_exception=[("0.0.0.0/0","vpn0")]; route("203.0.113.5",without_exception)
'vpn0'
5
server_routes=[("10.20.0.0/16","corp-lan")]; before=route("10.99.0.10",server_routes); server_routes.append(("10.99.0.0/24","vpn-gateway")); (before,route("10.99.0.10",server_routes))
(None, 'vpn-gateway')
6
allowed={("10.20.1.20",443)}; [(dest,port,(dest,port) in allowed) for dest,port in [("10.20.1.20",443),("10.20.1.20",22),("10.20.2.20",443)]]
[('10.20.1.20', 443, True), ('10.20.1.20', 22, False), ('10.20.2.20', 443, False)]
7
dual=full+[("::/0","wan0")]; (route("198.51.100.20",dual),route("2001:db8:20::20",dual))
('vpn0', 'wan0')

演習完了