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点…という作りにしました。

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

基本動作

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

通信を成立させるために、いくつかの 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 パターンって何だとか、ダイクストラ法って何だっけな、とか調べ始めてるので。ちゃんと基礎的なところの勉強せんといかんな…。
今回の参考書はこのへんです。

Rubyによるデザインパターン

Rubyによるデザインパターン

インターネットルーティング入門 第2版 (ネットワーキング入門シリーズ)

インターネットルーティング入門 第2版 (ネットワーキング入門シリーズ)

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