こんにちは、技術本部 プライベートクラウドグループの中西(@whywaita)です。

今回はCA Tech JOBの制度を用いてインターンに参加して頂いた川原さんからの寄稿記事です。

以下本文です。


こんにちは、インターン生の川原です。
本稿ではインターン中に遭遇したOpenStackのOpen vSwitch Firewall Driverに関するバグについて検証と修正方法について紹介します。

症状

以下の図のようにあるVMからハイパーバイザノード外のホストに対して帯域を多く消費するようなベンチマークを行なったところ、ハイパーバイザノードで起動しているVMの通信量が以下のグラフの様になりました。
上半分がそれぞれのVMが送信した通信量、下半分が受信した通信量です。
送信量のグラフからベンチマークを実施しているVM(送信元VM)の送信量は500 Mbps程度であることがわかります。
対して、送信元VMと同じネットワークに接続しているVMの受信量はベンチマークを実施しているVMの送信量とほぼ同値です。
このことから、あるVMがハイパーバイザノード外部に送信したパケットを同じブロードキャストドメインに接続しているVMが受信していると考えられました。
また、14:00ごろにVMが3台増えるのと同時にベンチマークの送信量が悪化していることから、パケットを受信するVMが増えることで実効帯域が低下する問題があることがわかります。

Open vSwitchはbr-intを経由してVMにパケットを送信します

意図した通信以外のパケットが多く流れています

発生している現象をまとめると


あるVMがハイパーバイザノード外のホストに送信したパケットを、同じハイパーバイザノードで実行されている、かつ同じネットワークに接続しているVMが受信する

となります。この現象によって、一つのハイパーバイザノードで同じネットワークに接続しているVMが増加するほど実効帯域が低下します。

Launchpadでは https://bugs.launchpad.net/neutron/+bug/1732067 という形でチケットになっています。
はじめはパフォーマンスの問題として起票されていましたが、他のVMの通信が見えるためセキュリティの問題としても認識されています。
しかし、同じハイパーバイザノードに同じネットワークに接続するVMが起動することは大規模環境では起きづらいため重要な問題として認識されておらず、長い期間放置されていました。

今回の問題が発生した環境は以下のとおりです。

  • OpenStack Queens
    • Open vSwitch Firewall Driver
    • L3 Agentは使わず、Provider Networkのみ利用
  • Ubuntu 16.04 LTS

原因

この問題はOpen vSwitch Firewall DriverのOpen vSwitchのテーブル設計の不備によって外部ホストのMACアドレスが学習されず外部ホストへ通信が全て内部のブリッジでフラッディングされていたことによって発生していました。
フラッディングされていた原因は、内部VMから外部ホストへの通信はNORMALアクションによって実現されていますが、外部ホストから内部VMへの通信はoutputアクションによって実現されていたためです。
詳細について、自分がたどった思考の順に記述していきます。

現象を確認した際、あるVMが送信したパケットを他の全てのVMが受信していることから、一つのパケットが複製されていることがわかりました。
ネットワークにおいてパケットを複製する処理というのは数少なく、パケットがフラッディングされているかQEMUのドライバ周りのバグではないかと予想しました。
Ethernetでは宛先のアドレスとポート番号を学習することでパケットを宛先に送り届けます。
しかし、受信したパケットの宛先アドレスを学習できていない場合、パケットを破棄するわけにはいかないので受信したポート以外のすべてのポートにパケットを送信するフラッディングという処理を行います。
詳しくは https://www.infraexpert.com/study/ethernet7.html を参照してください。

自分はQEMUのバグというよりはOpenStackの実装不備によって予期しないフラッディングが発生している可能性が高いと考えたため、Open vSwitchでフラッディングしている部分を探しました。
Open vSwitch Firewall DriverにおけるOpen vSwitchのフローテーブル設計はドキュメント (https://docs.openstack.org/neutron/stein/contributor/internals/openvswitch_firewall.html) に記載されています。
このドキュメントにおけるOVSのtable 94でフラッディングを行うと記載があるため、そこが怪しいと考え実際の環境を確認したところ、以下のフローの n_packets が継続的に増加していたためベンチマークのパケットがこのフローを通っていると考えました。

cookie=0xe4fc1a92d440c578, duration=620.429s, table=94, n_packets=9254870, n_bytes=29494975449, priority=1 actions=NORMAL

NORMALというアクションはOpen vSwitchでのみ実装されているOpenFlowの拡張命令で、Open vSwitchがパケットをL2スイッチングしてくれます。
一般的なL2スイッチのように宛先のMACアドレスが学習されていない場合、フラッディングします。
実際に ovs-appctl fdb/show br-int というコマンドで学習されたMACアドレスを確認したところ、宛先として設定される外部ホストのMACアドレスが学習されていませんでした。
このことから、外部ホストのMACアドレスが学習されていない状態でNORMALアクションが呼び出されていることが他のVMにパケットが送られてしまう原因であるとわかりました。

次に、なぜ外部ホストのMACアドレスが学習されなかったのかを調査しました。
Open vSwitchのNORMALアクションは一般的なL2スイッチと同様に、あるMACアドレスが送信元として設定されたパケットをあるポートから受信した際にその組を学習します。
今回の場合、外部ホストのMACアドレスを学習していないため、外部ホストが送信元として設定されたパケットである外部ホストから内部のVMへのパケットが通る経路を調査する必要があります。
ここで、Open vSwitchにあるパケットが適用されるフローをシミュレーションする機能 (http://docs.openvswitch.org/en/latest/topics/tracing/) があることを思い出しため、それを利用しました。

まず、内部VMから外部ホストへのパケットは以下のようになります。

$ ovs-appctl ofproto/trace br-int in_port=154,tcp,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,tcp_src=80,tcp_dst=49150 --ct-next 'trk,est'
Flow: tcp,in_port=154,vlan_tci=0x0000,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=80,tp_dst=49150,tcp_flags=0

bridge("br-int")
----------------
 0. priority 0, cookie 0xd0c5c34bc886afc4
    goto_table:60
60. in_port=154, priority 100, cookie 0xd0c5c34bc886afc4
    set_field:0x9a->reg5
    set_field:0x1->reg6
    resubmit(,71)
71. ip,reg5=0x9a,in_port=154,dl_src=fa:16:3e:81:c9:ec,nw_src=10.194.228.64, priority 65, cookie 0xd0c5c34bc886afc4
    ct(table=72,zone=NXM_NX_REG6[0..15])
    drop
     -> A clone of the packet is forked to recirculate. The forked pipeline will be resumed at table 72.

Final flow: tcp,reg5=0x9a,reg6=0x1,in_port=154,vlan_tci=0x0000,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=80,tp_dst=49150,tcp_flags=0
Megaflow: recirc_id=0,ct_state=-trk,eth,tcp,in_port=154,dl_src=fa:16:3e:81:c9:ec,nw_src=10.194.228.64,nw_frag=no
Datapath actions: ct(zone=1),recirc(0xa0f55)

===============================================================================
recirc(0xa0f55) - resume conntrack with ct_state=est|trk
===============================================================================

Flow: recirc_id=0xa0f55,ct_state=est|trk,ct_zone=1,eth,tcp,reg5=0x9a,reg6=0x1,in_port=154,vlan_tci=0x0000,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=80,tp_dst=49150,tcp_flags=0

bridge("br-int")
----------------
    thaw
        Resuming from table 72
72. ct_state=+est-rel-rpl,ip,reg5=0x9a, priority 74, cookie 0xd0c5c34bc886afc4
    resubmit(,73)
73. reg5=0x9a, priority 80, cookie 0xd0c5c34bc886afc4
    resubmit(,94)
94. priority 1, cookie 0xd0c5c34bc886afc4
    NORMAL
     -> no learned MAC for destination, flooding

bridge("br-provider")
---------------------
 0. in_port=2,dl_vlan=1, priority 4, cookie 0x37f0808c63dd6325
    set_field:5466->vlan_vid
    NORMAL
     -> forwarding to learned port

Final flow: unchanged
Megaflow: recirc_id=0xa0f55,ct_state=-new+est-rel-rpl,eth,ip,in_port=154,vlan_tci=0x0000,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_frag=no
Datapath actions: push_vlan(vid=1,pcp=0),3,pop_vlan,push_vlan(vid=1370,pcp=0),2,pop_vlan,4,6,7,9,10,13,28,11,25,8

ここで、Open vSwitch Firewall Driverのパケット受信処理は

  • 受信したポート番号などの情報をレジスタに保存 (table 60)
  • conntrackテーブルの更新 (table 71)
  • conntrackテーブルを参照し、ファイアウォールの処理 (table 72, 73)
  • NORMALアクションでL2スイッチング (table 94)

というふうになっていることを確認できます。

また、table 94の処理において

94. priority 1, cookie 0xd0c5c34bc886afc4
    NORMAL
     -> no learned MAC for destination, flooding

と記述されている通り、MACアドレスを学習していない状態でNORMALアクションが実施され、フラッディングしていることを確認できます。
次に外部ホストから内部VMへの通信は以下のように処理されます。

$ ovs-appctl ofproto/trace br-int in_port=1,dl_vlan=1370,tcp,dl_dst=fa:16:3e:81:c9:ec,dl_src=fa:16:3e:83:ee:b5,nw_dst=10.194.228.64,nw_src=10.194.228.53,tcp_dst=80,tcp_src=49150 --ct-next 'trk,est'
Flow: tcp,in_port=1,dl_vlan=1370,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_src=10.194.228.53,nw_dst=10.194.228.64,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=49150,tp_dst=80,tcp_flags=0

bridge("br-int")
----------------
 0. in_port=1,dl_vlan=1370, priority 3, cookie 0xd0c5c34bc886afc4
    set_field:4097->vlan_vid
    goto_table:60
60. dl_vlan=1,dl_dst=fa:16:3e:81:c9:ec, priority 90, cookie 0xd0c5c34bc886afc4
    set_field:0x9a->reg5
    set_field:0x1->reg6
    pop_vlan
    resubmit(,81)
81. ct_state=-trk,ip,reg5=0x9a, priority 90, cookie 0xd0c5c34bc886afc4
    ct(table=82,zone=NXM_NX_REG6[0..15])
    drop
     -> A clone of the packet is forked to recirculate. The forked pipeline will be resumed at table 82.

Final flow: tcp,reg5=0x9a,reg6=0x1,in_port=1,vlan_tci=0x0000,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_src=10.194.228.53,nw_dst=10.194.228.64,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=49150,tp_dst=80,tcp_flags=0
Megaflow: recirc_id=0,ct_state=-trk,eth,tcp,in_port=1,dl_vlan=1370,dl_vlan_pcp=0,dl_dst=fa:16:3e:81:c9:ec,nw_frag=no
Datapath actions: pop_vlan,ct(zone=1),recirc(0xa5f87)

===============================================================================
recirc(0xa5f87) - resume conntrack with ct_state=est|trk
===============================================================================

Flow: recirc_id=0xa5f87,ct_state=est|trk,ct_zone=1,eth,tcp,reg5=0x9a,reg6=0x1,in_port=1,vlan_tci=0x0000,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_src=10.194.228.53,nw_dst=10.194.228.64,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=49150,tp_dst=80,tcp_flags=0

bridge("br-int")
----------------
    thaw
        Resuming from table 82
82. ct_state=+est-rel-rpl,tcp,reg5=0x9a,tp_dst=0x40/0xffc0, priority 77, cookie 0xd0c5c34bc886afc4
    output:154

Final flow: unchanged
Megaflow: recirc_id=0xa5f87,ct_state=-new+est-rel-rpl,eth,tcp,in_port=1,nw_frag=no,tp_dst=0x40/0xffc0
Datapath actions: 15

要約すると、

  • 受信したポート番号などの情報をレジスタに保存 (table 60)
  • conntrackテーブルの更新 (table 81)
  • conntrackテーブルを参照し、ファイアウォールの処理をして、パケットをoutput (table 82)

という処理を行っていることがわかります。

outputアクションとはOpenFlowの標準命令で、あるポートに対してパケットを送信するという命令です。
つまり、送受信の処理を図にまとめると以下の通りになります。
VMから外部ホストへの通信はNORMALでL2スイッチングされていますが、外部ホストからVMへの通信はoutputアクションでL2スイッチングされており、外部ホストのMACアドレスが学習されていないことがわかります。

Open vSwitchのテーブル情報は行きと帰りで別のテーブルを参照しています

まとめると、内部VMから外部ホストへの通信はNORMALアクションによって実現されているが、外部ホストから内部ホストへの通信はoutputアクションによって実現されていたため外部ホストのMACアドレスが学習されず、外部ホストへ通信が全てbr-intでフラッディングされていたためとなります。

対策

対策としては送信と受信のどちらもoutputアクションかNORMALアクションのどちらかで処理するように統一する必要があります。

前述のLaunchpadにコメントされている修正(https://review.opendev.org/#/c/666991/)はoutputアクションで統一するアプローチをとっています。
しかし、今回の環境で実験したところフラッディングは部分的にしか抑制されませんでした。
outputアクションで統一する場合、動的に変化する可能性のある外部ホストのMACアドレスを学習するにはOpenFlowの制約によってコントローラという外部プロセスが必要になります。
Open vSwitch Firewall Driverにはコントローラが存在しないためこのアプローチで解決するのは難しいと私は考えています。
そこで、NORMALアクションで統一するアプローチで以下のようなパッチを作成しました。
先程の図を更新すると以下のようになります。

NORMALで行きも帰りも処理を行います

diff --git a/neutron/agent/linux/openvswitch_firewall/firewall.py b/neutron/agent/linux/openvswitch_firewall/firewall.py
index 786a2d6a19..380d635459 100644
--- a/neutron/agent/linux/openvswitch_firewall/firewall.py
+++ b/neutron/agent/linux/openvswitch_firewall/firewall.py
@@ -987,7 +987,7 @@ class OVSFirewallDriver(firewall.FirewallDriver):
                 dl_type=constants.ETHERTYPE_IPV6,
                 nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
                 icmp_type=icmp_type,
-                actions='output:{:d}'.format(port.ofport)
+                actions="push_vlan:0x8100,mod_vlan_vid:{:d},normal".format(port.vlan_tag)
             )
 
     def _initialize_ingress(self, port):
@@ -997,7 +997,7 @@ class OVSFirewallDriver(firewall.FirewallDriver):
             priority=100,
             dl_type=constants.ETHERTYPE_ARP,
             reg_port=port.ofport,
-            actions='output:{:d}'.format(port.ofport)
+            actions="push_vlan:0x8100,mod_vlan_vid:{:d},normal".format(port.vlan_tag)
         )
 
         # Allow custom ethertypes
@@ -1010,7 +1010,7 @@ class OVSFirewallDriver(firewall.FirewallDriver):
                         priority=100,
                         dl_type=hex_ethertype,
                         reg_port=port.ofport,
-                        actions='output:{:d}'.format(port.ofport)
+                        actions="push_vlan:0x8100,mod_vlan_vid:{:d},normal".format(port.vlan_tag)
                     )
                     continue
                 except ValueError:
@@ -1033,7 +1033,7 @@ class OVSFirewallDriver(firewall.FirewallDriver):
                 nw_proto=lib_const.PROTO_NUM_UDP,
                 tp_src=src_port,
                 tp_dst=dst_port,
-                actions='output:{:d}'.format(port.ofport)
+                actions="push_vlan:0x8100,mod_vlan_vid:{:d},normal".format(port.vlan_tag)
             )
 
         # Track untracked
@@ -1083,7 +1083,7 @@ class OVSFirewallDriver(firewall.FirewallDriver):
                 ct_state=state,
                 ct_mark=ovsfw_consts.CT_MARK_NORMAL,
                 ct_zone=port.vlan_tag,
-                actions='output:{:d}'.format(port.ofport)
+                actions="push_vlan:0x8100,mod_vlan_vid:{:d},normal".format(port.vlan_tag)
             )
         self._add_flow(
             table=ovs_consts.RULES_INGRESS_TABLE,
diff --git a/neutron/agent/linux/openvswitch_firewall/rules.py b/neutron/agent/linux/openvswitch_firewall/rules.py
index 9a44f64316..999c728387 100644
--- a/neutron/agent/linux/openvswitch_firewall/rules.py
+++ b/neutron/agent/linux/openvswitch_firewall/rules.py
@@ -208,7 +208,7 @@ def populate_flow_common(direction, flow_template, port):
     """Initialize common flow fields."""
     if direction == firewall.INGRESS_DIRECTION:
         flow_template['table'] = ovs_consts.RULES_INGRESS_TABLE
-        flow_template['actions'] = "output:{:d}".format(port.ofport)
+        flow_template['actions'] = "push_vlan:0x8100,mod_vlan_vid:{:d},normal".format(port.vlan_tag)
     elif direction == n_consts.EGRESS_DIRECTION:
         flow_template['table'] = ovs_consts.RULES_EGRESS_TABLE
         # Traffic can be both ingress and egress, check that no ingress rules

実験を行ったところ、実際にパケットのフラッディングを以下のように防げていることが確認できました。
17:20頃にパッチを適用し、漏洩していたパケットが止まるのと同時にベンチマークのパフォーマンスも4倍近く向上していることがわかります。

パッチを当てたところ漏洩が止まったようなグラフ出力が起きます

先程は外部ホストから内部VMへの通信でoutputアクションが使われていましたが、フローのシミュレーションでも以下のようにNORMALアクションでL2スイッチングするようになったことを確認できます。

$ ovs-appctl ofproto/trace br-int in_port=1,dl_vlan=1370,tcp,dl_dst=fa:16:3e:81:c9:ec,dl_src=fa:16:3e:83:ee:b5,nw_dst=10.194.228.64,nw_src=10.194.228.53,tcp_dst=80,tcp_src=49150 --ct-next 'trk,est'
Flow: tcp,in_port=1,dl_vlan=1370,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_src=10.194.228.53,nw_dst=10.194.228.64,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=49150,tp_dst=80,tcp_flags=0

bridge("br-int")
----------------
 0. in_port=1,dl_vlan=1370, priority 3, cookie 0x49fb8bcea1d9e1e7
    set_field:4097->vlan_vid
    goto_table:60
60. dl_vlan=1,dl_dst=fa:16:3e:81:c9:ec, priority 90, cookie 0x49fb8bcea1d9e1e7
    set_field:0x9a->reg5
    set_field:0x1->reg6
    pop_vlan
    resubmit(,81)
81. ct_state=-trk,ip,reg5=0x9a, priority 90, cookie 0x49fb8bcea1d9e1e7
    ct(table=82,zone=NXM_NX_REG6[0..15])
    drop
     -> A clone of the packet is forked to recirculate. The forked pipeline will be resumed at table 82.

Final flow: tcp,reg5=0x9a,reg6=0x1,in_port=1,vlan_tci=0x0000,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_src=10.194.228.53,nw_dst=10.194.228.64,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=49150,tp_dst=80,tcp_flags=0
Megaflow: recirc_id=0,ct_state=-trk,eth,tcp,in_port=1,dl_vlan=1370,dl_vlan_pcp=0,dl_dst=fa:16:3e:81:c9:ec,nw_frag=no
Datapath actions: pop_vlan,ct(zone=1),recirc(0xaba7f)

===============================================================================
recirc(0xaba7f) - resume conntrack with ct_state=est|trk
===============================================================================

Flow: recirc_id=0xaba7f,ct_state=est|trk,ct_zone=1,eth,tcp,reg5=0x9a,reg6=0x1,in_port=1,vlan_tci=0x0000,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_src=10.194.228.53,nw_dst=10.194.228.64,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=49150,tp_dst=80,tcp_flags=0

bridge("br-int")
----------------
    thaw
        Resuming from table 82
82. ct_state=+est-rel-rpl,tcp,reg5=0x9a,tp_dst=0x40/0xffc0, priority 77, cookie 0x49fb8bcea1d9e1e7
    push_vlan:0x8100
    set_field:4097->vlan_vid
    NORMAL
     -> forwarding to learned port

Final flow: recirc_id=0xaba7f,ct_state=est|trk,ct_zone=1,eth,tcp,reg5=0x9a,reg6=0x1,in_port=1,dl_vlan=1,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_src=10.194.228.53,nw_dst=10.194.228.64,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=49150,tp_dst=80,tcp_flags=0
Megaflow: recirc_id=0xaba7f,ct_state=-new+est-rel-rpl,eth,tcp,in_port=1,vlan_tci=0x0000/0x1fff,dl_src=fa:16:3e:83:ee:b5,dl_dst=fa:16:3e:81:c9:ec,nw_frag=no,tp_dst=0x40/0xffc0
Datapath actions: 15

パケット処理を要約すると

  • 受信したポート番号などの情報をレジスタに保存 (table 60)
  • conntrackテーブルの更新 (table 81)
  • conntrackテーブルを参照し、ファイアウォールの処理をして、NORMALアクションでL2スイッチング (table 82)

というふうになります。
また、内部VMから外部ホストへの通信は以下のようになります。

$ ovs-appctl ofproto/trace br-int in_port=154,tcp,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,tcp_src=80,tcp_dst=49150 --ct-next 'trk,est'
Flow: tcp,in_port=154,vlan_tci=0x0000,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=80,tp_dst=49150,tcp_flags=0

bridge("br-int")
----------------
 0. priority 0, cookie 0x85fc968e12225702
    goto_table:60
60. in_port=154, priority 100, cookie 0x542eca7f133c005
    set_field:0x9a->reg5
    set_field:0x1->reg6
    resubmit(,71)
71. ip,reg5=0x9a,in_port=154,dl_src=fa:16:3e:81:c9:ec,nw_src=10.194.228.64, priority 65, cookie 0x542eca7f133c005
    ct(table=72,zone=NXM_NX_REG6[0..15])
    drop
     -> A clone of the packet is forked to recirculate. The forked pipeline will be resumed at table 72.

Final flow: tcp,reg5=0x9a,reg6=0x1,in_port=154,vlan_tci=0x0000,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=80,tp_dst=49150,tcp_flags=0
Megaflow: recirc_id=0,ct_state=-trk,eth,tcp,in_port=154,dl_src=fa:16:3e:81:c9:ec,nw_src=10.194.228.64,nw_frag=no
Datapath actions: ct(zone=1),recirc(0xabeaa)

===============================================================================
recirc(0xabeaa) - resume conntrack with ct_state=est|trk
===============================================================================

Flow: recirc_id=0xabeaa,ct_state=est|trk,ct_zone=1,eth,tcp,reg5=0x9a,reg6=0x1,in_port=154,vlan_tci=0x0000,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_src=10.194.228.64,nw_dst=10.194.228.53,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=80,tp_dst=49150,tcp_flags=0

bridge("br-int")
----------------
    thaw
        Resuming from table 72
72. ct_state=+est-rel-rpl,ip,reg5=0x9a, priority 74, cookie 0x542eca7f133c005
    resubmit(,73)
73. reg5=0x9a, priority 80, cookie 0x542eca7f133c005
    resubmit(,94)
94. priority 1, cookie 0x85fc968e12225702
    NORMAL
     -> forwarding to learned port

bridge("br-provider")
---------------------
 0. in_port=2,dl_vlan=1, priority 4, cookie 0xc15917690554ff00
    set_field:5466->vlan_vid
    NORMAL
     -> forwarding to learned port

Final flow: unchanged
Megaflow: recirc_id=0xabeaa,ct_state=-new+est-rel-rpl,eth,ip,in_port=154,vlan_tci=0x0000/0x1fff,dl_src=fa:16:3e:81:c9:ec,dl_dst=fa:16:3e:83:ee:b5,nw_frag=no
Datapath actions: push_vlan(vid=1370,pcp=0),2

ここでは、以下のようにMACアドレスが学習されたことでフラッディングしなくなったことを確認できます。

94. priority 1, cookie 0x85fc968e12225702
    NORMAL
     -> forwarding to learned port

まとめ

本稿ではOpen vSwitch Firewall Driverにおける「他のVMのパケットを受信する」というバグについて紹介しました。
原因はパケットの送受信で二つの異なった手法で仮想ネットワークを構築していたため、外部ホストのMACアドレス学習が行われずVMから外部ホストへの通信がフラッディングされていたためでした。
そのため、送受信双方を一つの手法でL2スイッチングするように修正することで問題が解決したことを確認しました。

このバグは同じハイパーバイザノードに同じネットワークのVMが起動しないと発生しないことに加えて、広帯域の通信を行わない限り気づくことすら難しいです。
このような高負荷環境でのみ顕在化するバグを修正できたことはインターンシップならではの体験でした。
メンターの皆さんを始め、インターンシップ中にお世話になった皆さま本当にありがとうございました。

今後は、この修正案をOpenStackに報告しようと考えています。
すでに修正案があるためマージされるかはわかりませんがOSSへの貢献だと思ってがんばります。