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 に宛先のエントリがない(宛先がどこにいるのかわからない)ので転送できない、という状態になる。こうなると、送信元 VM が ARP 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 にしておいて、明示的に全クリアとかやれるようにしておくのが良いのではなかろうか。