コントローラプログラムの中で、各パケットを分析してプロトコルタイプや L4 ポート番号などフロー識別のための方法を調べる
まず入力は ylbswitch.py であれば、
1. class ylbswitch の
2. def install の
3. inst.register_for_packet_in( ) で、
4. packet_in_callback( ) を指定し、
5. これが packet_in_callback(dpid, inport, reason, len, bufid, packet) を引数として呼ばれてくる
パース処理は packet.parse とするとパースしてくれる模様。(謎)
ところで中では誰かがどこかで packet.parse とやってパースしてやらないといけないはずだが、それは ylbswitch.py にはない。
パース済みかどうかは packet.parsed を参照すると得られるらしい。
パース済みであれば packet.src で Ethernet source address, packet.dst で destination address などが得られる模様。
が、このメンバ変数(プロパティ)の一覧がどこにあるか、そのドキュメントのありかが分からない。
しょうがないので実装を見る。src/nox/lib/packet/ 以下にいろいろ用意されている。
最初は packet.parse 処理をどこかで誰かがやってくれているようだが、その ethernet 以降の上層のパースについては iph = packet.find('ipv4') などして手でパースさせる必要がある。
例えばこのディレクトリにある ethernet.py には def parse として実装記述有り。
またここで parsed プロパティに True を設定している。
仕組みはともかく、名前の一覧が無いと困るのでソースを見る。
たとえば ethernet.py の parse( ) 実装を見ると、以下のプロパティが設定されることがわかる。
ethernet.py
src, dst MAC アドレス
type プロトコル type
hdr_len ヘッダ長
payload_len ボディ長
同様に ipv4.py を見ると __init__ では以下のようになってる。まあこれだろう。
self.v = 4
self.hl = ipv4.MIN_LEN / 4
self.tos = 0
self.iplen = ipv4.MIN_LEN
self.id = int(time.time())
ipv4.ip_id += 1
self.flags = 0
self.frag = 0
self.ttl = 64
self.protocol = 0
self.csum = 0
self.srcip = 0
self.dstip = 0
tcp.py は
self.srcport = 0 # 16 bit
self.dstport = 0 # 16 bit
self.seq = 0 # 32 bit
self.ack = 0 # 32 bit
self.off = 0 # 4 bits
self.res = 0 # 4 bits
self.flags = 0 # reserved, 2 bits flags 6 bits
self.win = 0 # 16 bits
self.csum = 0 # 16 bits
self.urg = 0 # 16 bits
self.tcplen = 20 # Options?
self.options = []
あと paser() でこんなのを出してる。
self.hdr_len = self.off * 4
self.payload_len = dlen - self.hdr_len
self.tcplen = dlen
udp.py は
self.srcport = 0
self.dstport = 0
self.len = 8
self.csum = 0
self.payload = ''
あと paser() でこんなのを出してる。
self.hdr_len = udp.MIN_LEN
self.payload_len = self.len - self.hdr_len
icmp.py だと __init__ で
self.type = 0
self.code = 0
self.csum = 0
などなど。
そしてこれらの変数は if ( packet.type == ethernet.LLDP_TYPE ) などとして比較されるのだが、この値がどこに定義されているかも謎。
これもやはり実装を見る?( src/nox/lib/packet/ 以下)
実装としては ethernet というオブジェクトの定義は(当然) ethernet クラスにある。
例えば pyswitch.py などにしても、冒頭で
from nox.lib.packet.ethernet import ethernet
とやって import し、そこに含まれている class ethernet という定義をひっぱってきて、
if packet.type == ethernet.LLDP_TYPE:
return CONTINUE
などと書いている。(つまりこの ethernet というのはクラス名なのか!)
ethernet.py だと
定数として
ETHER_ANY = "\x00\x00\x00\x00\x00\x00"
ETHER_BROADCAST = "\xff\xff\xff\xff\xff\xff"
BRIDGE_GROUP_ADDRESS = "\x01\x80\xC2\x00\x00\x00"
LLDP_MULTICAST = "\x01\x80\xc2\x00\x00\x0e"
PAE_MULTICAST = '\x01\x80\xc2\x00\x00\x03' # 802.1x Port Access Entity
NDP_MULTICAST = '\x01\x23\x20\x00\x00\x01' # Nicira discovery
__init__ の中に
IP_TYPE = 0x0800
ARP_TYPE = 0x0806
RARP_TYPE = 0x8035
VLAN_TYPE = 0x8100
LLDP_TYPE = 0x88cc
PAE_TYPE = 0x888e # 802.1x Port Access Entity
ipv4.py だと __init__ に
IPv4 = 4
ICMP_PROTOCOL = 1
TCP_PROTOCOL = 6
UDP_PROTOCOL = 17
udp.py には特に何も無し
tcp.py だと TCP option の名前とか、FIN, SYN, RST といったフラグビットとか。
icmp.py だと定数として以下のものあり。
TYPE_ECHO_REPLY = 0
TYPE_DEST_UNREACH = 3
TYPE_SRC_QUENCH = 4
TYPE_REDIRECT = 5
TYPE_ECHO_REQUEST = 8
TYPE_TIME_EXCEED = 11
CODE_UNREACH_NET = 0
CODE_UNREACH_HOST = 1
CODE_UNREACH_PROTO = 2
CODE_UNREACH_PORT = 3
CODE_UNREACH_FRAG = 4
CODE_UNREACH_SRC_RTE = 5
具体的にコントローラプログラムに渡されてくるパケットデータ(Ethernet フレーム)から、より上層の(プロトコルタイプに応じた)当該フィールドを取り出す方法について調べる。
実は example の dnsspy.py が結構サンプルとしては良い。 まず callback 関数として登録したパケット処理関数には packet という引数で「captured data」なるフォーマットでデータが渡ってくる、とある。 しかし実際にはここは packet と名付けられた引数で、かつ中身は ethernet インスタンスで渡される。 そこで Ethernet ヘッダのプロパティは packet.type や packet.src などとすれば直接取り出せる。
次に IP ヘッダを取り出すには、この ethernet インスタンスから IP ヘッダを取り出す必要がある。
なぜか find という関数名になっている。
iph = packet.find('ipv4')
などとする。へええ、という感じ。
問題はこの先、ip header ではなく ip body からどうやって TCP ヘッダを取り出すか?と思ったら find は ethernet packet そのものを相手にヘッダを取り出してくれる。(だから find ?)
まず IP ヘッダを取り出し、それのプロトコルを見て UDP だったら find で再び(Ethernet フレーム全体から) UDP ヘッダを取り出す、といった感じ。
if packet.type == ethernet.IP_TYPE:
iph = packet.find('ipv4')
if iph.protocol == ipv4.ICMP_PROTOCOL:
# 何らか ICMP だった場合の処理
elif iph.protocol == ipv4.UDP_PROTOCOL:
udph = packet.find('udp')
if udph.dstport == 53:
この find で失敗すると(例えばICMPパケットの ethernet インスタンスに向かって find('udp') と言ったとか)、結果は no になる。
エラー判定のサンプルを dnsspy.py から取り出すと以下のようになる。
dnsh = packet.find('dns')
if not dnsh:
log.err('received invalid DNS packet',system='dnsspy')
return CONTINUE
こんな感じ。
というわけで「Ethernet で、IPv4で、ICMP かどうかを識別する」コードを検討する。
条件としては、
・packet.type が ethernet.IP_TYPE で
・packet.protocol が ipv4.ICMP_PROTOCOL であることを確認
できれば良い。
具体的には packet が ethrenet インスタンスなので、IP より上の層で判定する場合には手動で parse する必要が有る点に注意。
以下に pyswitch に手を入れる箇所を抜粋して説明する。
from nox.lib.packet.ethernet import ethernet <<< これは元からあった
from nox.lib.packet.ipv4 import ipv4 <<< この行を追加した
def forward_l2_packet(dpid, inport, packet, buf, bufid):
dstaddr = packet.dst.tostring()
if not ord(dstaddr[0]) & 1 and inst.st[dpid].has_key(dstaddr):
prt = inst.st[dpid][dstaddr]
if prt[0] == inport:
log.err('**warning** learned port = inport', system="pyswitch")
inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)
else:
# Yasu update. add process depends packet type. <<< ここから記述追加
if packet.type == ethernet.IP_TYPE:
iph = packet.find('ipv4') <<< ここで ethernet インスタンスである packet から ipv4 インスタンスである iph を生成
if iph.protocol == ipv4.ICMP_PROTOCOL: << で、その後は iph インスタンスのプロパティを調べる
# 何らか ICMP だった場合の処理
else:
# 何らか ICMP 以外の IP だった場合の処理
else:
# IP 以外(ARP 等)の処理 <<< ここまで
しばらくこれを頼りに書くが、何かしらリファレンスマニュアルがあるのだとは思う。