LoginSignup
22
25

More than 5 years have passed since last update.

PINGアスキーアート with OpenFlow - TremaとOpen vSwitchでICMPを制御してみた

Last updated at Posted at 2016-03-30

Ciscoルータのpingコマンドでアスキーアートを描いてみました。
OpenFlowでICMPパケットを許可・遮断を細かく制御して実現しています。OpenFlowコントローラはTrema、OpenFlowスイッチはOpen vSwitchを利用してます。

PINGアスキーアート

Ciscoルータのpingコマンドは、宛先にICMPパケットを送信し、その結果に応じて1文字で表現されます。正常時は「!」、到達不可時は「.」など、結果に応じて文字は変わります(参考)。1行あたり70回分の実行結果が表示されます。PINGアスキーアートではこのCiscoルータの仕様を利用し、OpenFlowでICMPパケットを制御し、pingの実行結果を変化させてアスキーアートを描きます。

図. Ciscoルータのpingコマンドの実行結果(1行70文字)
Cisco PING.png

全体の構成

PINGアスキーアートを実現するために、OpenFlowでICMPパケットを制御します。CiscoルータにOpenFlowスイッチを下図のように接続します。ルータから送信されるICMPパケットはOpenFlowスイッチを経由し、OpenFlowコントローラに伝えられ、許可と遮断を制御します。

図. 全体の構成
全体構成.PNG

アスキーアートの元画像

OpenFlowコントローラでは、アスキーアートとして描画する下図のような画像を読み込み2値化し、黒ピクセルは許可、白ピクセルは遮断として読み込みます。読み込む画像はpingコマンドの実行結果に合わせて幅70ピクセルとなります。
サンプルで用意した画像は下図になります。幅70ピクセル、高さ50ピクセルで合計3500ピクセルあります。pingコマンドで繰り返す回数(pingのrepeatオプション)に3500を指定すればちょうどです。

図. アスキーアートにする画像
img.png

制御方法

OpenFlowスイッチから送信されたICMP Echo Requestパケットを、OpenFlowコントローラが受信(PACKET_IN)するたびに、許可と遮断を判断し、OpenFlowスイッチに結果を送信(PACKET_OUT)します。当初に実装した方式は、許可の場合はPACKET_INしたICMPパケットをそのままPACKET_OUTし、遮断の場合はPACKET_OUTしないでICMPパケットをドロップさせる方式でした。

ドロップ方式の問題点

ICMPパケットをドロップさせる方式は実装が単純ですが、実行時間が凄まじく時間がかかることが問題となりました。
CiscoルータのPINGのタイムアウトは最低0秒に設定できますが、タイムアウトが早すぎて、ほとんどがタイムアウトしてドロップとなり、意図した動作をしません。そのため、実質的に設定できるタイムアウトは最低1秒です。
上図を2値化した場合、黒ピクセルが774ピクセル、白ピクセルが2726ピクセルです。白ピクセルはドロップ対象となり、1秒のタイムアウトが発生するため、約45分かかることになります。逆に黒ピクセルをドロップ対象にしても約12分かかり、時間がかかりすぎます。

到達不能方式による改善案

ドロップさせる方式ではCiscoのpingコマンドのタイムアウトが問題になります。このため、タイムアウトに依存しない方法で実行時間を短くします。
今回は遮断の場合、ICMP Unreachableを生成し、送信元に返送する方式にします。Ciscoルータのpingの場合、ICMP Unreachableは「U」と表されます。こうすることで、即時結果に反映されるため、実行時間が飛躍的に短縮できます。
なお、pingコマンドの実行結果が、今までは「.」だった箇所が「U」となってしまい、微妙に見栄えが変わります。

実装

OpenFlowスイッチ、OpenFlowコントローラ、Ciscoルータは下記の通り実装しています。
開発の都合から、OpenFlowスイッチとOpenFlowコントローラは別筐体で実行しています。

図. 環境
環境.PNG

OpenFlowスイッチ

OpenFlowスイッチは下記のとおり、LIVA本体にUSB型のLANアダプタを2つ接続し、合計3つのNICを持っています。LIVAにUbuntu14.04とOpen vSwitchをインストールして、OpenFlowスイッチとして動作させます。

NIC
eth1とeth2をOpenFlowスイッチのポートとし、レイヤ2ブリッジさせます。OpenFlowスイッチの名前はofs0としています。

  • eth0: 管理ポート
  • eth1: 内部ポート
  • eth2: 外部ポート

図. OpenFlowスイッチの物理構成
環境.png

設定コマンド
$ sudo ovs-vsctl add-br ofs0
$ sudo ovs-vsctl set bridge ofs0 protocols=OpenFlow10
$ sudo ovs-vsctl add-port ofs0 eth1
$ sudo ovs-vsctl add-port ofs0 eth2
$ sudo ifconfig eth1 up
$ sudo ifconfig eth2 up
$ sudo ovs-vsctl set-controller ofs0 tcp:192.168.88.17:6653
$ sudo ovs-vsctl show
428db0fc-4015-4a6e-86f5-9d3dfba68904
    Bridge "ofs0"
        Controller "tcp:192.168.88.17:6653"
        Port "eth2"
            Interface "eth2"
        Port "eth1"
            Interface "eth1"
        Port "ofs0"
            Interface "ofs0"
                type: internal
    ovs_version: "2.5.0"
$ sudo ovs-ofctl dump-flows ofs0
NXST_FLOW reply (xid=0x4):
 cookie=0x0, duration=51.458s, table=0, n_packets=5, n_bytes=310, idle_age=8, priority=0,in_port=1 actions=output:2
 cookie=0x0, duration=51.420s, table=0, n_packets=0, n_bytes=0, idle_age=51, priority=0,dl_dst=ff:ff:ff:ff:ff:ff actions=FLOOD
$ sudo ovs-ofctl dump-ports ofs0
OFPST_PORT reply (xid=0x2): 3 ports
  port  1: rx pkts=1164747, bytes=347301435, drop=0, errs=0, frame=0, over=0, crc=0
           tx pkts=122833, bytes=14868218, drop=0, errs=0, coll=0
  port LOCAL: rx pkts=0, bytes=0, drop=0, errs=0, frame=0, over=0, crc=0
           tx pkts=0, bytes=0, drop=0, errs=0, coll=0
  port  2: rx pkts=295161, bytes=26161341, drop=0, errs=0, frame=0, over=0, crc=0
           tx pkts=473559, bytes=126584447, drop=0, errs=0, coll=0

OpenFlowコントローラ

OpenFlowコントローラは、OpenFlowフレームワークのTremaを用いて実装しました。

  • ハード本体:Intel NUC NUC5i5RY
  • OS:Ubuntu 14.04
  • ソフト:ImageMagick 6.7.7-10
  • ソフト:Ruby 2.2.4
    • trema 0.10.1 : OpenFlowコントローラ
    • rmagick 2.15.4 : 画像操作ライブラリ

各メソッドの簡易的な説明です。

switch_readyメソッド
逆方向のフロー(外部から内部へのパケット送信)とブロードキャストのフローをFLOW_MODでOpenFlowスイッチに設定しています。今回必要なのは内部から外部へのICMPパケットのため、それ以外のフローを事前に設定しています。

packet_inメソッド:
PACKET_INの処理をするメソッドです。send_bitmap_icmp_replyメソッドの実行で例外が発生すると、send_normal_packetでそのままPACKET_OUTします。

send_normal_packetメソッド:
PACKET_INで内部ポートから入ってきたパケットを外部ポートへ送信(PACKET_OUT)します。

send_bitmap_icmp_replyメソッド:
PACKET_INしたパケットがICMP Echo Requestかどうかの判定と、ICMP Unreachableを送信する対象化の判断をします。

send_icmp_unreachableメソッド:
ICMP Echo Requestに対するICMP Unreachableを生成し、送信元に送信しています。Pio::Icmp::Replyで生成し、echo_dataに元のICMP Echo RequestのIPヘッダとIPペイロードを入れています(イーサネットヘッダを取り除いた部分)。

bitmapメソッド:
宛先・送信元IPをキーに@bitmapから@counterに基づいて許可・遮断の条件を読み出します。読みだした後にカウンタの@counterに1を足します。@counterにはICMPの受信回数が入ります。

load_bitmap_imageメソッド:
画像を読み込み2値化します。白はfalse、黒はtrueの2値化結果を@bitmapという名の1次元配列に保存します。画像の2値化の方法はこのサイトを参考にさせていただきました。

trema_controller.rb
require 'rmagick'

INTERNAL_PORT = 2
EXTERNAL_PORT = 1
ETHERNET_HEADER_SIZE = 14

# TremaController
class TremaController < Trema::Controller
  def start(_args)
    logger.info "#{name} started."

    @bitmap = load_bitmap_image('img.png')    # 2値化した画像の1次元配列
    @counter = Hash.new { |h, k| h[k] = 0 }   # ICMPパケットの送信カウンタ
  end

  def switch_ready(datapath_id)
    logger.info "switch_ready: #{datapath_id}"
    # 外部ポートから内部ポートへの転送フロー
    send_flow_mod_add(
      datapath_id,
      match: Match.new(in_port: EXTERNAL_PORT),
      actions: SendOutPort.new(INTERNAL_PORT)
    )
    # ブロードキャストパケットをフラッディングさせるフロー
    send_flow_mod_add(
      datapath_id,
      match: Match.new(destination_mac_address: 'FF:FF:FF:FF:FF:FF'),
      actions: SendOutPort.new(:flood)
    )
  end

  def switch_disconnected(datapath_id)
    logger.info "switch_disconnected: #{datapath_id}"
  end

  def packet_in(datapath_id, message)
    begin
      # 条件に一致した場合、ICMP Unreachableを送信する
      send_bitmap_icmp_reply(datapath_id, message)
      return
    rescue => e
    end
    # 内部ポートから外部ポートへパケットを送信する
    send_normal_packet(datapath_id, message)
  end

  private

  def send_normal_packet(datapath_id, message)
    send_packet_out(
      datapath_id,
      packet_in: message,
      actions: SendOutPort.new(EXTERNAL_PORT)
    )
  end

  def send_bitmap_icmp_reply(datapath_id, message)
    request = Pio::Icmp.read(message.raw_data)
    raise 'NotIcmpRequest' unless request.class == Pio::Icmp::Request
    raise 'NotTargetRequest' if bitmap(
      request.destination_ip_address,
      request.source_ip_address
    )
    send_icmp_unreachable(datapath_id, request)
  end

  def send_icmp_unreachable(datapath_id, req)
    reply = Pio::Icmp::Reply.new(
      source_mac:             req.destination_mac,
      destination_mac:        req.source_mac,
      source_ip_address:      req.destination_ip_address,
      destination_ip_address: req.source_ip_address,
      identifier:             req.icmp_identifier,
      sequence_number:        req.icmp_sequence_number,
      echo_data:              req.to_binary[ETHERNET_HEADER_SIZE..-1]
    )
    reply.icmp_type = 3
    reply.icmp_code = 1

    send_packet_out(
      datapath_id,
      raw_data: reply.to_binary,
      actions: SendOutPort.new(INTERNAL_PORT)
    )
  end

  def bitmap(dst_ip, src_ip)
    result = @bitmap[@counter[[dst_ip, src_ip]] % @bitmap.size]
    @counter[[dst_ip, src_ip]] += 1
    result
  end

  def load_bitmap_image(filepath)
    # 参考: http://blog.livedoor.jp/itukano/archives/51838000.html
    bitmap = []
    img = Magick::ImageList.new(filepath)
    img.rows.times do |row|
      img.columns.times do |col|
        px = img.export_pixels(col, row, 1, 1)
        # フルカラー画像を2値化
        t_px = (px[0] * 0.30 + px[1] * 0.59 + px[2] * 0.11)
        bitmap << (t_px > 170 * 257 ? false : true)
      end
    end
    bitmap
  end
end
実行結果
$ trema --version
trema version 0.10.1
$ trema run trema_controller.rb
TremaController started.
switch_ready: 57818231803846

Ciscoルータ

CISCO1812JでVRFを設定し、1台で送信元ルータと宛先ルータとして動作させています。

  • ハード本体:Cisco 1812-J
  • バージョン:15.1(4)M4

ルータのポート

  • FastEthernet0:内部ポート、192.168.1.1/30、VRF:VPN1
  • FastEthernet1:外部ポート、192.168.1.2/30、VRF:VPN2
  • FastErhernet9(Vlan1):管理ポート、192.168.88.2(DHCP割り当て)、VRF:Global

内部ポートから外部ポートへPINGを実行
VRFを指定して、VPN1からVPN2へPINGを実行しています。

pingコマンドの実行方法
c1812j#ping vrf VPN1 192.168.1.2 repeat 3500
CISCO1812-Jコンフィグ
version 15.1
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname c1812j
!
boot-start-marker
boot-end-marker
!
!
vrf definition VPN1
 !
 address-family ipv4
 exit-address-family
!
vrf definition VPN2
 !
 address-family ipv4
 exit-address-family
!
enable secret 4 tnhtc92DXBhelxjYk8LWJrPV36S2i4ntXrpb4RFmfqY
!
aaa new-model
!
!
!
!
!
!
!
aaa session-id common
!
crypto pki token default removal timeout 0
!
!
dot11 syslog
ip source-route
!
!
!
!
!
ip cef
ip domain name local
no ipv6 cef
!
multilink bundle-name authenticated
!
!
!
license udi pid CISCO1812-J/K9 sn **********
username cisco password 0 cisco
!
!
! 
!
!
!
!
!
!
interface BRI0
 no ip address
 encapsulation hdlc
 shutdown
!
interface FastEthernet0
 vrf forwarding VPN1
 ip address 192.168.1.1 255.255.255.252
 duplex auto
 speed auto
!
interface FastEthernet1
 vrf forwarding VPN2
 ip address 192.168.1.2 255.255.255.252
 duplex auto
 speed auto
!
interface FastEthernet2
 no ip address
!
interface FastEthernet3
 no ip address
!
interface FastEthernet4
 no ip address
!
interface FastEthernet5
 no ip address
!
interface FastEthernet6
 no ip address
!
interface FastEthernet7
 no ip address
!
interface FastEthernet8
 no ip address
!
interface FastEthernet9
 no ip address
!
interface Vlan1
 ip address dhcp
!
ip forward-protocol nd
no ip http server
no ip http secure-server
!
!
ip route vrf VPN1 0.0.0.0 0.0.0.0 FastEthernet0 192.168.1.2
ip route vrf VPN2 0.0.0.0 0.0.0.0 FastEthernet1 192.168.1.1
!
no cdp run
!
!
!
!
!
!
!
!
control-plane
!
!
!
line con 0
line aux 0
line vty 0 4
 transport input telnet ssh
line vty 5 15
 transport input telnet ssh
!
end

実行結果

下記の動画が実行結果になります。

実行結果

参考文献

22
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
25