Pocket

こんにちは

今回はSDNシリーズの四回目になります。

一回目は、SDNとは何なのかを簡単な応用例を示して説明しました。
二回目は、OpenDaylightとのインストールと、MininetのセットアップおよびOpenDaylightへの接続と疎通確認まで実施しました。
また、前回はOpenDaylightのサンプルアプリのビルドと、そのアプリを用いてMininet上に作成したスイッチの制御を実行してみました。

今回は前回ビルドしたサンプルアプリの解説をしたいと思います。
(長くなったので、Mininetコマンドの解説は次回にさせていただきます。申し訳ありません。)

OpenDaylightのアーキテクチャ

まずはOpenDaylightアーキテクチャの解説です。

以下の図は二回目で紹介した図ですが、まずこのうち”Base Network Service Functions”、SAL(Service Abstraction Layer)、およびOpenFlowについて解説します。

Base Network Service Functions

ネットワーク上のデバイス情報を収集するためのサービス群です。
以下、各バンドルの解説です。

表1(出典:OpenDaylight Tutorial | OpenFlow.org
バンドル API 説明
ARP Handler IHostFinder ARPを処理してホストの場所を学習する
Host Tracker IflptoHost SDN上でホストの相対的な場所を追跡する
Switch Manager ISwitchManager コントローラ内のすべてのスイッチのリストを保持する
Topology Manager ITopologyManager ネットワーク全体のトポロジを保持する
Stats Manager IStatisticsManager IReadServiceを利用して統計情報を収集する
FRM IForwardingRulesManager フローデータベースにアクセスする※
User Manager IUserManager ユーザ管理を担う

ソース上のコメントより。

SAL(Service Abstraction Layer)

SDN対応ネットワーク機器にアクセスするためのAPIです。
以下、各クラスの解説です。

表2(出典:OpenDaylight Tutorial | OpenFlow.org
Class 説明
Action OpenFlowのフローエントリに設定するアクション
Match OpenFlowのフローエントリに設定するマッチングルール
IFlowProgrammerService スイッチに対してフローエントリの追加更新削除を行う
IDataPacketService パケット操作のためのサービス
OpenFlowのForwardアクションを発行する
IReadService スイッチのフロー/ポート/キューのビューを取得する
ITopologyService ネットワーク上で新しいノードやリンクなどを検出した場合に、その情報を伝える
IDataPacketService パケットデコード等、パケット操作を行う
IListenDataPacket パケット処理のためのクラス
このクラスを継承してコントローラクラスを作成する

OpenFlow

前回はMininet上に仮想的なスイッチとホストを作成して、OpenDaylight使ってそのスイッチを制御する方法を示しました。
二回目の記事で紹介したように、MininetはOpenFlow対応の仮想ネットワークデバイスを生成するツールです。

従って、前回ビルドしたControllerアプリもOpenFlowアプリになりますので、ここでOpenFlowの解説をしておきます。

OpenFlowとOpenDaylightの関係は、図1の左下に記載されているように、OpenDaylightでサポートしているプロトコルの1つになります。

OpenFlowの詳細については、多くの日本語サイトがありますので、そちらでご確認願います。
ここでは、サンプルアプリの説明のため、OpenFlowについては以下の点のみ抑えておきます。

  • パケットの制御は、ヘッダフィールドのマッチングルールとアクションで実施する。
    ※ここのマッチングルールとアクションをハンドリングするOpenDaylightのクラスが、表2のMatchとActionです。
  • コントローラとネットワーク機器の間では、パケットの制御情報をメッセージ(OpenFlowメッセージ)によってやり取りする。

サンプルアプリ(tutorial_L2_forwarding)

上記のOpenDaylightのサービス群やクラスおよびOpenFlowに対する説明を踏まえて、サンプルアプリの解説をしていきたいと思います。
簡単な流れは以下のようになっています。※これは、Learning Switchの動作です。

  1. スイッチからのPacket-in受信を契機として処理開始
  2. 受信パケットから以下の情報を取得
    1. 送信元MACアドレス
    2. 送信元ポート番号
    3. 宛先MACアドレス
  3. 取得した送信元MACアドレスと送信元ポート番号の対をHashMapに格納
  4. 宛先MACアドレスを元にHashMapから宛先ポート番号を取得
    1. 取得できなかった場合
      受信元ポート番号以外のすべてのポートにパケットを送信(floodPacket)
    2. 取得できた場合
      1. 送信元ポート番号と宛先MACアドレスをそれぞれMatchリストに追加
      2. 取得できた送信先ポート番号からパケットを送出するアクションをActionに登録
      3. 上記のMatchリストとActionをフローエントリに追加
      4. 取得したポート番号のポートに対してパケットを送出

このサンプルアプリは、TutorialL2Forwarding.java、およびActivator.javaの二つのモジュールから成り立っています。
このうち、Activator.javaはOSGiフレームワークを使用したバンドルアクティベータで、本体はTutorialL2Forwarding.javaに記述されています。

以下、ソースの解説です。

TutorialL2Forwarding.java

はじめに前提として以下の行を書き換えます。※これをしないと、サンプルコントローラは単なるRepeater Hubになります。

private String function = "hub";

private String function = "switch";

このクラスはSouthbound APIからパケットを取得するために必要なOpenDaylightのIListenDataPacketクラスを継承します。

(List1)

public class TutorialL2Forwarding implements IListenDataPacket {

1.Packet-in

IListenDataPacketクラスのreceiveDataPacketメソッドをオーバーライドすることにより、パケットのコピーを取得します(List2 182行目)。
このメソッドはOpenFlowのPacket-inイベントが発生すると呼び出されます。

2.送信元接続ポート抽出、受信パケット復号

receiveDataPacketメソッドの引数として渡されたRawPacketのgetIncomingNodeConnectorメソッドを用いて送信元接続ポートを抽出します。(List2 187行目)
次に、dataPacketServiceを使用してパケットを復号し、パケットの各フィールドへアクセスするために、org.opendaylight.controller.sal.packet.IDataPacketServiceクラスのdecodeDataPacketメソッドを使用して、Packetオブジェクトに変換します。(List2 193行目)

(List2)

@Override
public PacketResult receiveDataPacket(RawPacket inPkt) {
	if (inPkt == null) {
		return PacketResult.IGNORED;
	}

	NodeConnector incoming_connector = inPkt.getIncomingNodeConnector();

	// Hub implementation
	if (function.equals("hub")) {
		floodPacket(inPkt);
	} else {
		Packet formattedPak = this.dataPacketService.decodeDataPacket(inPkt);
		if (!(formattedPak instanceof Ethernet)) {
			return PacketResult.IGNORED;
		}

		learnSourceMAC(formattedPak, incoming_connector);
		NodeConnector outgoing_connector =
			knowDestinationMAC(formattedPak);
		if (outgoing_connector == null) {
			floodPacket(inPkt);
		} else {
			if (!programFlow(formattedPak, incoming_connector,
						outgoing_connector)) {
				return PacketResult.IGNORED;
			}
			inPkt.setOutgoingNodeConnector(outgoing_connector);
			this.dataPacketService.transmitDataPacket(inPkt);
		}
	}
	return PacketResult.CONSUME;
}

3.送信元情報の取得

learnSourceMACローカルメソッドでMACアドレスと入力ポートの対をHashMapに格納する。(List2 198行目、List3 215-218行目)
復号したパケットからorg.opendaylight.controller.sal.packet.EthernetクラスのgetSourceMACAddressメソッドとorg.opendaylight.controller.sal.packet.BitBufferHelperのtoNumberメソッドを用いて送信元MACアドレスを取り出す。(216、217行目)
送信元MACアドレスと送信元接続ポートの対をHashMapに格納。(218行目)

(List3)

private void learnSourceMAC(Packet formattedPak, NodeConnector incoming_connector) {
byte[] srcMAC = ((Ethernet)formattedPak).getSourceMACAddress();
	long srcMAC_val = BitBufferHelper.toNumber(srcMAC);
	this.mac_to_port.put(srcMAC_val, incoming_connector);
}

4.宛先MACアドレス取得、接続ポート判定

knowDestinationMACローカルメソッドで内部のHashMapから宛先MACアドレスを取り出す。(List2 200行目)org.opendaylight.controller.sal.packet.EthernetクラスのgetDestinationMACAddressメソッドで宛先MACアドレスを取り出す。(222、223行目)
取り出した宛先MACアドレスの接続ポートを返却。(224行目)

(List4)

private NodeConnector knowDestinationMAC(Packet formattedPak) {
	byte[] dstMAC = ((Ethernet)formattedPak).getDestinationMACAddress();
	long dstMAC_val = BitBufferHelper.toNumber(dstMAC);
	return this.mac_to_port.get(dstMAC_val) ;
}

5.foodPacket

List4 knowDestinationMACローカルメソッドの戻り値で宛先MACアドレスの接続ポートが取得できなかった場合、
(宛先MACアドレスの接続ポートを学習していなかった場合)はfloodPacketローカルメソッドで送信元以外のすべてのポートへパケットを送出する。(List2 202行目)
floodPacketローカルメソッドでは、ISwitchManagerクラスのgetUpNodeConnectorsメソッドを使用してスイッチ内のすべてのポートを抽出(List5 163、164行目)し、抽出したポートの数分(送信元ポートを除く)IDataPacketServiceクラスのtransmitDataPacketメソッドを使用してパケットを送出する。(List5 171行目)

(List5)

private void floodPacket(RawPacket inPkt) {
	NodeConnector incoming_connector = inPkt.getIncomingNodeConnector();
	Node incoming_node = incoming_connector.getNode();

	Set<NodeConnector> nodeConnectors =
				this.switchManager.getUpNodeConnectors(incoming_node);

	for (NodeConnector p : nodeConnectors) {
		if (!p.equals(incoming_connector)) {
			try {
				RawPacket destPkt = new RawPacket(inPkt);
				destPkt.setOutgoingNodeConnector(p);
				this.dataPacketService.transmitDataPacket(destPkt);
			} catch (ConstructionException e2) {
				continue;
			}
		}
	}
}

6.フローエントリ組み立て、フローテーブル書込

宛先MACアドレスの接続ポートが取得できた場合、programFlowローカルメソッドを呼び出し、フローエントリの組み立てとスイッチへの登録を行う。(List2 204、205行目)
programFlowローカルメソッドでは、Matchクラスを使用して、マッチングルールを作成。(List6 232-234行目)
ここでは、入力ポートが今回の送信元ポートであること(List6 233行目)と、宛先MACアドレスが今回の宛先MACアドレスであること(List6 234行目)となっています。

(List6)

private boolean programFlow(Packet formattedPak,
			NodeConnector incoming_connector,
			NodeConnector outgoing_connector) {
	byte[] dstMAC = ((Ethernet)formattedPak).getDestinationMACAddress();

	Match match = new Match();
	match.setField( new MatchField(MatchType.IN_PORT, incoming_connector) );
	match.setField( new MatchField(MatchType.DL_DST, dstMAC.clone()) );

	List<Action> actions = new ArrayList<Action>();
	actions.add(new Output(outgoing_connector));

	Flow f = new Flow(match, actions);
	f.setIdleTimeout((short)5);

	// Modify the flow on the network node
	Node incoming_node = incoming_connector.getNode();
	Status status = programmer.addFlow(incoming_node, f);

	if (!status.isSuccess()) {
		logger.warn("SDN Plugin failed to program the flow: {}. The failure is: {}",
					f, status.getDescription());
		return false;
	} else {
		return true;
	}
}

次に、アクションの作成をします。
ここでは今回の宛先ポートからパケットを送出するルールになっています。(List6 237行目)
これらマッチングルールとアクションをFlowクラスに格納(List6 239行目)し、IFlowProgrammerServiceクラスのaddFlowメソッドに渡して実際にスイッチにフローエントリを挿入しています。(List6 244行目)

7.パケット送出

フローエントリの挿入に成功したら、入力パケットを出力ポートから送出します。(List2 209行目)

8.処理終了

最後にパケットが正常に処理されたことを示すPacketResult.CONSUMEを返して処理を終了します。(List2 212行目)

終わりに

今回はサンプルソースの解説だけで大きくなってしまったので、予定していたMininetコマンドの解説は次回にします。
以上です。