コントローラプログラムの中で、各パケットを分析してプロトコルタイプや 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 等)の処理 <<< ここまで
しばらくこれを頼りに書くが、何かしらリファレンスマニュアルがあるのだとは思う。