Skip to content

Latest commit

 

History

History
377 lines (292 loc) · 20.9 KB

learning_switch13.adoc

File metadata and controls

377 lines (292 loc) · 20.9 KB

OpenFlow1.3版ラーニングスイッチ

ラーニングスイッチを OpenFlow1.3 で実装し、OpenFlow1.0版のラーニングスイッチの欠点を解消します。

OpenFlow1.0版ラーニングスイッチの問題点

7章で実装したラーニングスイッチには、実は以下の問題点があります。

フローテーブルが煩雑になる

OpenFlow1.0では同時に使えるフローテーブルは 1 つという制限があります。このため、ラーニングスイッチのようにBPDUフレームなどのフィルタリング用のフローエントリとパケット転送用のフローエントリが一つのフローテーブルに混在すると、後から見たときに解読が大変です。

起動時の大量のPacketInを防げない

OpenFlow1.0ではフローエントリにマッチしないパケットはすべてPacket Inします。このため、switch_ready ハンドラでフィルタリング用のフローエントリを設定するよりも前にパケットがコントローラへ大量に到着すると、packet_in ハンドラの大量呼び出しによりコントローラがパンクしてしまいます。

マルチプルテーブル

フローテーブルは 1 つという OpenFlow1.0 の制限は、OpenFlow1.3 でなくなっています。OpenFlow1.3 では 1 つのパケットの処理を複数のフローテーブルを使って処理できます。このようなパケット処理をパイプライン処理と呼びます。ちょうどCPUの命令パイプラインのように、パケット処理を「フィルタリング」→「書き換え」→ …​ →「転送」とステージごとに進めていくイメージです。フローテーブルごとに役割を明確にできるので、プログラマから見てフローエントリを整理しやすいというメリットがあります。

pipeline
図 8-1: OpenFlow1.3でのマルチプルテーブルによるパイプライン処理

テーブルの移動

このパイプライン処理は、テーブル ID が 0 のテーブルから始まり GotoTable インストラクションによって次のテーブルに移動することで進みます。パイプライン処理の入口となるテーブル、つまり Packet In したときに最初に入るテーブルの ID は 0 と決まっています。現在のテーブルから次のテーブルへと処理を移行するには GotoTable インストラクションに次のテーブル ID を指定します。このとき指定するテーブル ID は、現在のテーブル ID よりも大きい必要があります。

pipeline goto
図 8-2: テーブル ID 0 から始まり GotoTable インストラクションで次のテーブルへ処理を移動

OpenFlow1.3 のアクション

さて「GotoTable インストラクション」という用語を今まで断りなく使ってきましたが、OpenFlow1.3 ではパケットに対する処理を「アクション」と「インストラクション」に分けて書きます。まずはアクションから説明しましょう。

アクションの1つの用途はパケットの書き換えです。書き換えアクションの種類は OpenFlow1.0 に比べて大幅に増えており、マッチフィールドで指定できるフィールドの書き換えや VLAN ヘッダの操作に加え、TTL や MPLS, IPv6 パケット等への操作が追加されています (表8-1)。

Table 1. OpenFlow 1.3 で使えるアクション一覧 (パケットのフィールド書き換え)
アクションのクラス名 説明

SetField

マッチ条件で指定できるフィールドをパケットにセットする

CopyTtlOut

2番目に外側のTTLの値を一番外側のTTLにコピーする

CopyTtlIn

一番外側のTTLの値を1つ内側のTTLにコピーする

SetMplsTtl

MPLSシムヘッダのTTLをセットする

DecrementMplsTtl

MPLSシムヘッダのTTLを1つ減らす

PushVlanHeader

新しいVLANヘッダをパケットに追加する

PopVlanHeader

一番外側のVLANヘッダをパケットから取り除く

PushMpls

新しいMPLSシムヘッダをパケットに追加する

PopMpls

一番外側のMPLSタグまたはシムヘッダをパケットから取り除く

Group

指定したグループテーブルでパケットを処理する

SetIpTtl

IPv4のTTLまたはIPv6のhop limitをセットする

DecrementIpTtl

IPv4のTTLまたはIPv6のhop limitを1つ減らす

PushPbb

新しいPBBサービスインスタンスヘッダ (I-TAG TCI) をパケットに追加する

PopPbb

一番外側のPBBサービスインスタンスヘッダ (I-TAG TCI) をパケットから取り除く

もう1つのアクションの用途はパケットの出力です。指定したポートへ出力したり、ポートに関連付けられたキューにパケットを追加するのに使います (表8-2)。

Table 2. OpenFlow 1.3 で使えるアクション一覧 (パケットの出力)
アクションのクラス名 説明

SendOutPort

指定したスイッチの (論理) ポートにパケットを出力する

SetQueue

SendOutPort で指定したポートの指定したキューにパケットを追加する

インストラクション

インストラクションはアクションよりも一段上の処理で、フローテーブルの移動とアクションの実行方法を記述できます。たとえば GotoTable インストラクションは、次のように Flow Mod の instructions パラメータに指定しておくことで、マッチしたパケットが到着するとそのパケット処理を指定したフローテーブルへと続けます。

GotoTable インストラクションの指定方法
# テーブル 0 番から 1 番へ GotoTable
send_flow_mod_add(
  datapath_id,
  table_id: 0,
    ...
  instructions: GotoTable.new(1)
)

インストラクションのもう1つの用途は、アクションを適用するタイミングの指定です。指定方法は次の 2 通りです。

  • Apply 指定したアクションを直ちにパケットへ適用する

  • WriteActions 指定したアクションを後で適用するために、パケットに関連付ける

Apply を使うと指定したアクションを直ちにパケットへ適用できます。これはちょうど、OpenFlow1.0 の Flow Mod で actions を指定した場合と同じ効果を持ちます。

Apply で指定したアクションをパケットへ直ちに適用
# ポート 1 番へ出力
send_flow_mod_add(
  datapath_id,
    ...
  instructions: Apply.new(SendOutPort.new(1))
)

WriteActions は指定したアクションを後でまとめて適用するために使います。GotoTable でテーブルを移動しながら、パケットに WriteActions で指定したアクションを「後で適用するアクション」に追加していきます。そして GotoTable を含まないフローエントリにパケットがマッチしたタイミングで、そのパケットの「後で適用するアクション」をまとめて適用します。

pipeline write actions
図 8-3: WriteActions でアクションを後でまとめて適用

「この後で適用するアクション」をアクションセットと呼びます。アクションセットはいわゆる集合なので、同じアクションを複数入れることはできません。WriteActions 以外にも、アクションセットを空にする Clear インストラクションがあります。ここまでのインストラクションを含めてインストラクション一覧を紹介しましょう。

Table 3. OpenFlow 1.3 で使えるインストラクション一覧
インストラクションのクラス名 説明

GotoTable

マッチしたパケットの処理を指定したテーブルに引き継ぐ

Apply

指定したアクションを実行する

WriteActions

アクションセットに指定したアクションを追加する

Clear

アクションセットを空にする

WriteMetadata

テーブル間で引き継げる 64bit のメタデータをセット

Meter

パケットを指定したメーターに適用する

OpenFlow1.3 での Packet In

OpenFlow1.3では、フローエントリにマッチしないパケットはPacket Inしません。このため OpenFlow1.0 で問題となった、フローエントリの設定前にパケットが大量に到着しうるという問題を解決できます。OpenFlow1.3でPacketInを起こすためには、アクションに SendOutPort.new(:controller) (コントローラへパケットを送り PacketIn を起こす) を指定したフローエントリを明示的に追加します。

OpenFlow1.3版ラーニングスイッチの仕組み

OpenFlow1.3版ラーニングスイッチでは、役割の異なる2つのフローテーブルを用いてイーサネットスイッチを実現します。

フィルタリングテーブル

転送しないパケットをドロップする。それ以外のパケットは転送テーブルに送る

転送テーブル

学習したMACアドレスを使ってパケットを転送する。宛先MACアドレスが見つからない場合にはフラッディングする

ソースコード解説

OpenFlow1.3版パッチパネルのソースコードはlib/learning_switch13.rbになります。

lib/learning_switch13.rb
require 'fdb'

# An OpenFlow controller that emulates an ethernet switch.
class LearningSwitch13 < Trema::Controller
  timer_event :age_fdb, interval: 5.sec

  INGRESS_FILTERING_TABLE_ID = 0
  FORWARDING_TABLE_ID = 1

  AGING_TIME = 180

  def start(_args)
    @fdb = FDB.new
    logger.info "#{name} started."
  end

  def switch_ready(datapath_id)
    add_bpdu_drop_flow_entry(datapath_id)
    add_default_broadcast_flow_entry(datapath_id)
    add_default_flooding_flow_entry(datapath_id)
    add_default_forwarding_flow_entry(datapath_id)
  end

  def packet_in(_datapath_id, packet_in)
    @fdb.learn(packet_in.source_mac, packet_in.in_port)
    add_forwarding_flow_and_packet_out(packet_in)
  end

  def age_fdb
    @fdb.age
  end

  private

  def add_forwarding_flow_and_packet_out(packet_in)
    port_no = @fdb.lookup(packet_in.destination_mac)
    add_forwarding_flow_entry(packet_in, port_no) if port_no
    packet_out(packet_in, port_no || :flood)
  end

  def add_forwarding_flow_entry(packet_in, port_no)
    send_flow_mod_add(
      packet_in.datapath_id,
      table_id: FORWARDING_TABLE_ID,
      idle_timeout: AGING_TIME,
      priority: 2,
      match: Match.new(in_port: packet_in.in_port,
                       destination_mac_address: packet_in.destination_mac,
                       source_mac_address: packet_in.source_mac),
      instructions: Apply.new(SendOutPort.new(port_no))
    )
  end

  def packet_out(packet_in, port_no)
    send_packet_out(
      packet_in.datapath_id,
      packet_in: packet_in,
      actions: SendOutPort.new(port_no)
    )
  end

  def add_default_broadcast_flow_entry(datapath_id)
    send_flow_mod_add(
      datapath_id,
      table_id: FORWARDING_TABLE_ID,
      idle_timeout: 0,
      priority: 3,
      match: Match.new(destination_mac_address: 'ff:ff:ff:ff:ff:ff'),
      instructions: Apply.new(SendOutPort.new(:flood))
    )
  end

  def add_default_flooding_flow_entry(datapath_id)
    send_flow_mod_add(
      datapath_id,
      table_id: FORWARDING_TABLE_ID,
      idle_timeout: 0,
      priority: 1,
      match: Match.new,
      instructions: Apply.new(SendOutPort.new(:controller))
    )
  end

  def add_bpdu_drop_flow_entry(datapath_id)
    send_flow_mod_add(
      datapath_id,
      table_id: INGRESS_FILTERING_TABLE_ID,
      idle_timeout: 0,
      priority: 2,
      match: Match.new(destination_mac_address: '01:80:C2:00:00:00')
    )
  end

  def add_default_forwarding_flow_entry(datapath_id)
    send_flow_mod_add(
      datapath_id,
      table_id: INGRESS_FILTERING_TABLE_ID,
      idle_timeout: 0,
      priority: 1,
      match: Match.new,
      instructions: GotoTable.new(FORWARDING_TABLE_ID)
    )
  end
end

switch_ready ハンドラ

switch_ready ハンドラでは、まだ学習していないパケットのデフォルト処理を新しく起動したスイッチのフローテーブルに書き込みます。

LearningSwitch13#switch_ready (lib/learning_switch13.rb)
def switch_ready(datapath_id)
  add_bpdu_drop_flow_entry(datapath_id)
  add_default_broadcast_flow_entry(datapath_id)
  add_default_flooding_flow_entry(datapath_id)
  add_default_forwarding_flow_entry(datapath_id)
end

最初に呼び出す add_bpdu_drop_flow_entry では、不要なスパニングツリーの BPDU フレームをドロップするフローエントリを書き込みます。

LearningSwitch13#add_bpdu_drop_flow_entry (lib/learning_switch13.rb)
def add_bpdu_drop_flow_entry(datapath_id)
  send_flow_mod_add(
    datapath_id,
    table_id: INGRESS_FILTERING_TABLE_ID,
    idle_timeout: 0,
    priority: 2,
    match: Match.new(destination_mac_address: '01:80:C2:00:00:00')
  )
end

Flow Mod に指定するパラメータのうち、ポイントとなるのは次の 3 つです。

table_id

スイッチに入ってきたパケットの種類を見てドロップするかどうかを最初にフィルタリングする必要があるので、table_id には 0 (INGRESS_FILTERING_TABLE_ID) を指定します。

idle_timeout

BPDU フレームのドロップはスイッチの起動中はずっと有効なので、idle_timeout には 0 (フローエントリを消さない) を指定します。

priority

ドロップ処理は入ってきたパケットに対して最初に行うフィルタリングなので、テーブルID = 0 のフローエントリのうち最大優先度にします。ここでは 2 を指定します。

続く add_default_forwarding_flow_entry では、BPDU フレーム以外のパケットを FORWARDING_TABLE_ID で処理します。

LearningSwitch13#add_default_forwarding_flow_entry (lib/learning_switch13.rb)
link:vendor/learning_switch/lib/learning_switch13.rb[role=include]

ここで重要なパラメータは次の 3 つです。

priority

優先度を 1 に設定することで、より優先度の高いBPDUフレーム処理 (優先度 = 2) が終わったあとにこの処理を行う

match

空のマッチを指定することで、BPDUフレームでないパケットをすべてこのフローエントリで拾う

instructions

GotoTable(FORWARDING_TABLE_ID) を指定することで、以降の処理をテーブル 1 に移す

最後の add_default_flooding_flow_entry では、宛先 MAC アドレスをまだ学習していない場合のデフォルト処理をフローテーブルに書き込みます。

LearningSwitch13#add_default_flooding_flow_entry (lib/learning_switch13.rb)
link:vendor/learning_switch/lib/learning_switch13.rb[role=include]
table_id

ここで追加するフローエントリは、直前の GotoTable でテーブル ID INGRESS_FILTERING_TABLE_ID から FORWARDING_TABLE_ID に移動した後に処理さる。このため、table_id には FORWARDING_TABLE_ID を指定する

priority

フラッディング処理は宛先 MAC アドレスをまだ学習していなかった場合のデフォルト処理なので、優先度は低めの 1 を指定する

instructions

フラッディングのための SendOutPort.new(:flood) アクションと、Packet In を起こするための SendOutPort.new(:controller)Apply インストラクションで適用する

packet_in ハンドラ

packet_in ハンドラでは、Packet In したパケットの送信元 MAC アドレス + In Port の組を学習します。学習した組はテーブル ID が FORWARDING_TABLE_ID であるフローテーブルにフローエントリとして追加します。

LearningSwitch13#switch_ready (lib/learning_switch13.rb)
def packet_in(_datapath_id, packet_in)
  @fdb.learn(packet_in.source_mac, packet_in.in_port)
  add_forwarding_flow_and_packet_out(packet_in)
end

private

def add_forwarding_flow_and_packet_out(packet_in)
  port_no = @fdb.lookup(packet_in.destination_mac)
  add_forwarding_flow_entry(packet_in, port_no) if port_no
  packet_out(packet_in, port_no || :flood)
end

def add_forwarding_flow_entry(packet_in, port_no)
  send_flow_mod_add(
    packet_in.datapath_id,
    table_id: FORWARDING_TABLE_ID,
    idle_timeout: AGING_TIME,
    priority: 2,
    match: Match.new(in_port: packet_in.in_port,
                     destination_mac_address: packet_in.destination_mac,
                     source_mac_address: packet_in.source_mac),
    instructions: Apply.new(SendOutPort.new(port_no))
  )
end

ここでの Flow Mod パラメータのポイントは次のとおりです。

priority

優先度を FORWARDING_TABLE_ID の他のフローエントリ (フラッディング) よりも高くすることで、このフローエントリにマッチしない場合だけフラッディングするようにする

idle_timeout

フローエントリの寿命を指定しておくことで、OpenFlow1.0 版のラーニングスイッチで行ったタイマによるエイジングと同じ効果を出せる

match, instructions

宛先が Packet In の送信元 MAC アドレスと同じだったら、Packet In の in_port から入ったパケットをそちらに送る、というエントリを入れる

まとめ

ラーニングスイッチを OpenFlow1.3 で実装することで、OpenFlow1.0 版での問題点を解決しました。

  • マルチプルテーブルを使うことで、フローテーブルごとにパケット処理を分けデバッグしやすくできる

  • GotoTable インストラクションを使うことで、1つのパケットを複数のフローテーブルで処理できる

  • OpenFlow1.3 ではデフォルトで Packet In が起こらない。このため、OpenFlow1.0 で問題となるフローエントリ設定前の packet_in ハンドラの大量呼び出しが起こらない

続く章ではアジャイル開発手法を使って、コントローラを反復的に開発する手法を紹介します。テストコードを書きながら徐々に機能を追加していくことで、バグの少ないコントローラを着実に開発できます。