Trema/MyRoutingSwitch(2)

ちまちま修正している。

機能追加

起動時に予期せぬイベントがきたら無視する

switch_ready, features_request/reply があるんだから、そこの処理が終わるまで、指定された dpid での packet_in 処理は受け付けないことにする。

  def packet_in dpid, packet_in
    if @switch_ready[dpid]
    # 処理
    else
      # ignore packet_in until complete features request/reply
      warn "Switch:#{dpid} is not ready"
      return
    end
  end
特殊パケットは捨てる

リンクローカル(169.254.0.0/16), マルチキャストパケット(224.0.0.0/24)はスイッチ側で落とす(drop)ようにしてもらう。

  def switch_ready dpid
    send_message dpid, FeaturesRequest.new
    send_drop_flow_mod dpid, "169.254.0.0/16"
    send_drop_flow_mod dpid, "224.0.0.0/24"
  end

  def send_drop_flow_mod dpid, nw_src
    send_flow_mod_add(
      dpid,
      :idle_timeout => 0,
      :match => Match.new(
        :dl_type => 0x0800,
        :nw_src => nw_src
      )
    )
  end

コントローラ起動直後のスイッチのフローをダンプするとこんな感じになります。

root@oftest03:~# ./mod_flows_all.sh dump
------------------
dump flows: ovsbr0
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=16.319s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=169.254.0.0/16 actions=drop
cookie=0x2, duration=16.319s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=224.0.0.0/24 actions=drop
------------------
dump flows: ovsbr1
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=16.334s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=169.254.0.0/16 actions=drop
cookie=0x2, duration=16.334s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=224.0.0.0/24 actions=drop
------------------
dump flows: ovsbr2
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=16.333s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=169.254.0.0/16 actions=drop
cookie=0x2, duration=16.333s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=224.0.0.0/24 actions=drop
------------------
dump flows: ovsbr3
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=16.355s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=169.254.0.0/16 actions=drop
cookie=0x2, duration=16.355s, table=0, n_packets=0, n_bytes=0, priority=65535,ip,nw_src=224.0.0.0/24 actions=drop
root@oftest03:~#

これ、テスト中に、いったんスイッチのフロー消してもう一回やろうとか思ってsudo ovs-ofctl del-flows ovsbr0 とかやった瞬間に、169.254/16 とか 224.0.0/24 とかまで全部消えて、余計なpacket_in が上がるようになるので注意しましょう。

Proxy ARP Request

テストをやっていてコントローラの起動・停止を繰り返していると、VM(ホスト)には ARP がキャッシュされているが、コントローラ側(arp_table)には ARP 情報がない、という状況が起きる。すると、送信元 VM送信先MAC を知ってるので直接 IP Packet を投げるが、コントローラは arp_table に宛先のエントリがない(宛先がどこにいるのかわからない)ので転送できない、という状態になる。こうなると、送信元 VMARP Request 投げ直してくれるか、偶然送信先 VM が何かしらのパケットを発生させて arp_table にエントリが載るか、を待つしかなくて、通信ができるようになるまで時間がかかる。

ということで、IPv4 Packet が届いているけど arp_table 内に送信先エントリがなくて転送できない場合に、コントローラ側が ARP Request を生成して Flooding してしまうようにしてみた。

  def handle_ipv4 dpid, packet_in
    # 略
    arp_entry = @arp_table.lookup_by_ipaddr(packet_in.ipv4_daddr)
    if arp_entry
      # 略
    else
      warn "NOT FOUND path from #{packet_in.ipv4_saddr} to #{packet_in.ipv4_daddr}"
      # if not found in arp_table,
      # search where it is, using arp request flooding (proxy arp)
      interface = Interface.new(packet_in.macsa, packet_in.ipv4_saddr)
      data = create_arp_request_from interface, packet_in.ipv4_daddr
      flood_arp_request dpid, packet_in.in_port, data
    end
  end

VM に頼らずに積極的に ARP Request を投げることでレスポンスは良くなる。VM 側もパケット投げて返ってこなければいずれ ARP Request を再度投げることになるわけで、それをコントローラ側が代行するという方向。まあ、積極的に Flooding するのが本当に良いのかどうかとかは考える余地があるのかなあという気もするけど。

[追記] : L2 ネットワークなので本当は Unknown Unicast は Flooding するというのが一般的ですよね。arp で位置解決することで、Unknown Packet は落ちちゃうし。今回こういう処理をやってみたのは以下の理由からです。(書いておかないと忘れそうだから書いておきます。が…まあそんなに深い意図はナイですね。)

  • これまで simple router をベースにした改造(SimpleL3Switchとか)をやっていて、arp による位置解決、という刷り込みのようなものがあった。
  • Unknown Unicast Flooding だと、ペイロード(packet data)の転送が含まれる。ARP Request の法が軽いだろう。(トラフィック面・プログラム実装的な面で)
  • コントローラ実装やってるのでちょっと実験的(一般的なスイッチの処理ではない)方法を試してみてもイイかな、と思った。
Optimize Last Hop Flow Rule

いま、行き帰りL2/L3マッチでフローを追加してある。で、例えば ofvm01 → ofvm07 への icmp だと ovsbr3 にはこういうフローが入る。

------------------
dump flows: ovsbr3
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=9.786s, table=0, n_packets=4, n_bytes=392, priority=65535,ip,dl_src=52:54:00:00:00:01,dl_dst=52:54:00:00:00:07,nw_src=192.168.11.126,nw_dst=192.168.11.120 actions=output:4
cookie=0x2, duration=9.776s, table=0, n_packets=4, n_bytes=392, priority=65535,ip,dl_src=52:54:00:00:00:07,dl_dst=52:54:00:00:00:01,nw_src=192.168.11.120,nw_dst=192.168.11.126 actions=output:2

問題は "actions=output:4" のフロー。ofvm01 以外の他の VM からも ofvm07 への icmp とかを撃っていると、どんどんフローがふえる。

root@oftest03:~# tail -n24 ping01.txt | grep "output:4"
 cookie=0xf, duration=360.23s, table=0, n_packets=8, n_bytes=784, priority=65535,ip,dl_src=52:54:00:00:00:05,dl_dst=52:54:00:00:00:07,nw_src=192.168.11.122,nw_dst=192.168.11.120 actions output:4
 cookie=0x13, duration=306.772s, table=0, n_packets=8, n_bytes=784, priority=65535,ip,dl_src=52:54:00:00:00:02,dl_dst=52:54:00:00:00:07,nw_src=192.168.11.125,nw_dst=192.168.11.120 actions=output:4
 cookie=0x16, duration=73.108s, table=0, n_packets=5, n_bytes=490, priority=65535,ip,dl_src=52:54:00:00:00:04,dl_dst=52:54:00:00:00:07,nw_src=192.168.11.123,nw_dst=192.168.11.120 actions=output:4
 cookie=0x11, duration=330.628s, table=0, n_packets=8, n_bytes=784, priority=65535,ip,dl_src=52:54:00:00:00:03,dl_dst=52:54:00:00:00:07,nw_src=192.168.11.124,nw_dst=192.168.11.120 actions=output:4
 cookie=0x5, duration=400.489s, table=0, n_packets=8, n_bytes=784, priority=65535,ip,dl_src=52:54:00:00:00:06,dl_dst=52:54:00:00:00:07,nw_src=192.168.11.121,nw_dst=192.168.11.120 actions=output:4
 cookie=0x1, duration=1739.188s, table=0, n_packets=63, n_bytes=8812, priority=65535,ip,dl_src=52:54:00:00:00:01,dl_dst=52:54:00:00:00:07,nw_src=192.168.11.126,nw_dst=192.168.11.120 actions=output:4
root@oftest03:~#

でもこれ、最後 ovsbr3 から ofvm07 に渡す、という意味では全部同じ役割で、送信元を識別する意味がないわけだ。最後の1ホップについては送信元指定で何かやれるかというと特にないので、束ねてしまう。

  def handle_ipv4 dpid, packet_in
    # 略
      while path[now_dpid]
        next_dpid = path[now_dpid]
        link = @topology.get_link(now_dpid, next_dpid)

        puts "flow_mod: dpid:#{now_dpid}/port:#{link.port1} -> dpid:#{next_dpid}"
        flow_mod now_dpid, srcdst_match(packet_in), SendOutPort.new(link.port1)
        now_dpid = next_dpid
      end

      # last hop
      action = SendOutPort.new(goal_port)
      flow_mod goal_dpid, dst_match(packet_in), action
      packet_out goal_dpid, packet_in.data, action
    # 略
  end

  def srcdst_match packet_in
    Match.new(
      :dl_src  => packet_in.macsa,
      :dl_dst  => packet_in.macda,
      :dl_type => packet_in.eth_type,
      :nw_src  => packet_in.ipv4_saddr.to_s,
      :nw_dst  => packet_in.ipv4_daddr.to_s
    )
  end


  def dst_match packet_in
    Match.new(
      :dl_dst  => packet_in.macda,
      :dl_type => packet_in.eth_type,
      :nw_dst  => packet_in.ipv4_daddr.to_s
    )
  end

すると上にあったフローは全部一つにまとまる。

root@oftest03:~# tail -n14 ping02.txt | grep "output:4"
 cookie=0xf, duration=24.958s, table=0, n_packets=24, n_bytes=2352, priority=65535,ip,dl_dst=52:54:00:00:00:07,nw_dst=192.168.11.120 actions=output:4
root@oftest03:~#

……と書いていて気づいたけど、統計情報とか個別に取りたいとかそういうのがあれば別か。

Bug Fix

ARP Request Flooding

packet_in.in_port を除外するために

      endpoint_ports = @topology.get_endpoint_ports.dup
      if endpoint_ports
        endpoint_ports[dpid].delete(port)

みたいなコードを入れてたんですが、これ、複製(dup)されるのって Hash だけ( Hash Value として入っているリストのリファレンスまで)なので、リストそのものは複製されていない。そのため、delete 実行時にはオリジナルのデータが消えてしまうというありがちなミスを犯していました。結果として、ある程度時間がたつと、Flood 対象のポートが全部なくなって、Flooding されなくなって通信が停まるという状況になる orz

で、もう delete とか危なげなメソッド使うのやめてしまえと。

    endpoint_ports = @topology.get_endpoint_ports
    if endpoint_ports
      if endpoint_ports[dpid]
        endpoint_ports.each_pair do |each_dpid, port_numbers|
          actions = []
          port_numbers.each do |each|
            next if each_dpid == dpid and port == each

ループの中でいちいち if チェックするのが無駄かなあとは思いつつ、でも、Hash の中身を再帰的に複製していくコストもどっこいじゃねえのと思い直し、安全そうな記述にしてしまった。

おまけ

OVS上の各ブリッジにあるフロー操作が面倒なのでスクリプトを書いておく。

#!/bin/sh

USERID=`id -u`
if [ $USERID -gt 0 ]
then
    echo "You're not root"
    exit
fi

BRLIST="ovsbr0 ovsbr1 ovsbr2 ovsbr3"
case $1 in
    dump)
        for br in $BRLIST
        do
            echo "------------------"
            echo "dump flows: $br"
            ovs-ofctl dump-flows $br
        done
        ;;
    clear)
        for br in $BRLIST
        do
            echo "------------------"
            echo "clear flows: $br"
            ovs-ofctl del-flows $br
            ovs-ofctl add-flow $br 'dl_type=0x0800,nw_src=224.0.0.0/24,actions=drop'
            ovs-ofctl add-flow $br 'dl_type=0x0800,nw_src=169.254.0.0/16,actions=drop'
            ovs-ofctl dump-flows $br
        done
        ;;
    *)
        echo "usage: $0 <commands>"
        echo "    commands = dump|clear"
        ;;
esac

リンクローカルとマルチキャストを無視するためのエントリを自動的に追加するので、これでズバッとフローエントリを全クリアしても大丈夫。

一応、コントローラ側で send_flow_mod_add するときに :idle_timeout 入れたりしてるけど、デバッグ中とかだと、エントリが自動的に消えてしまうと事象を追いかけられなくなったりして面倒だったりする。作ってるときは :idle_timeout => 0 にしておいて、明示的に全クリアとかやれるようにしておくのが良いのではなかろうか。