めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Comparison between OVS Plugin with GRE Tunnel and Midonet with GRE Tunnel

俺メモです。。。。Midokuraさんの了承を得たので、堂々と書きます(笑)
あとで整理してまとめ直します。

整理して資料にしました! ⇒ 完全分散エッジ処理で実現するNeutron仮想ネットワーク

OVS PluginでGRE Tunnelを使う場合

・Port based tunnelで、OVS(br-tun)上に対向のコンピュートノードごとにトンネル接続用ポート(トンネルポート)を用意。
・コンピュートノード内部では、内部VLANで仮想L2スイッチ(サブネット)を分離。
・物理ネットワークにパケットを出すタイミングで、内部VLAN IDをTunnel IDにマッピングして、適切なトンネルポートにフォワード。
・受信側は、Tunnel IDを内部VLAN IDに逆変換。

簡単に言うと、仮想L2スイッチごとに、(Tunnel IDで識別される)トンネルを用意する方式。

(参考資料)
http://assafmuller.com/2013/10/14/gre-tunnels-in-openstack-neutron/

MidonetでGRE Tunnelを使う場合

・Flow based tunnelで、1つのトンネルポートで複数の対向ノードと通信(パケットをトンネルポートにフォワードするタイミングで、「Tunnel ID, src IP, dst IP」の3つ組を指定する方式)
・VMがOVSにパケットを入れたタイミングで、Agentが仮想NWトポロジーを全部計算して、宛先のVM(宛先のコンピュートノードと宛先のVMが接続したOVSポート番号)をその場で決定。
・宛先OVSポート番号をTunnel IDにマッピングして、加えて、「src IP=自分のIP、dst IP=宛先コンピュートノードのIP」を指定してトンネルポートにフォーワード。
・受信側は、Tunnel IDをOVSポート番号に変換して、該当ポートにパケットを転送。

簡単に言うと、宛先VMごとに、(Tunnel IDで識別される)トンネルを用意する方式。

「VMごとにトンネルを用意」というと、膨大な数のトンネルで大変になると思うかも知れませんが、GREとしてのトンネルセッションは、あくまで、コンピュートノード間で1本で、その中を通るGREパケットのヘッダに入る「トンネルID」で識別しているだけなので、大丈夫です。というか、GREはステートレスなので、そもそもセッションという概念はありませんが。

(参考資料)
Introduction to GRE Tunneling – Theory
Flow Based Tunneling for Open vSwitch

MidonetのOVS flowtableの様子

node01:192.168.40.7 ----- 192.168.40.8:node02 の2ノード構成
vm01:192.168.1.4 192.168.1.6:vm02

それぞれのノードでDatapath(OVS)を初期化

[node01]# mm-dpctl --delete-dp midonet; service midolman restart
[node02]# mm-dpctl --delete-dp midonet; service midolman restart

初期化直後のOVS(midonet)の状態

[node01]# mm-dpctl --show-dp midonet
Datapath name   : midonet
Datapath index : 10
Datapath Stats:
  Flows :0
  Hits  :0
  Lost  :0
  Misses:0
Port #0 "midonet"  Internal  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #1 "tngre-overlay"  Gre  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #2 "tnvxlan-overlay"  VXLan  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #3 "tnvxlan-vtep"  VXLan  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #4 "tap81362f30-43"  NetDev  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}

[node01]# mm-dpctl --dump-dp midonet
0 flows
[node02]# mm-dpctl --list-dps
Found 1 datapaths:
        midonet

[node02]# mm-dpctl --show-dp midonet
Datapath name   : midonet
Datapath index : 10
Datapath Stats:
  Flows :0
  Hits  :0
  Lost  :0
  Misses:0
Port #0 "midonet"  Internal  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #1 "tngre-overlay"  Gre  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #2 "tnvxlan-overlay"  VXLan  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #3 "tnvxlan-vtep"  VXLan  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}
Port #4 "tap39fa3d97-29"  NetDev  Stats{rxPackets=0, txPackets=0, rxBytes=0, txBytes=0, rxErrors=0, txErrors=0, rxDropped=0, txDropped=0}

[node02]# mm-dpctl --dump-dp midonet
0 flows

簡単に言うと、どちらのノードにも「midonet」というOVSが1個あって、Port#4にVMが刺さっていて、Port#1がGREでトンネルするためのポート。Flowtableはまだどちらも空っぽの状態。

ここで、node01上のVM(192.168.1.4)から、node02上のVM(192.168.1.6)にSSH接続します。その後のFlowtableを見ると、次のように変化します。

まず、送り元のnode01で見るとこんな感じ。

[node01]# mm-dpctl --dump-dp midonet
2 flows
  Flow:
    match keys:
      FlowKeyTunnel{tun_id=11, ipv4_src=192.168.40.8, ipv4_dst=192.168.40.7, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}
      FlowKeyInPort{portNo=1}
      FlowKeyEthernet{eth_src=fa:16:3e:8d:72:ce, eth_dst=fa:16:3e:b6:2e:ec}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.6, ipv4_dst=192.168.1.4, ipv4_proto=6, ipv4_tos=16, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=22, tcp_dst=49580}
    actions:
      FlowActionOutput{portNumber=4}
    stats: FlowStats{n_packets=7, n_bytes=1376}
    tcpFlags: PSH | ACK
    lastUsedTime: 318959933
  Flow:
    match keys:
      FlowKeyInPort{portNo=4}
      FlowKeyEthernet{eth_src=fa:16:3e:b6:2e:ec, eth_dst=fa:16:3e:8d:72:ce}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.4, ipv4_dst=192.168.1.6, ipv4_proto=6, ipv4_tos=16, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=49580, tcp_dst=22}
    actions:
      FlowActionSetKey{flowKey=FlowKeyTunnel{tun_id=13, ipv4_src=192.168.40.7, ipv4_dst=192.168.40.8, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}}
      FlowActionOutput{portNumber=1}
    stats: FlowStats{n_packets=2, n_bytes=276}
    tcpFlags: PSH | ACK
    lastUsedTime: 318959973

同じく、受け側のnode02はこんな感じ

[node02]# mm-dpctl --dump-dp midonet
4 flows
  Flow:
    match keys:
      FlowKeyTunnel{tun_id=4, ipv4_src=192.168.40.7, ipv4_dst=192.168.40.8, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}
      FlowKeyInPort{portNo=1}
      FlowKeyEthernet{eth_src=fa:16:3e:b6:2e:ec, eth_dst=fa:16:3e:8d:72:ce}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.4, ipv4_dst=192.168.1.6, ipv4_proto=6, ipv4_tos=0, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=49580, tcp_dst=22}
    actions:
      FlowActionOutput{portNumber=4}
  Flow:
    match keys:
      FlowKeyTunnel{tun_id=13, ipv4_src=192.168.40.7, ipv4_dst=192.168.40.8, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}
      FlowKeyInPort{portNo=1}
      FlowKeyEthernet{eth_src=fa:16:3e:b6:2e:ec, eth_dst=fa:16:3e:8d:72:ce}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.4, ipv4_dst=192.168.1.6, ipv4_proto=6, ipv4_tos=0, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=49580, tcp_dst=22}
    actions:
      FlowActionOutput{portNumber=4}
  Flow:
    match keys:
      FlowKeyInPort{portNo=4}
      FlowKeyEthernet{eth_src=fa:16:3e:8d:72:ce, eth_dst=fa:16:3e:b6:2e:ec}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.6, ipv4_dst=192.168.1.4, ipv4_proto=6, ipv4_tos=16, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=22, tcp_dst=49580}
    actions:
      FlowActionSetKey{flowKey=FlowKeyTunnel{tun_id=11, ipv4_src=192.168.40.8, ipv4_dst=192.168.40.7, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}}
      FlowActionOutput{portNumber=1}
    stats: FlowStats{n_packets=6, n_bytes=1310}
    tcpFlags: PSH | ACK
    lastUsedTime: 318910509
  Flow:
    match keys:
      FlowKeyTunnel{tun_id=13, ipv4_src=192.168.40.7, ipv4_dst=192.168.40.8, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}
      FlowKeyInPort{portNo=1}
      FlowKeyEthernet{eth_src=fa:16:3e:b6:2e:ec, eth_dst=fa:16:3e:8d:72:ce}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.4, ipv4_dst=192.168.1.6, ipv4_proto=6, ipv4_tos=16, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=49580, tcp_dst=22}
    actions:
      FlowActionOutput{portNumber=4}
    stats: FlowStats{n_packets=5, n_bytes=890}
    tcpFlags: PSH | ACK
    lastUsedTime: 318910549

ポイントとなるフローを見ておきます。まず、node01の次のフローは、vm01(portNo=4)から来たSSHパケットを「tun_id=13」をセットしてトンネルポートに転送します。

  Flow:
    match keys:
      FlowKeyInPort{portNo=4}
      FlowKeyEthernet{eth_src=fa:16:3e:b6:2e:ec, eth_dst=fa:16:3e:8d:72:ce}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.4, ipv4_dst=192.168.1.6, ipv4_proto=6, ipv4_tos=16, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=49580, tcp_dst=22}
    actions:
      FlowActionSetKey{flowKey=FlowKeyTunnel{tun_id=13, ipv4_src=192.168.40.7, ipv4_dst=192.168.40.8, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}}
      FlowActionOutput{portNumber=1}
    stats: FlowStats{n_packets=2, n_bytes=276}
    tcpFlags: PSH | ACK
    lastUsedTime: 318959973

一方、このパケットを受けたnode02は、次のフローでこのパケットを処理します。「tun_id=13」で受けたvm02宛のSSHパケットを、vm02が接続されたPort#4に直接に転送しています。

  Flow:
    match keys:
      FlowKeyTunnel{tun_id=13, ipv4_src=192.168.40.7, ipv4_dst=192.168.40.8, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}
      FlowKeyInPort{portNo=1}
      FlowKeyEthernet{eth_src=fa:16:3e:b6:2e:ec, eth_dst=fa:16:3e:8d:72:ce}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.1.4, ipv4_dst=192.168.1.6, ipv4_proto=6, ipv4_tos=0, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=49580, tcp_dst=22}
    actions:
      FlowActionOutput{portNumber=4}

vm02からvm01への返答パケットも同じように処理されます。node02の方で「tun_id=11」にパケットを送って、node01の方で、「tun_id=11」で受けたパケットをvm01のPort#4に転送します。

原理的には、tun_idで転送先VM(ポート)は識別されているので、それ以外の条件は無視して、tun_id=11のパケットは全部Port#4に転送するフローを用いることも可能ですが、そのようにはなっていません。きちんと、現在行われている通信を判断して、そのシーケンスに属するパケットだけを転送するようにしています。(外部からtun_idを強制セットした変なパケットを送り込まれるのを防止するためのような気がします。)

NATに関する考察

パケット送出元のAgentがすべての仮想ネットワークトポロジーを把握して、最終到達点のVMを決定するという、ある意味シンプルな仕組みですが、仮想ネットワークトポロジーの中でIPマスカレードが入る場合を考えると、ちょっと面白いことになります。(こっからは、みなさんがOpenFlowの仕組みをしっている前提で説明します。)

仮想ネットワーク内部にルーターがあって、そこでIPマスカレードして(送信元IPをルーターの代表IPに変換して)、相手の(たとえば、node02)のVMに送ります。受け手のVMからの返信パケットは、宛先が代表IPになっているので、node02のAgentは、仮想ネットワーク内部のパケットの動きを計算する中で、NATの逆変換をして、本当の宛先VMを見つけて、そこにパケットを送りつけるフローエントリーを生成します。この際、エントリーのパケットマッチ条件として、宛先ポートを明示することが重要です。なぜなら、複数のVMが同一の代表IPでnode02のVMに接続に来た場合、返送先のポート番号によって、実際に返送するべきVMが決まるからです。宛先ポートを明示せずにパケットマッチしてしまうと、返送先のVMが決まらなくて困ります。

ところが

同じことをpingの場合に考えるとうまくいきません。pingの場合は、ICMPヘッダーのIdentifierで、個別の通信シーケンスが区別されます。しかしながら、OpenFlowのフローエントリーでは、このIdentifierでマッチするという機能が含まれていません。そのため、pingのマスカレード処理については、フローエントリーで表現することができず、OpenFlowコントローラー側でマスカレード変換のステート情報を管理して、個々のパケットごとにPacket-Inして処理する必要があります。

Midonetでもこの問題はあるようで、pingだけは特別に、フローテーブルに落とさずにAgentが個々のパケットを直接に処理して、宛先VMに転送しているようです。

外部ネットワークとの通信

外部ネットワークとの通信は、GWサーバーを用意します。下図ではGWサーバー1台ですが、実際には複数台数でロードバランスします。

[8.8.8.8] ---- [PhysRouter] ---- [Midonet GW Server] ------- [node01]
                                                        |
                                                        ---- [node02]

node01上のVMから8.8.8.8にSSHする場合を考えます。論理的にはテナントルータでNATしたうえで、外部の8.8.8.8に到達します。

実際の動きとしては、まず、node01に、NATしたうえでGWサーバーに転送するエントリーができます。

  Flow:
    match keys:
      FlowKeyInPort{portNo=4}
      FlowKeyEthernet{eth_src=fa:16:3e:11:7c:2c, eth_dst=02:65:b4:09:aa:6a}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=192.168.3.5, ipv4_dst=8.8.8.8, ipv4_proto=6, ipv4_tos=16, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=43771, tcp_dst=22}
    actions:
      FlowActionSetKey{flowKey=FlowKeyEthernet{eth_src=fa:16:3e:9a:76:2c, eth_dst=fa:16:3e:fa:78:6a}}
      FlowActionSetKey{flowKey=FlowKeyIPv4{ipv4_src=172.16.128.3, ipv4_dst=8.8.8.8, ipv4_proto=6, ipv4_tos=0, ipv4_ttl=62, ipv4_frag=0}}
      FlowActionSetKey{flowKey=FlowKeyTunnel{tun_id=16, ipv4_src=192.168.30.7, ipv4_dst=192.168.30.10, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}}
      FlowActionOutput{portNumber=1}

一方、GWサーバーでは、このパケットを外部に転送するエントリーができます。

  Flow:
    match keys:
      FlowKeyTunnel{tun_id=16, ipv4_src=192.168.30.7, ipv4_dst=192.168.30.10, tun_flag=0, ipv4_tos=0, ipv4_ttl=-1}
      FlowKeyInPort{portNo=1}
      FlowKeyEthernet{eth_src=fa:16:3e:9a:76:2c, eth_dst=fa:16:3e:fa:78:6a}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=172.16.128.3, ipv4_dst=8.8.8.8, ipv4_proto=6, ipv4_tos=0, ipv4_ttl=62, ipv4_frag=0}
      FlowKeyTCP{tcp_src=43771, tcp_dst=22}
    actions:
      FlowActionOutput{portNumber=4}

8.8.8.8からの返答パケットについては、GWサーバーで逆NAT変換してから、node01に転送します。

  Flow:
    match keys:
      FlowKeyInPort{portNo=4}
      FlowKeyEthernet{eth_src=fa:16:3e:ec:e7:da, eth_dst=fa:16:3e:2e:54:a1}
      FlowKeyEtherType{etherType=0x800}
      FlowKeyIPv4{ipv4_src=8.8.8.8, ipv4_dst=172.16.128.3, ipv4_proto=6, ipv4_tos=0, ipv4_ttl=64, ipv4_frag=0}
      FlowKeyTCP{tcp_src=22, tcp_dst=43771}
    actions:
      FlowActionSetKey{flowKey=FlowKeyEthernet{eth_src=02:65:b4:09:aa:6a, eth_dst=fa:16:3e:11:7c:2c}}
      FlowActionSetKey{flowKey=FlowKeyIPv4{ipv4_src=8.8.8.8, ipv4_dst=192.168.3.5, ipv4_proto=6, ipv4_tos=0, ipv4_ttl=6
2, ipv4_frag=0}}
      FlowActionSetKey{flowKey=FlowKeyTunnel{tun_id=5, ipv4_src=192.168.30.11, ipv4_dst=192.168.30.7, tun_flag=0, ipv4_
tos=0, ipv4_ttl=-1}}
      FlowActionOutput{portNumber=1}
    stats: FlowStats{n_packets=5, n_bytes=1875}
    tcpFlags: PSH | ACK
    lastUsedTime: 290859609

その後、node01側でこのパケットをVMに返します。NATの変換と逆変換が異なるノードで行われているということは、各ノードのagentがNATのステート情報をきちんと共有していることを意味します。

node01とゲートウェイの間のGREパケットをtcpdumpでみると、次のようになります。NATの変換・逆変換が上記のタイミングで行われていることがわかります。

02:19:14.257684 IP 192.168.30.7 > 192.168.30.11: GREv0, key=0x12, length 82: IP 172.16.128.3.43771 > 8.8.8.8.ssh: Flags [S], seq 2970049644, win 14600, options [mss 1460,sackOK,TS val 72390453 ecr 0,nop,wscale 2], length 0
02:19:14.281226 IP 192.168.30.11 > 192.168.30.7: GREv0, key=0x5, length 82: IP 8.8.8.8.ssh > 192.168.3.5.43771: Flags [S.], seq 3583501603, ack 2970049645, win 14020, options [mss 1414,sackOK,TS val 28875923 ecr 72390453,nop,wscale 7], length 0