Ciscoルータのpingコマンドでアスキーアートを描いてみました。
OpenFlowでICMPパケットを許可・遮断を細かく制御して実現しています。OpenFlowコントローラはTrema、OpenFlowスイッチはOpen vSwitchを利用してます。
OpenFlowをTremaとOVSで試してみました!Ciscoルータから送信されたICMPをOFスイッチで制御してアスキーアートを描きました。 pic.twitter.com/TsX1q1F7Bz
— kooshin (@kooshin) 2016年3月13日
PINGアスキーアート
Ciscoルータのpingコマンドは、宛先にICMPパケットを送信し、その結果に応じて1文字で表現されます。正常時は「!」、到達不可時は「.」など、結果に応じて文字は変わります(参考)。1行あたり70回分の実行結果が表示されます。PINGアスキーアートではこのCiscoルータの仕様を利用し、OpenFlowでICMPパケットを制御し、pingの実行結果を変化させてアスキーアートを描きます。
図. Ciscoルータのpingコマンドの実行結果(1行70文字)
全体の構成
PINGアスキーアートを実現するために、OpenFlowでICMPパケットを制御します。CiscoルータにOpenFlowスイッチを下図のように接続します。ルータから送信されるICMPパケットはOpenFlowスイッチを経由し、OpenFlowコントローラに伝えられ、許可と遮断を制御します。
アスキーアートの元画像
OpenFlowコントローラでは、アスキーアートとして描画する下図のような画像を読み込み2値化し、黒ピクセルは許可、白ピクセルは遮断として読み込みます。読み込む画像はpingコマンドの実行結果に合わせて幅70ピクセルとなります。
サンプルで用意した画像は下図になります。幅70ピクセル、高さ50ピクセルで合計3500ピクセルあります。pingコマンドで繰り返す回数(pingのrepeatオプション)に3500を指定すればちょうどです。
制御方法
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コントローラは別筐体で実行しています。
OpenFlowスイッチ
OpenFlowスイッチは下記のとおり、LIVA本体にUSB型のLANアダプタを2つ接続し、合計3つのNICを持っています。LIVAにUbuntu14.04とOpen vSwitchをインストールして、OpenFlowスイッチとして動作させます。
- ハード本体:LIVA LIVA-B3-2G-32G
- LANアダプタ:Logitech LAN-GTJU3H3
- LANアダプタ:Logitech LAN-GTJU3
- OS:Ubuntu 14.04
- ソフト:Open vSwitch 2.5.0
NIC
eth1とeth2をOpenFlowスイッチのポートとし、レイヤ2ブリッジさせます。OpenFlowスイッチの名前はofs0としています。
- eth0: 管理ポート
- eth1: 内部ポート
- eth2: 外部ポート
$ 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値化の方法はこのサイトを参考にさせていただきました。
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を実行しています。
c1812j#ping vrf VPN1 192.168.1.2 repeat 3500
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
実行結果
下記の動画が実行結果になります。
参考文献
- ping および traceroute コマンドについて Cisco Systems
- [Ruby]Rubyで画像処理をやってみる[RMagick][CentOS] -ε-いつかのブログ-з-
- [増補改訂版]クラウド時代のネットワーク技術 OpenFlow実践入門 - 技術評論社
- TremaでOpenFlowプログラミング - 上記のWeb版