Linux Kernel の VXLAN 実装へ IP フラグメントを許容するパッチと DKMS パッケージの紹介

はじめに

CuteIP の sobadon です。 Linux Kernel には VXLAN の実装があります。 この VXLAN 実装は、VXLAN でカプセル化されたパケットの IP フラグメントは許容していません。 そのため MTU 1500 の上に同じく MTU 1500 の仮想 L2 ネットワークを構築できません。

パフォーマンスを犠牲にしてでも VXLAN でカプセル化されたパケットの IP フラグメントしながらパケットのカプセル化を行いたい場合があります。 例えば NTT 東日本・西日本の IPv6 NGN の上に MTU 1500 の仮想 L2 ネットワークの構築が挙げられます。

本記事では、IP フラグメントを許容するために必要な VXLAN 実装へのパッチと、それを適用した DKMS パッケージの作成について紹介します。

以降、Linux Kernel に関する説明では v6.5 を前提とします。 また、VXLAN のアンダーレイネットワークは IPv6 を前提とします。

背景

MTU 1500 のアンダーレイネットワークの上に MTU 1500 の仮想 L2 ネットワーク(ここでは、ping 192.0.2.1 -s 1472 -M do のように 1500 bytes が通ることとします)を構築しようとすると VXLAN ヘッダーなどにともないパケットのサイズが大きくなるため、VXLAN でカプセル化されたパケットの IP フラグメントが必要となります。 しかし、Linux Kernel の VXLAN 実装は VXLAN でカプセル化されたパケットの IP フラグメントを許容していません。 この挙動は、以下に引用する RFC7348 の 4.3. Physical Infrastructure Requirements に準拠しているととらえれば、理解できます。

VTEPs MUST NOT fragment VXLAN packets. Intermediate routers may fragment encapsulated VXLAN packets due to the larger frame size. The destination VTEP MAY silently discard such VXLAN fragments. To ensure end-to-end traffic delivery without fragmentation, it is RECOMMENDED that the MTUs (Maximum Transmission Units) across the physical network infrastructure be set to a value that accommodates the larger frame size due to the encapsulation. Other techniques like Path MTU discovery (see [RFC1191] and [RFC1981]) MAY be used to address this requirement as well.

パケットのカプセル化を行う際に VXLAN でカプセル化されたパケットの IP フラグメントすることはパフォーマンスの点で好ましいものではないです。 一方で、パフォーマンスを犠牲にしてでも VXLAN でカプセル化されたパケットの IP フラグメントしながらパケットのカプセル化を行いたい場合もあります。 そのような場合には、VXLAN でカプセル化されたパケットの IP フラグメントを許容する VXLAN 実装が必要です。

パッチ

変更しなければならない処理は 2 点あります。 変更の差分は以下に示すコミットで確認できます

https://github.com/cuteip/vxlan2-dkms/commit/95fd19b977995c8747493c62117f49493edc243e

diff --git a/src/v6.5/vxlan_core.c b/src/v6.5/vxlan_core.c
index c9a9373..ef75408 100644
--- a/src/v6.5/vxlan_core.c
+++ b/src/v6.5/vxlan_core.c
@@ -2624,9 +2624,16 @@ void vxlan_xmit_one(struct sk_buff *skb, struct net_device *dev,
 				goto out_unlock;
 		}

+		// skb_tunnel_check_pmtu() の reply が true の場合、
+		// fragmentation needed や packet too big の ICMP エラーが返される(返されててしまう)。
+		// ignore-df するとき、PMTU の結果を返答する必要がないので、reply = false にして返さないようにする。
+		// patch 前は、以下のような packet too big のメッセージを返答する挙動がみられた。
+		// 18:05:59.020745 52:54:00:35:59:c7 > 52:54:00:d7:36:c7, ethertype IPv6 (0x86dd), length 1294: (hlim 64, next-header ICMPv6 (58) payload length: 1240) fd11:bbbb::1 > fd11:bbbb::2: [bad icmp6 cksum 0xb644 -> 0x4099!] ICMP6, packet too big, mtu 1430
+		// 参考:
+		// - https://lore.kernel.org/netdev/c7b3e4800ea02d964bab7dd9f349e0a0720bff2d.1596487323.git.sbrivio@redhat.com/
 		err = skb_tunnel_check_pmtu(skb, ndst,
 					    vxlan_headroom((flags & VXLAN_F_GPE) | VXLAN_F_IPV6),
-					    netif_is_any_bridge_port(dev));
+					    false);
 		if (err < 0) {
 			goto tx_error;
 		} else if (err) {
@@ -2657,6 +2664,12 @@ void vxlan_xmit_one(struct sk_buff *skb, struct net_device *dev,
 		if (err < 0)
 			goto tx_error;

+		// inner で DF=1 だったとしても、outer は DF=0 になってほしい。
+		// vxlan_xmit() のコメントでは inner の DF を outer に継承する、と書かれているが、
+		// ここの ignore_df 書き換えはそれを無視する形
+		// 参考:
+		// - OpenVPN で outer IPv6 パケットの ignore_df: https://github.com/OpenVPN/ovpn-dco/commit/3ba6c07ababd4d491c77f5327f8105bead6ddccc
+		skb->ignore_df = 1;
 		udp_tunnel6_xmit_skb(ndst, sock6->sock->sk, skb, dev,
 				     &local_ip.sin6.sin6_addr,
 				     &dst->sin6.sin6_addr, tos, ttl,
@@ -4784,5 +4797,5 @@ module_exit(vxlan_cleanup_module);
 MODULE_LICENSE("GPL");
 MODULE_VERSION(VXLAN_VERSION);
 MODULE_AUTHOR("Stephen Hemminger <stephen@networkplumber.org>");
-MODULE_DESCRIPTION("Driver for VXLAN encapsulated traffic");
+MODULE_DESCRIPTION("Driver for VXLAN encapsulated traffic (ignore-df patch)");
 MODULE_ALIAS_RTNL_LINK("vxlan");

これらの変更点について説明します。

1. ICMP Packet Too Big パケットの送信無効化

パッチを適用していない VXLAN 実装とブリッジインターフェースを使って構築した仮想 L2 ネットワークで、ping コマンドを用いて ICMP Echo Request パケットを送信すると、PMTU Discovery の結果として ICMP Packet Too Big パケットが返されます。

# fd11:bbbb::1 と fd11:bbbb::2 はアンダーレイネットワーク
18:05:59.020745 52:54:00:35:59:c7 > 52:54:00:d7:36:c7, ethertype IPv6 (0x86dd), length 1294: (hlim 64, next-header ICMPv6 (58) payload length: 1240) fd11:bbbb::1 > fd11:bbbb::2: [bad icmp6 cksum 0xb644 -> 0x4099!] ICMP6, packet too big, mtu 1430

この PMTU Discovery の挙動については以下に示すパッチの説明が参考になります。

[PATCH net-next v2 2/6] tunnels: PMTU discovery support for directly bridged IP packets - Stefano Brivio

PMTU Discovery は不要であるため、skb_tunnel_check_pmtu()bool replyfalse にして ICMP パケットを返答しないようにします。

2. skb->ignore_df = 1

vxlan_xmit() より、outer パケットの DF フラグは inner パケットから継承されます。 そのため、仮想 L2 ネットワーク上で ping 192.0.2.1 -s 1472 -M do のように DF フラグを立てて ICMP パケットを送ると VXLAN でカプセル化したパケットの DF フラグも立ちます。 したがって、仮想 L2 ネットワーク上で DF フラグを立てて 1500 bytes のパケットを送信しようとすると、カプセル化されることにともない 1500 bytes を超えてしまうが、DF フラグが立っているがゆえに VXLAN でカプセル化されたパケットの IP フラグメントが行なわれずに、仮想 L2 ネットワークにとっては疎通できない状態になってしまいます1

そこで、skbignore_df フラグを強制的に立たせることで VXLAN でカプセル化されたパケットの IP フラグメントを許容するようにします。

パッチとして skb->ignore_df = 1 を適用しやすかったのは udp_tunnel6_xmit_skb() の直前でした。これは OpenVPN における変更を参考にしました。

パッチの結果

  • 環境
    • fd11::1, fd11::2:アンダーレイネットワーク
    • fd11:bbbb::1, fd11:bbbb::2, 192.168.200.1, 192.168.200.2:オーバーレイネットワーク
  • ping 192.168.200.2 -s 1472 -M do
  • ping fd11:bbbb::2 -s 1452 -M do
# アンダーレイ
19:20:30.839841 52:54:00:81:14:66 > 52:54:00:19:e7:d0, ethertype IPv6 (0x86dd), length 1510: (hlim 64, next-header Fragment (44) payload length: 1456) fd11::1 > fd11::2: frag (0xc71de4d7:0|1448) 49750 > 4789: VXLAN, flags [I] (0x08), vni 100
52:54:00:35:59:c7 > 52:54:00:d7:36:c7, ethertype IPv4 (0x0800), length 1432: truncated-ip - 82 bytes missing! (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto ICMP (1), length 1500)
    192.168.200.1 > 192.168.200.2: ICMP echo request, id 48932, seq 1, length 1480
19:20:30.839900 52:54:00:81:14:66 > 52:54:00:19:e7:d0, ethertype IPv6 (0x86dd), length 144: (hlim 64, next-header Fragment (44) payload length: 90) fd11::1 > fd11::2: frag (0xc71de4d7:1448|82)
19:20:30.842318 52:54:00:19:e7:d0 > 52:54:00:81:14:66, ethertype IPv6 (0x86dd), length 1510: (hlim 64, next-header Fragment (44) payload length: 1456) fd11::2 > fd11::1: frag (0x13847c10:0|1448) 38112 > 4789: VXLAN, flags [I] (0x08), vni 100
52:54:00:d7:36:c7 > 52:54:00:35:59:c7, ethertype IPv4 (0x0800), length 1432: truncated-ip - 82 bytes missing! (tos 0x0, ttl 64, id 21474, offset 0, flags [none], proto ICMP (1), length 1500)
    192.168.200.2 > 192.168.200.1: ICMP echo reply, id 48932, seq 1, length 1480
19:20:30.842324 52:54:00:19:e7:d0 > 52:54:00:81:14:66, ethertype IPv6 (0x86dd), length 144: (hlim 64, next-header Fragment (44) payload length: 90) fd11::2 > fd11::1: frag (0x13847c10:1448|82)

19:22:21.070664 52:54:00:81:14:66 > 52:54:00:19:e7:d0, ethertype IPv6 (0x86dd), length 1510: (hlim 64, next-header Fragment (44) payload length: 1456) fd11::1 > fd11::2: frag (0x860433f2:0|1448) 49109 > 4789: VXLAN, flags [I] (0x08), vni 100
52:54:00:35:59:c7 > 52:54:00:d7:36:c7, ethertype IPv6 (0x86dd), length 1432: truncated-ip6 - 82 bytes missing!(flowlabel 0x10727, hlim 64, next-header ICMPv6 (58) payload length: 1460) fd11:bbbb::1 > fd11:bbbb::2: ICMP6, echo request, id 8128, seq 1
19:22:21.070750 52:54:00:81:14:66 > 52:54:00:19:e7:d0, ethertype IPv6 (0x86dd), length 144: (hlim 64, next-header Fragment (44) payload length: 90) fd11::1 > fd11::2: frag (0x860433f2:1448|82)
19:22:21.071892 52:54:00:19:e7:d0 > 52:54:00:81:14:66, ethertype IPv6 (0x86dd), length 1510: (hlim 64, next-header Fragment (44) payload length: 1456) fd11::2 > fd11::1: frag (0x028fcd76:0|1448) 35577 > 4789: VXLAN, flags [I] (0x08), vni 100
52:54:00:d7:36:c7 > 52:54:00:35:59:c7, ethertype IPv6 (0x86dd), length 1432: truncated-ip6 - 82 bytes missing!(flowlabel 0xe4933, hlim 64, next-header ICMPv6 (58) payload length: 1460) fd11:bbbb::2 > fd11:bbbb::1: ICMP6, echo reply, id 8128, seq 1
19:22:21.071893 52:54:00:19:e7:d0 > 52:54:00:81:14:66, ethertype IPv6 (0x86dd), length 144: (hlim 64, next-header Fragment (44) payload length: 90) fd11::2 > fd11::1: frag (0x028fcd76:1448|82)
# オーバーレイ
19:20:30.652223 52:54:00:35:59:c7 > 52:54:00:d7:36:c7, ethertype IPv4 (0x0800), length 1514: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto ICMP (1), length 1500)
    192.168.200.1 > 192.168.200.2: ICMP echo request, id 48932, seq 1, length 1480
19:20:30.652359 52:54:00:d7:36:c7 > 52:54:00:35:59:c7, ethertype IPv4 (0x0800), length 1514: (tos 0x0, ttl 64, id 21474, offset 0, flags [none], proto ICMP (1), length 1500)
    192.168.200.2 > 192.168.200.1: ICMP echo reply, id 48932, seq 1, length 1480

19:22:20.882768 52:54:00:35:59:c7 > 52:54:00:d7:36:c7, ethertype IPv6 (0x86dd), length 1514: (flowlabel 0x10727, hlim 64, next-header ICMPv6 (58) payload length: 1460) fd11:bbbb::1 > fd11:bbbb::2: [icmp6 sum ok] ICMP6, echo request, id 8128, seq 1
19:22:20.882806 52:54:00:d7:36:c7 > 52:54:00:35:59:c7, ethertype IPv6 (0x86dd), length 1514: (flowlabel 0xe4933, hlim 64, next-header ICMPv6 (58) payload length: 1460) fd11:bbbb::2 > fd11:bbbb::1: [icmp6 sum ok] ICMP6, echo reply, id 8128, seq 1

DKMS パッケージ

パッチを適用した VXLAN 実装(以下、vxlan2 と呼ぶ)を利用するとき、毎度毎度ソースコードにパッチを当ててビルドするのは手間であるため、DKMS を利用して容易に利用できるようにしました。

以下に示す GitHub リポジトリのリリースページから deb パッケージにまとめたものを入手可能です。

https://github.com/cuteip/vxlan2-dkms/releases

複数の Kernel バージョンに対応するために、バージョンごとにソースコードのディレクトリを分離しました。 複数の Kernel バージョンに対応するために #if LINUX_VERSION_CODE >= KERNEL_VERSION(6,5,0) のように切り分ける手法があります。 しかし、今回は upstream となる Linux Kernel の VXLAN 実装へわずかなパッチを当てるだけなので、同一ソースコード内にて #if で分岐するのではなく、そもそもソースコードを分離して dkms build のビルド時に分岐するようにしました。

vxlan2 モジュールは、本家の vxlan モジュールを置き換える形になってほしいので /etc/modprobe.d/blacklist-vxlan.confblacklist vxlan を追加して vxlan モジュールがロードされないようにしました。

おわりに

Linux Kernel に実装されている VXLAN 実装にパッチを当てて VXLAN でカプセル化されたパケットの IP フラグメントを許容するようにしました。 これにより、パフォーマンスが犠牲になるものの MTU 1500 の上に MTU 1500 の仮想 L2 ネットワークを VXLAN にて構築できるようになりました。

またパッチ後のモジュール vxlan2 を容易に利用できるようにするため DKMS パッケージを作成し、 https://github.com/cuteip/vxlan2-dkms/releases から利用可能です。

付録:MTU 1500 の上に MTU 1500 の仮想 L2 ネットワークを構築する他の手段

本記事では VXLAN だけに焦点を当てましたが、他の手段について紹介します。

IPv4 GRETAP には net/ipv4/ip_gre.c#L1594 の通り ignore-df オプションがあるので ip link コマンド経由で容易に実現できます。

[no]ignore-df - enables/disables IPv4 DF
suppression on this tunnel.  Normally datagrams
that exceed the MTU will be fragmented; the
presence of the DF flag inhibits this, resulting
instead in an ICMP Unreachable (Fragmentation
Required) message.  Enabling this attribute causes
the DF flag to be ignored.

https://man7.org/linux/man-pages/man8/ip-link.8.html

一方で IPv6 GRETAP には net/ipv6/ip6_gre.c#L2204-L2226 の通り ignore-df に相当するオプションが存在しないため手軽に実現できません。


  1. IP フラグメントされた VXLAN パケットの 2 つ目以降だけ送信されるなど奇妙な動きになることがあった。 ↩︎

Built with Hugo
テーマ StackJimmy によって設計されています。