Hatena::ブログ(Diary)

# cat /var/log/stereocat | tail -n3 RSSフィード

2013-05-18

Trema/MyRoutingSwitch(4), テスト関連メモ

引き続き routing switch 周りでいじってる。先週〜今週でいじったことのメモ。

Unknown Unicastの取り扱い

Trema/MyRoutingSwitch(2) にも書いたけど、L2スイッチとして動作する割には Unknown Unicast の動作をイレギュラーな形 (proxy arp) にしてありました。試してみたかったので。ただ、動作として標準的なものを作っておくかと思ったのと、単純な Unknown Unicast Flooding にした方が実装上シンプルになるので、いったん Flooding するように変更してみました。

テストコード

テストコードもらったので、それを元にいっくつかテストを入れています。

  • test with single switch
    • スイッチひとつだけ
    • トポロジ情報がない(他のスイッチとつながっていない、standalone な状況にあるスイッチ、もしくはsingle switch environment)のテスト
  • test with multiple switch
    • スイッチ6個の環境
    • 最短経路検索のテスト
    • フロー情報のチェック(dump_flows)は経路として確定できるところについてだけ書いています。
    • 等コストになる場合(選択可能な経路が複数ある場合)も、テストは作り込めそうだけどまだやれてない。

Unknown Unicastの取り扱いとphostの動作

'trema send_packets' で phost 間のパケット送受信を試しているのだけど、このとき

  • 送信 phost は直接 unicast ipv4 を送信する (ARP Request を生成しない)
  • 受信 phost は受信した unicast ipv4 についてリプライを返さない。(受信カウントする)
  • 受信 phost は arp request に対してリプライを返す。

という動作をしています。なので、テスト実行時の arp_table の学習の仕方が、unknown unicast の取り扱いによってちょっと変わる。それによってテストの作り方もちょっと変わる。

proxy arp で処理する場合
       ipv4 packet         arp request[flood]
 host1 ==========> switch1 ===========> host2
                           arp reply
                           <===========

controller の arp_table が空の状況で host1 が ipv4 packet を送信すると、コントローラ側は宛先がどのポートにいるか知らないのでそのままでは送れません(unknown unicast)。そのため、何かしらの flooding を行って、宛先からの返答を待ちます。返答によって宛先がどこに接続されているかを把握します。

proxy arp を使っている場合、unknown の packet を送ることはあきらめて宛先の位置の確認だけします*1。phost (host2) はコントローラが生成した arp request に応答してくれるので、初期状態で arp_table が空でも送信元・先がどのポートにつながっているかの情報を得られます。(ipv4 packet, arp reply がどのポートに入ったかを arp_table に記録する。)

phost が arp request については reply を返してくれるので片方向 (host1 → host2) のパケット送信で arp_table には送信元・先の情報が載ります。送信元・先の情報が arp_table に乗っている状態で、次に何らかのパケット (ipv4 unicast) が入ってくると、flow_mod してスイッチに flow entry を入れてやることができます。

floodingで処理する場合
       ipv4 packet         ipv4 packet[flood]
 host1 ==========> switch1 ===========> host2

Unknown unicast packet の flooding で処理するようすると、phost(host2) が ipv4 packet に対して応答返さない(受信はカウントする)ので、host2 がどこにいるのかをコントローラは把握できません。コントローラが送信元・先がどこにいるかを把握するには、host1/host2 双方が何らかのパケットを出してコントローラに位置を教える必要があります。

そのため、テストでは逆方向の send_packets も必要になります。

       ipv4 packet         ipv4 packet
 host1 <========== switch1 <========== host2

逆方向にパケットを送ると、送信先(host1)は arp_table に載っているので、host1 宛て (host1 へ outport する) flow entry が設定されます (floodではない)。

まとめ

  • テストを増やしました
    • 書き方は… trema apps にあったものとかを元に作っているのでコピペ成分多め。どう書くべきかというのがはっきりわかった上で書いているわけじゃないのが微妙…。その辺はもうちょっと勉強しないとなあ…
  • phost 周りの動作をまとめました
    • OVS/KVM 環境で実際にやるのと、trema test framework 上でやるのと、動作に違いがあるのでまとめておいた。基本的な話なんだけど、どうも頭が上手く切り替わらないので。
    • phost が arp なしで ipv4 unicast を生成するのはこれはこれで良い。
      • unknown unicast のテストとか一発でできる。
      • 片方向ずつ、動作テストができる。(OVS/KVMによるテスト環境だと、VM間パケット送信で自動的に reply が返ってしまうので、1パケット送信で 2 方向の処理が走って、これが結構面倒。multiple switch なテストでも行き/返り個別に処理を追いかけられる方やりやすい状況がある。)
    • あとは、明示的に phost から arp request を生成する、あるいは phost が ipv4 unicast に対して reply を返す、という動作が指定できるとやりやすいのかなあと思ったりした。

*1:host1/host2 が別なネットワークセグメントにいる場合はこういう処理がまわります。L2 でこういうのを作ったのは、Simple Router とか L3 処理のあるコントローラをいじったりしていたのでそれを引きずっています。

2013-05-02

Trema/MyRoutingSwitch(3), ダイクストラ法デバッグ

えーと。案の定というかなんというか。最短経路検索の実装がおかしかったですね。

下図のようなトポロジでデバッグプリント出して L2 routing の処理を追いかけてみました。非対称なトポロジにしないと動作テストとしてはダメだよねやっぱり。

f:id:stereocat:20130502231729p:image:w600

ofvm01 (192.168.11.126) から ofvm07 (192.168.11.120) へ ping を撃つときに出力されるデバッグプリントを追って確認してみます。(とりあえずは個人的なメモとして。)

[追記 2013-05-03] わりと投げやりな感じで書いたので + またコードいじったので全体的に内容見直し。

Step.1

以下「ノード」はスイッチ(OFS)の事だと思ってください。あと、リンクコストは固定で全部 10 にしてあります。(LLDPで動的にリンク情報を取得しているので、リンクコストとして適切な値が取れていない。)

IPv4: dpid:1, port:5, 192.168.11.126->192.168.11.120
[get_path], start:7, goal:1
---------------:--------------------------
remains        : [5, 6, 1, 2, 3, 4]
dist table     : {5 => 99999, 6 => 99999, 1 => 99999, 7 => 0, 2 => 99999, 3 => 99999, 4 => 99999, }
pred table     : {5 => , 6 => , 1 => , 7 => , 2 => , 3 => , 4 => , }
base (dist)    : 7 (0)
neighbors      : [6, 4]
next dist tbl  : {5 => 99999, 6 => 10, 1 => 99999, 7 => 0, 2 => 99999, 3 => 99999, 4 => 10, }

最終的には dpid:0x1 (ovsbr0) → dpid:0x7 (ovsbr6) への経路を求めるわけですが、経路探索上は逆順になっています。探索していく中で、あるノード(ここではOFS)の一つ前のホップがわかるようになっているからで、ゴール→スタートまで探索がおわると、スタート→ゴールの経路がたどれるような結果が得られます。これが pred で、pred[dpid] で dpid の一つ前にたどるノードの dpid が得られます。初期状態ではまだ何も確定していないので中身は空っぽです。

その他使っているデータはこんな感じ

  • base: 現在基点としているノードの dpid
  • dist: 各ノードのスタートからのコスト(距離) (dist[dpid] #=> 基点からdpidまでの距離)
  • remains: 未確定のノードのリスト
  • neighbors: 基点としているノードに隣接しているノードのリスト(dpidのリスト)
  • next dist: 基点隣接ノードのコスト更新後の dist

処理

  • 最初は初期状態なので、基点 = スタート(dpid:0x7), コストは初期値(99999は定数として決めてあります)。
  • 未確定(remains)ノードのうち、距離(dist)が最小のノードを基点として選択 (Step.1ではstart:0x7)
    • 選択されたら remains からは削除
    • 本当は priority queue から取り出す、というような操作だけど priority queue クラス作るのが面倒だったので、距離最小ノード選択だけ実装してる。
  • 基点(0x7)に隣接するノード: 0x4, 0x6
  • 隣接ノードのコストを更新
    • コストは、今テーブルに入っている値より今の基点(base_dpid)を元にしたコストの方が小さければテーブルの値を更新します。更新するときに、pred に基点 dpid を入れておきます。(ノードのコストがどのノードを基点とした値で更新されたのか→どのノードが「ひとつ前」なのか、を記録しておく)
stepremainsbaseneighborsdistance
10x1,0x2,0x3,0x4,0x5,0x60x70x4,0x60x4(10),0x6(10)

Step.2

---------------:--------------------------
remains        : [5, 6, 1, 2, 3]
dist table     : {5 => 99999, 6 => 10, 1 => 99999, 7 => 0, 2 => 99999, 3 => 99999, 4 => 10, }
pred table     : {5 => , 6 => 7, 1 => , 7 => , 2 => , 3 => , 4 => 7, }
base (dist)    : 4 (10)
neighbors      : [3, 6, 7]
next dist tbl  : {5 => 99999, 6 => 10, 1 => 99999, 7 => 0, 2 => 99999, 3 => 20, 4 => 10, }
  • 未確定のうち距離が最小: 0x4 → 基点
  • 基点の隣接ノード: 0x3, 0x6, 0x7(0x7は確定済みなのでパス)
  • 隣接ノードのコストを更新
stepremainsbaseneighborsdistance
20x1,0x2,0x3,0x5,0x60x40x3.0x6.0x70x3(20).0x6(10)

Step.3-6

---------------:--------------------------
remains        : [5, 1, 2, 3]
dist table     : {5 => 99999, 6 => 10, 1 => 99999, 7 => 0, 2 => 99999, 3 => 20, 4 => 10, }
pred table     : {5 => , 6 => 7, 1 => , 7 => , 2 => , 3 => 4, 4 => 7, }
base (dist)    : 6 (10)
neighbors      : [4, 5, 7]
next dist tbl  : {5 => 20, 6 => 10, 1 => 99999, 7 => 0, 2 => 99999, 3 => 20, 4 => 10, }
---------------:--------------------------
remains        : [1, 2, 3]
dist table     : {5 => 20, 6 => 10, 1 => 99999, 7 => 0, 2 => 99999, 3 => 20, 4 => 10, }
pred table     : {5 => 6, 6 => 7, 1 => , 7 => , 2 => , 3 => 4, 4 => 7, }
base (dist)    : 5 (20)
neighbors      : [6, 2]
next dist tbl  : {5 => 20, 6 => 10, 1 => 99999, 7 => 0, 2 => 30, 3 => 20, 4 => 10, }
---------------:--------------------------
remains        : [1, 2]
dist table     : {5 => 20, 6 => 10, 1 => 99999, 7 => 0, 2 => 30, 3 => 20, 4 => 10, }
pred table     : {5 => 6, 6 => 7, 1 => , 7 => , 2 => 5, 3 => 4, 4 => 7, }
base (dist)    : 3 (20)
neighbors      : [1, 2, 4]
next dist tbl  : {5 => 20, 6 => 10, 1 => 30, 7 => 0, 2 => 30, 3 => 20, 4 => 10, }
---------------:--------------------------
remains        : [2]
dist table     : {5 => 20, 6 => 10, 1 => 30, 7 => 0, 2 => 30, 3 => 20, 4 => 10, }
pred table     : {5 => 6, 6 => 7, 1 => 3, 7 => , 2 => 5, 3 => 4, 4 => 7, }
base (dist)    : 1 (30)
neighbors      : [3, 2]
next dist tbl  : {5 => 20, 6 => 10, 1 => 30, 7 => 0, 2 => 30, 3 => 20, 4 => 10, }
stepremainsbaseneighborsdistance
30x1,0x2,0x3,0x50x60x4.0x5.0x70x5(20)
40x1,0x2,0x30x50x2,0x60x2(30)
50x1,0x20x30x1,0x2,0x40x1(30),0x2(30)
60x20x10x2,0x30x2(30)

というのを繰り返していきます。リンクコストが固定で、リンクが交差する面倒なところがないので、コストの書き換えとかほとんど起こらないですね。

Step.7

---------------:--------------------------
remains        : []
dist table     : {5 => 20, 6 => 10, 1 => 30, 7 => 0, 2 => 30, 3 => 20, 4 => 10, }
pred table     : {5 => 6, 6 => 7, 1 => 3, 7 => , 2 => 5, 3 => 4, 4 => 7, }
base (dist)    : 2 (30)
neighbors      : [1, 5, 3]
next dist tbl  : {5 => 20, 6 => 10, 1 => 30, 7 => 0, 2 => 30, 3 => 20, 4 => 10, }
flow_mod: dpid:1/port:1 -> dpid:3
flow_mod: dpid:3/port:3 -> dpid:4
flow_mod: dpid:4/port:3 -> dpid:7

未確定(remains)がなくなったら終わりです。

stepremainsbaseneighborsdistance
7-0x20x1,0x3,0x5-

おわったら pred の内容を元に、中間経路をだして flow_mod していきます。

  • dpid:1 で SendOutPort(1) すると dpid:3 に流れます。
  • dpid:3 で SendOutPort(3) すると dpid:4 に流れます。
  • dpid:4 で SendOutPort(4) すると dpid:7 に流れます。

スイッチ間のリンク情報を元にトポロジ情報をみているので、ホスト(エンドポイント)のいるポートはわかりません。最後は arp_table (FDB) を参照します。arp_table を見ると、dpid:7 の port:3 に ofvm07 がいることがわかります。

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 にしておいて、明示的に全クリアとかやれるようにしておくのが良いのではなかろうか。

2013-04-30

Trema/MyRoutingSwitch

10連休! なのでちょっとお試し。

はじめに

これまで VMwarePlayer+Ubuntu/KVM/OVSでOpenFlowテスト環境をつくる(6) で複数スイッチ環境でやるための環境セットアップはしたものの、その上でのアプリの実装とか実験とかできていなかったので、連休を利用してやってみました。いや、やろうやろうと思って trema/apps にある routing switch のコードをちょっと読んでみたりとかしていたんだけどね。yasuhito/ruby_topology でトポロジ把握ができれば routing switch は作れるだろうから、もういったん自分で作ってしまえばいいじゃないかと思ったのでした。

とか既に公開されている物もあったりするので再発明感が否めないけど、まあお勉強ということで。

つくるもの

設定

routing-switch をそのまま ruby で作ってみます。条件はこんな感じ。

という条件にしました。L2ブロードキャスト制御をスイッチ側に任せるという方法もあると思うけど(apps の routing switch のコード見てる選択できるようになってる感じ)、今回はまず基本から、ということで、単一セグメント + ブロードキャスト制御はコントローラまかせ、です。

環境

前に作った三角形の構成だと最短経路探索が意味をなさないのでせめて4点…という作りにしました。

f:id:stereocat:20130430203559p:image:w600

VM 配置とか周辺の構成はこんな感じ

f:id:stereocat:20130430203600p:image:w600

基本動作

複数のスイッチをまたいで単一L2セグメントとしての通信を実現させようとするとだいたいこんな動作が必要だろう、というのを図におこしてみました。こういうのを書いておかないと、どこで何の処理かかなきゃいけないのか、わからなくなる…。

f:id:stereocat:20130430203601p:image:w600

通信を成立させるために、いくつかの packet_in 処理が発生するので、それぞれで見ていきます。

  • (1) : ARP Request の処理
    • 要求されたアドレスが (1a)未知 (1b)既知(FDBに既にある) でFloodするかどうかが変わります。
    • とはいえ、知っていれば直接リクエストを送信、知らなければばらまいてリクエストを送信、というだけ。
  • (2) : ARP Reply の処理
    • 返答があれば要求元に転送します。
  • (3) : 未知 IPv4 Packet の処理
    • ARP のやりとりが完了していれば通信が行われますが、スイッチ側に対応するフローがなければpacket_in が発生します。この場合、複数のスイッチを経由してどうにかして送信先にパケットを届けてもらう必要があるので、経路を検索し、経路中のスイッチのそのためのフローを書き込みます。
    • フローには行きと帰りの2方向があります。1回の packet_in に対して両方向書き込むこともできると思いますが、今回は行き/帰りでそれぞれ2回 packet_in を発生させることにします。
  • (4) : 既知 IPv4 Packet の処理
    • スイッチ側に対応するフローがあればコントローラには何も上がってきません。

実装方針

コードはここにあります。

lib/arp-table.rb
  • 上では FDB と書いていますが ARPTable で作っています。前に作った奴をそのままコピー。
  • ARPEntry のインスタンス変数に dpid を足して、[IP Address]→[datapath-id, port, hwaddr(mac)] という対応を管理できるようにしておく。
lib/topology.rb
  • get_path というメソッドで、ダイクストラ法を使って経路探索を実装。
    • リンクコストは今は全部固定値です。
  • 最終的には path[dpid] = <1hop前のdpid> という hash が得られます。
  • その他追加しているのは LinkIndex に丸投げ
    • get_endpoint_ports: 非スイッチ間になっているポート情報を返す
    • get_link: 指定された2つのスイッチ間をつなぐリンク情報を返す

上の図にあるトポロジでしか試していないので最短経路検索メソッドの信頼性については今ひとつな気がする。というか無駄が多い気がするけど…まあいいか。

lib/linkindex.rb

元のコードが LLDP でリンク情報を取ってきて Topology::links にためておくようになっているのだけど、これだけだと経路探索とかやる上で使い勝手が悪いので、リンク/ポート情報(@links, @ports) の管理をやるためのクラスを作りました、というだけ。

  • @switch_index
    • port/link 情報 (Trema::Port, Link object を管理するための Hash)
  • @switch_neighbor
    • 指定されたスイッチ(dpid)が接続しているスイッチのリストをキャッシュしておくための Hash
    • 経路探索で、スイッチの隣接情報を調べるために使う
  • @switch_endpoint
    • 指定されたスイッチ(dpid)にあるエンドポイント(正確には非OpenFlow Switch間接続で使われているポート)のリストをキャッシュしておくための Hash
    • 未知の L2 Broadcast に対して Flooding するときの出力先ポート一覧として使う。

コントローラ側では、LLDP を一定時間ごとに投げて、packet_in でトポロジの変換を検出するようになっているので、LinkIndex のオブジェクトも Topology のオブザーバとして登録しておいて、トポロジ変化があったらこの辺の情報を全部作り直すようにしてある。


やってみて

わかったこと
  • 特殊パケットの取り扱いをどうするかは最初に考えておくこと。
    • L2 Broadcast
      • 今回は全部コントローラで処理
    • IP Multicast
      • 今のコードだと明示的な処理はされていないのでちょっと問題かな… (packet_in したあとで単純に path 検索で答えが出ないので放置、という状態。ウチの場合、HSRP とか OSPF のマルチキャストパケットとか入ってくる…。)
    • っていう話を Trema Day #2 とかで聞いた気がする。
はまりどころ

今回はまったところ。

  • packet_in したスイッチと、その処理として flow_mod, packet_out する先のスイッチが一致しない。
    • いや当然なんだけど。
    • 前の(単一スイッチ環境でやってたときの)コードをパクって使っていたりすると、「どのスイッチに対して指示を出すか」が全部固定なので、その辺の考え方が追いついていなくて通信が成立しないという状況が発生。
  • 最後の1ホップ/同一スイッチ間折り返し
    • 経路探索は始点スイッチ→終点スイッチで入れて出します。つまり、始点=終点(同じスイッチ内での折り返し)とか、終点スイッチから宛先ノードへのルールとかが必要になります。…というのを見落とさないようにしましょう。経路中のスイッチにはフローが書き込まれてるのに、終点スイッチから宛先に送るためのルールが入ってない…とか。
  • 安易な Match 条件に気をつけろ。
    • 最初、handle_ipv4 で、とりあえず ExactMatch でいいやとか軽い思いつきでマッチ条件を書いたら、icmp が最初の1発だけ通るけど2発目以降通らなくなって無限ループに陥って死亡、というのが発生。
    • 処理を追いかけてみると、各スイッチに放り込まれるフロールールが、L1の情報まで含めて条件指定されていることが発覚。(ExactMatch だとそうなるというのを見落としていた。) そのため、最初のスイッチでは問題がないけど、次のスイッチでは Match に引っかからずに packet_in が発生 → スイッチ間リンクのポートで arp_table 上書き → 以降こんな感じで、パケットが来るたびに変な処理が続いて無限ループへ突入。
    • …と、いうことで今回は L2/L3 情報だけで Match させるように設定。
  def build_match packet_in
    Match.new(
      :dl_src => packet_in.macsa,
      :dl_dst => packet_in.macda,
      :nw_src => packet_in.ipv4_saddr,
      :nw_dst => packet_in.ipv4_daddr
    )
  end
    • ofvm01 → ofvm07 への icmp についてこういうフローが書き込まれます。行き/帰りで2フローずつ。
------------------
dump flows: ovsbr0
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=4.107s, table=0, n_packets=0, n_bytes=0, priority=65535,dl_src=52:54:00:00:00:01,dl_dst=52:54:00:00:00:07 actions=output:3
cookie=0x2, duration=4.097s, table=0, n_packets=0, n_bytes=0, priority=65535,dl_src=52:54:00:00:00:07,dl_dst=52:54:00:00:00:01 actions=output:5
------------------
dump flows: ovsbr1
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=4.115s, table=0, n_packets=0, n_bytes=0, priority=65535,dl_src=52:54:00:00:00:01,dl_dst=52:54:00:00:00:07 actions=output:2
cookie=0x2, duration=4.104s, table=0, n_packets=0, n_bytes=0, priority=65535,dl_src=52:54:00:00:00:07,dl_dst=52:54:00:00:00:01 actions=output:1
------------------
dump flows: ovsbr2
NXST_FLOW reply (xid=0x4):
------------------
dump flows: ovsbr3
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=4.126s, table=0, n_packets=0, n_bytes=0, priority=65535,dl_src=52:54:00:00:00:01,dl_dst=52:54:00:00:00:07 actions=output:4
cookie=0x2, duration=4.115s, table=0, n_packets=0, n_bytes=0, priority=65535,dl_src=52:54:00:00:00:07,dl_dst=52:54:00:00:00:01 actions=output:2
  • あと、はまったというわけではないけど、Features Request とか終わっていなくて、まだパケット処理するための情報が全部集まってないときに packet_in とかイベントが発生してそのままコントローラが死ぬことがあります。
    • これも Trema Day #2 で聞いた気がする。スイッチを黙らせるとかいう話があった気がするけどあれは何をどうやってるんだろうか。

[追記 2013-05-01]

上の Match の組み立てだと足りないですね。というか dump-flows 下結果が L2 Only でマッチしていますね。というのに気づかずにやっちゃってるのはどうかと思います。Class: Trema::Match — Documentation for trema/trema (master) 見ると書いてあるのですが、

  • :nw_src (String) — the IPv4 source address to match if dl_type is set to 0x0800.
  • :nw_dst (String) — the IPv4 destination address to match if dl_type is set to 0x0800.

なので、:dl_type 指定してやらないといけないのでした。L2/L3でのマッチは正しくはこうですね。

  def build_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
root@oftest03:~# ./dump_flows_all.sh
------------------
dump flows: ovsbr0
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=9.763s, 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:3
cookie=0x2, duration=9.753s, 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:5
------------------
dump flows: ovsbr1
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=9.77s, 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:2
cookie=0x2, duration=9.76s, 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:1
------------------
dump flows: ovsbr2
NXST_FLOW reply (xid=0x4):
------------------
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
root@oftest03:~#

おわりに

こまごまとはまったりしていたけど、単一L2セグメントの処理ということで、基本的には learning swtich の処理そのまま。トポロジ情報の処理と経路探索の処理が書けてしまえば、あとはそれほど難しくないです。L3の処理が入ると自分で ARP の生成処理したりしないといけないとか、パケット処理がいろいろ入るのだけど。L2だけでいいなら。スイッチに入ってきた物をどこにどう転送するか、という処理だけなので。ここまで ruby-topology のコード読み始めて2〜3日くらいでした。

経路探索のテストとかはちゃんとやったほうがいいなあ…と思いつつ、まだやっていないので、そのあたりを上手いことやる方法とかをもうちょっと考えたいところ。

あと、基本的な知識が足りてないなあ、というのをひしひしと感じる。コード読みつつ、Observer パターンって何だとか、ダイクストラ法って何だっけな、とか調べ始めてるので。ちゃんと基礎的なところの勉強せんといかんな…。

今回の参考書はこのへんです。

この先は、sliceable switch 的な方向に進むか、L3 処理を乗っけてみるか…かなあ。経路切り替えとかバランシングの話とかもいいなあ。