ラーニングスイッチを OpenFlow1.3 で実装し、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の命令パイプラインのように、パケット処理を「フィルタリング」→「書き換え」→ … →「転送」とステージごとに進めていくイメージです。フローテーブルごとに役割を明確にできるので、プログラマから見てフローエントリを整理しやすいというメリットがあります。
このパイプライン処理は、テーブル ID が 0 のテーブルから始まり GotoTable インストラクションによって次のテーブルに移動することで進みます。パイプライン処理の入口となるテーブル、つまり Packet In したときに最初に入るテーブルの ID は 0 と決まっています。現在のテーブルから次のテーブルへと処理を移行するには GotoTable インストラクションに次のテーブル ID を指定します。このとき指定するテーブル ID は、現在のテーブル ID よりも大きい必要があります。
さて「GotoTable インストラクション」という用語を今まで断りなく使ってきましたが、OpenFlow1.3 ではパケットに対する処理を「アクション」と「インストラクション」に分けて書きます。まずはアクションから説明しましょう。
アクションの1つの用途はパケットの書き換えです。書き換えアクションの種類は OpenFlow1.0 に比べて大幅に増えており、マッチフィールドで指定できるフィールドの書き換えや VLAN ヘッダの操作に加え、TTL や MPLS, IPv6 パケット等への操作が追加されています (表8-1)。
アクションのクラス名 | 説明 |
---|---|
|
マッチ条件で指定できるフィールドをパケットにセットする |
|
2番目に外側のTTLの値を一番外側のTTLにコピーする |
|
一番外側のTTLの値を1つ内側のTTLにコピーする |
|
MPLSシムヘッダのTTLをセットする |
|
MPLSシムヘッダのTTLを1つ減らす |
|
新しいVLANヘッダをパケットに追加する |
|
一番外側のVLANヘッダをパケットから取り除く |
|
新しいMPLSシムヘッダをパケットに追加する |
|
一番外側のMPLSタグまたはシムヘッダをパケットから取り除く |
|
指定したグループテーブルでパケットを処理する |
|
IPv4のTTLまたはIPv6のhop limitをセットする |
|
IPv4のTTLまたはIPv6のhop limitを1つ減らす |
|
新しいPBBサービスインスタンスヘッダ (I-TAG TCI) をパケットに追加する |
|
一番外側のPBBサービスインスタンスヘッダ (I-TAG TCI) をパケットから取り除く |
もう1つのアクションの用途はパケットの出力です。指定したポートへ出力したり、ポートに関連付けられたキューにパケットを追加するのに使います (表8-2)。
アクションのクラス名 | 説明 |
---|---|
|
指定したスイッチの (論理) ポートにパケットを出力する |
|
|
インストラクションはアクションよりも一段上の処理で、フローテーブルの移動とアクションの実行方法を記述できます。たとえば GotoTable インストラクションは、次のように Flow Mod の instructions
パラメータに指定しておくことで、マッチしたパケットが到着するとそのパケット処理を指定したフローテーブルへと続けます。
# テーブル 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
を指定した場合と同じ効果を持ちます。
# ポート 1 番へ出力
send_flow_mod_add(
datapath_id,
...
instructions: Apply.new(SendOutPort.new(1))
)
WriteActions
は指定したアクションを後でまとめて適用するために使います。GotoTable でテーブルを移動しながら、パケットに WriteActions
で指定したアクションを「後で適用するアクション」に追加していきます。そして GotoTable を含まないフローエントリにパケットがマッチしたタイミングで、そのパケットの「後で適用するアクション」をまとめて適用します。
「この後で適用するアクション」をアクションセットと呼びます。アクションセットはいわゆる集合なので、同じアクションを複数入れることはできません。WriteActions
以外にも、アクションセットを空にする Clear
インストラクションがあります。ここまでのインストラクションを含めてインストラクション一覧を紹介しましょう。
インストラクションのクラス名 | 説明 |
---|---|
|
マッチしたパケットの処理を指定したテーブルに引き継ぐ |
|
指定したアクションを実行する |
|
アクションセットに指定したアクションを追加する |
|
アクションセットを空にする |
|
テーブル間で引き継げる 64bit のメタデータをセット |
|
パケットを指定したメーターに適用する |
OpenFlow1.3では、フローエントリにマッチしないパケットはPacket Inしません。このため OpenFlow1.0 で問題となった、フローエントリの設定前にパケットが大量に到着しうるという問題を解決できます。OpenFlow1.3でPacketInを起こすためには、アクションに SendOutPort.new(:controller)
(コントローラへパケットを送り PacketIn を起こす) を指定したフローエントリを明示的に追加します。
OpenFlow1.3版ラーニングスイッチでは、役割の異なる2つのフローテーブルを用いてイーサネットスイッチを実現します。
- フィルタリングテーブル
-
転送しないパケットをドロップする。それ以外のパケットは転送テーブルに送る
- 転送テーブル
-
学習したMACアドレスを使ってパケットを転送する。宛先MACアドレスが見つからない場合にはフラッディングする
OpenFlow1.3版パッチパネルのソースコードは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
ハンドラでは、まだ学習していないパケットのデフォルト処理を新しく起動したスイッチのフローテーブルに書き込みます。
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 フレームをドロップするフローエントリを書き込みます。
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
で処理します。
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 アドレスをまだ学習していない場合のデフォルト処理をフローテーブルに書き込みます。
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 したパケットの送信元 MAC アドレス + In Port の組を学習します。学習した組はテーブル ID が FORWARDING_TABLE_ID であるフローテーブルにフローエントリとして追加します。
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
ハンドラの大量呼び出しが起こらない
続く章ではアジャイル開発手法を使って、コントローラを反復的に開発する手法を紹介します。テストコードを書きながら徐々に機能を追加していくことで、バグの少ないコントローラを着実に開発できます。