#!/usr/bin/env python | |
#--------------------------------------------------------------------------# | |
# Copyright (C) 2015 - 2016 by Tibit Communications, Inc. # | |
# All rights reserved. # | |
# # | |
# _______ ____ _ ______ # | |
# /_ __(_) __ )(_)_ __/ # | |
# / / / / __ / / / / # | |
# / / / / /_/ / / / / # | |
# /_/ /_/_____/_/ /_/ # | |
# # | |
#--------------------------------------------------------------------------# | |
""" EOAM protocol implementation in scapy """ | |
TIBIT_VERSION_NUMBER = '1.1.4' | |
import argparse | |
import logging | |
import time | |
from hexdump import hexdump | |
from datetime import datetime | |
logging.getLogger("scapy.runtime").setLevel(logging.ERROR) | |
from scapy.layers.l2 import Ether, Dot1Q | |
from scapy.sendrecv import sendp | |
from scapy.fields import PacketField | |
from scapy.packet import bind_layers | |
from scapy.fields import StrField, X3BytesField | |
from scapy.packet import Packet | |
from scapy.fields import ByteEnumField, XShortField, XByteField, MACField, \ | |
ByteField, BitEnumField, BitField, ShortField | |
from scapy.fields import XLongField, StrFixedLenField, XIntField, \ | |
FieldLenField, StrLenField, IntField | |
import fcntl, socket, struct # for get hw address | |
from EOAM_Layers import EOAM_MULTICAST_ADDRESS, IGMP_MULTICAST_ADDRESS, OAM_ETHERTYPE | |
from EOAM_Layers import VENDOR_SPECIFIC_OPCODE, CABLELABS_OUI, TIBIT_OUI | |
from EOAM_Layers import RxedOamMsgTypeEnum, RxedOamMsgTypes | |
from EOAM_Layers import EOAMPayload, EOAM_EventMsg, EOAM_VendSpecificMsg, EOAM_TibitMsg, EOAM_DpoeMsg, EOAM_OmciMsg | |
# TODO should remove import * | |
from EOAM_TLV import * | |
ADTRAN_SHORTENED_VSSN = u'4144' # 'AD' | |
TIBIT_SHORTENED_VSSN = u'5442' # 'TB' | |
def get_oam_msg_type(log, frame): | |
respType = RxedOamMsgTypeEnum["Unknown"] | |
recv_frame = frame | |
if recv_frame.haslayer(EOAMPayload): | |
if recv_frame.haslayer(EOAM_EventMsg): | |
respType = RxedOamMsgTypeEnum["Event Notification"] | |
elif recv_frame.haslayer(EOAM_OmciMsg): | |
respType = RxedOamMsgTypeEnum["OMCI Message"] | |
else: | |
dpoeOpcode = 0x00 | |
if recv_frame.haslayer(EOAM_TibitMsg): | |
dpoeOpcode = recv_frame.getlayer(EOAM_TibitMsg).dpoe_opcode; | |
elif recv_frame.haslayer(EOAM_DpoeMsg): | |
dpoeOpcode = recv_frame.getlayer(EOAM_DpoeMsg).dpoe_opcode; | |
# Get Response | |
if (dpoeOpcode == DPoEOpcodes["Get Response"]): | |
respType = RxedOamMsgTypeEnum["DPoE Get Response"] | |
# Set Response | |
elif (dpoeOpcode == DPoEOpcodes["Set Response"]): | |
respType = RxedOamMsgTypeEnum["DPoE Set Response"] | |
# File Transfer ACK | |
elif (dpoeOpcode == DPoEOpcodes["File Transfer"]): | |
respType = RxedOamMsgTypeEnum["DPoE File Transfer"] | |
else: | |
log.info("Unsupported DPoE Opcode {:0>2X}".format(dpoeOpcode)) | |
else: | |
log.info("Invalid OAM Header") | |
log.info('Received OAM Message - %s' % RxedOamMsgTypes[respType]) | |
return respType | |
def handle_get_value(log, loadstr, startOfTlvs, queryBranch, queryLeaf): | |
retVal = False; | |
value = 0 | |
branch = 0 | |
leaf = 0 | |
bytesRead = startOfTlvs | |
loadstrlen = len(loadstr) | |
while (bytesRead <= loadstrlen): | |
(branch, leaf) = struct.unpack_from('>BH', loadstr, bytesRead) | |
if (branch != 0): | |
bytesRead += 3 | |
length = struct.unpack_from('>B', loadstr, bytesRead)[0] | |
bytesRead += 1 | |
if (length == 1): | |
value = struct.unpack_from(">B", loadstr, bytesRead)[0] | |
elif (length == 2): | |
value = struct.unpack_from(">H", loadstr, bytesRead)[0] | |
elif (length == 4): | |
value = struct.unpack_from(">I", loadstr, bytesRead)[0] | |
elif (length == 8): | |
value = struct.unpack_from(">Q", loadstr, bytesRead)[0] | |
else: | |
if (length >= 0x80): | |
log.info('Branch 0x{:0>2X} Leaf 0x{:0>4X} {}'.format(branch, leaf, DPoEVariableResponseEnum[length])) | |
# Set length to zero so bytesRead doesn't get mistakenly incremented below | |
length = 0 | |
else: | |
# Attributes with a length of zero are actually 128 bytes long | |
if (length == 0): | |
length = 128; | |
valStr = ">{}s".format(length) | |
value = struct.unpack_from(valStr, loadstr, bytesRead)[0] | |
if (length > 0): | |
bytesRead += length | |
if (branch != OamBranches["DPoE Object"]): | |
if ( ((queryBranch == 0) and (queryLeaf == 0)) or | |
((queryBranch == branch) and (queryLeaf == leaf)) ): | |
# Prevent zero-lengthed values from returning success | |
if (length > 0): | |
retVal = True; | |
break | |
else: | |
break | |
if (retVal == False): | |
value = 0 | |
return retVal,bytesRead,value,branch,leaf | |
def get_value_from_msg(log, frame, branch, leaf): | |
retVal = False | |
value = 0 | |
recv_frame = frame | |
if recv_frame.haslayer(EOAMPayload): | |
payload = recv_frame.payload | |
if hasattr(payload, 'body'): | |
loadstr = payload.body.load | |
# Get a specific TLV value | |
(retVal,bytesRead,value,retbranch,retleaf) = handle_get_value(log, loadstr, 0, branch, leaf) | |
else: | |
log.info('received frame has no payload') | |
else: | |
log.info('Invalid OAM Header') | |
return retVal,value, | |
def check_set_resp_attrs(log, loadstr, startOfTlvs): | |
retVal = True; | |
branch = 0 | |
leaf = 0 | |
length = 0 | |
bytesRead = startOfTlvs | |
loadstrlen = len(loadstr) | |
while (bytesRead <= loadstrlen): | |
(branch, leaf) = struct.unpack_from('>BH', loadstr, bytesRead) | |
# print "Branch/Leaf 0x{:0>2X}/0x{:0>4X}".format(branch, leaf) | |
if (branch != 0): | |
bytesRead += 3 | |
length = struct.unpack_from('>B', loadstr, bytesRead)[0] | |
# print "Length: 0x{:0>2X} ({})".format(length,length) | |
bytesRead += 1 | |
if (length >= 0x80): | |
log.info('Branch 0x{:0>2X} Leaf 0x{:0>4X} {}'.format(branch, leaf, DPoEVariableResponseEnum[length])) | |
if (length > 0x80): | |
retVal = False; | |
break; | |
else: | |
bytesRead += length | |
else: | |
break | |
return retVal,branch,leaf,length | |
def check_set_resp(log, frame): | |
rc = False | |
branch = 0 | |
leaf = 0 | |
status = 0 | |
recv_frame = frame | |
if recv_frame.haslayer(EOAMPayload): | |
payload = recv_frame.payload | |
if hasattr(payload, 'body'): | |
loadstr = payload.body.load | |
# Get a specific TLV value | |
(rc,branch,leaf,status) = check_set_resp_attrs(log, loadstr, 0) | |
else: | |
log.info('received frame has no payload') | |
else: | |
log.info('Invalid OAM Header') | |
return rc,branch,leaf,status | |
def handle_get_event_context(log, loadstr, startOfTlvs, queryType): | |
retVal = False; | |
value = 0 | |
objType = 0 | |
bytesRead = startOfTlvs | |
loadstrlen = len(loadstr) | |
while (bytesRead <= loadstrlen): | |
objType = struct.unpack_from('>H', loadstr, bytesRead)[0] | |
# print "Branch/Leaf 0x{:0>2X}/0x{:0>4X}".format(branch, leaf) | |
if (objType != 0): | |
bytesRead += 2 | |
length = struct.unpack_from('>B', loadstr, bytesRead)[0] | |
# print "Length: 0x{:0>2X} ({})".format(length,length) | |
bytesRead += 1 | |
if (length == 1): | |
value = struct.unpack_from(">B", loadstr, bytesRead)[0] | |
elif (length == 2): | |
value = struct.unpack_from(">H", loadstr, bytesRead)[0] | |
elif (length == 4): | |
value = struct.unpack_from(">I", loadstr, bytesRead)[0] | |
elif (length == 8): | |
value = struct.unpack_from(">Q", loadstr, bytesRead)[0] | |
else: | |
valStr = ">{}s".format(length) | |
value = struct.unpack_from(valStr, loadstr, bytesRead)[0] | |
# print "Value: {}".format(value) | |
if (length > 0): | |
bytesRead += length | |
if ( (queryType == 0) or (queryType == objType) ): | |
# Prevent zero-lengthed values from returning success | |
if (length > 0): | |
retVal = True; | |
break | |
else: | |
break | |
if (retVal == False): | |
value = 0 | |
return retVal,bytesRead,value,objType | |
def handle_tibit_oam_event(log, loadstr): | |
bytesRead = 0 | |
loadstrlen = len(loadstr) | |
if loadstrlen > 0: | |
rc = True | |
num_iters = 0 | |
bytesRead = 0 | |
link_mac = "" | |
msg = "" | |
# Theare are two contexts in a Tibit-specific event - Source & Reference Contexts | |
while(rc == True and num_iters < 2): | |
objType = 0 | |
(rc,bytesRead,value,objType) = handle_get_event_context(log, loadstr, bytesRead, objType) | |
if (rc == True): | |
if objType == 0x0001: | |
# print "PON Object 0x{:0>4X} Value = {}".format(objType, value) | |
pass | |
elif objType == 0x000A: | |
# This is a Unicast Logical Link context. Determine if this a GPON or EPON link | |
if value[1:5] == "TBIT": | |
# | |
link_mac = ''.join(s.encode('hex') for s in value[1:3]) | |
link_mac += ''.join(s.encode('hex') for s in value[5:9]) | |
else: | |
link_mac = ''.join(s.encode('hex') for s in value[1:7]) | |
# print "Unicast Logical Link Object 0x{:0>4X} Value = {}".format(objType, link_mac) | |
else: | |
log.info("Object Type 0x{:0>4X} value = {}".format(objType, value)) | |
elif (branch != 0): | |
log.error("Object Type 0x{:0>4X} no value".format(objType)) | |
num_iters += 1 | |
# Pull the Event Code and Event Length out of the event | |
(evtCode, evtLen) = struct.unpack_from('>HB', loadstr, bytesRead) | |
bytesRead += 3 | |
# print "Event Code : 0x{:0>4X}".format(evtCode) | |
# print "Event Len : 0x{:0>4X}".format(evtLen) | |
# Tibit Registration Event | |
if (evtCode == 0x0001): | |
# Handle Registration Status attribute | |
regStatus = struct.unpack_from('>B', loadstr, bytesRead)[0] | |
if regStatus == 1: | |
msg = "Link {} Registered".format(link_mac) | |
else: | |
msg = "Link {} Deregistered".format(link_mac) | |
return objType,evtCode,msg | |
def handle_dpoe_oam_event(log, loadstr): | |
bytesRead = 0 | |
loadstrlen = len(loadstr) | |
if loadstrlen > 0: | |
(evtCode, raised, objType) = struct.unpack_from('>BBH', loadstr, bytesRead) | |
bytesRead += 4 | |
# print "Event Code : 0x{:0>4X}".format(evtCode) | |
# print "Event Len : 0x{:0>4X}".format(evtLen) | |
if ((loadstrlen - bytesRead) == 2): | |
objInst = struct.unpack_from(">H", loadstr, bytesRead)[0] | |
elif ((loadstrlen - bytesRead) == 4): | |
objInst = struct.unpack_from(">I", loadstr, bytesRead)[0] | |
objTypeStr = ObjectContextEnum[objType] | |
evtCodeStr = DPoEEventCodeEnum[evtCode] | |
raisedStr = "Raised" | |
if (raised): | |
rasiedStr = "Cleared" | |
#print "{} : {} - {} {}".format(objTypeStr, objInst, evtCodeStr, raisedStr) | |
return objType,evtCode,objTypeStr+":"+evtCodeStr | |
def handle_oam_event(log, frame): | |
recv_frame = frame | |
if recv_frame.haslayer(EOAM_EventMsg): | |
now = datetime.now().strftime('%Y-%m-%f %H:%M:%S.%f') | |
event = recv_frame.getlayer(EOAM_EventMsg) | |
if hasattr(event, 'body'): | |
loadstr = event.body.load | |
if (event.tlv_type != VENDOR_SPECIFIC_OPCODE): | |
log.error("unexpected tlv_type 0x%x (expected 0xFE)" % event.tlv_type) | |
elif (event.oui == CABLELABS_OUI): | |
log.info("DPoE Event") | |
objType,eventCode,msg = handle_dpoe_oam_event(log, loadstr) | |
elif (event.oui == TIBIT_OUI): | |
log.info("Tibit-specific Event") | |
objType,eventCode,msg = handle_tibit_oam_event(log, loadstr) | |
log.info("Description: %s" % msg) | |
log.info("sequence: 0x%04x" % event.sequence) | |
log.info("tlv_type: 0x%x" % event.tlv_type) | |
log.info("length: 0x%x" % event.length) | |
log.info("oui: 0x%06x" % event.oui) | |
log.info("time_stamp: %s" % now) | |
log.info("obj_type: "+hex(objType)) | |
log.info("event_code: "+hex(eventCode)) | |
# TODO - Store the event for future use or generate alarm | |
#event_data = [msg, event.sequence, objType, eventCode, now] | |
def handle_omci(log, frame): | |
recv_frame = frame | |
if recv_frame.haslayer(EOAM_OmciMsg): | |
omci = recv_frame.getlayer(EOAM_OmciMsg) | |
if hasattr(omci, 'body'): | |
loadstr = omci.body.load | |
#log.info("trans_id: 0x%04x" % omci.trans_id) | |
#log.info("msg_type: 0x%x" % omci.msg_type) | |
#log.info("dev_id: 0x%x" % omci.dev_id) | |
#log.info("me_class: 0x%04x" % omci.me_class) | |
#log.info("me_inst: 0x%04x" % omci.me_inst) | |
bytesRead = 0 | |
# TODO - Handle OMCI message | |
def handle_fx_ack(log, loadstr): | |
response_code = Dpoe_FileAckRspOpcodes["OK"] | |
(fx_opcode, acked_block, response_code) = struct.unpack('>BHB', loadstr[0:4]) | |
if (fx_opcode == Dpoe_FileXferOpcodes["File Transfer Ack"]): | |
pass | |
#log.info(" Acked_block: {} Code: {}".format(acked_block, DPoEFileAckRespCodeEnum[response_code])) | |
else: | |
log.error("Unexpected File Transfer Opcode {} when expecting ACK".format(DPoEFileXferOpcodeEnum[fx_opcode])) | |
return response_code,acked_block | |
def check_resp(log, frame): | |
respType = RxedOamMsgTypeEnum["Unknown"] | |
recv_frame = frame | |
if recv_frame.haslayer(EOAMPayload): | |
if recv_frame.haslayer(EOAM_EventMsg): | |
handle_oam_event(log, recv_frame) | |
elif recv_frame.haslayer(EOAM_OmciMsg): | |
handle_omci(log, recv_frame) | |
else: | |
dpoeOpcode = 0x00 | |
if recv_frame.haslayer(EOAM_TibitMsg): | |
dpoeOpcode = recv_frame.getlayer(EOAM_TibitMsg).dpoe_opcode; | |
elif recv_frame.haslayer(EOAM_DpoeMsg): | |
dpoeOpcode = recv_frame.getlayer(EOAM_DpoeMsg).dpoe_opcode; | |
if hasattr(recv_frame, 'body'): | |
payload = recv_frame.payload | |
loadstr = payload.body.load | |
# Get Response | |
if (dpoeOpcode == DPoEOpcodes["Get Response"]): | |
bytesRead = 0 | |
rc = True | |
while(rc == True): | |
branch = 0 | |
leaf = 0 | |
(rc,bytesRead,value,branch,leaf) = handle_get_value(log, loadstr, bytesRead, branch, leaf) | |
if (rc == True): | |
log.info('Branch 0x{:0>2X} Leaf 0x{:0>4X} value = {}'.format(branch, leaf, value)) | |
elif (branch != 0): | |
log.info('Branch 0x{:0>2X} Leaf 0x{:0>4X} no value'.format(branch, leaf)) | |
# Set Response | |
elif (dpoeOpcode == DPoEOpcodes["Set Response"]): | |
(rc,branch,leaf,status) = check_set_resp_attrs(loadstr, 0) | |
if (rc == True): | |
log.info('Set Response had no errors') | |
else: | |
log.info('Branch 0x{:X} Leaf 0x{:0>4X} {}'.format(branch, leaf, DPoEVariableResponseEnum[status])) | |
# File Transfer ACK | |
elif (dpoeOpcode == DPoEOpcodes["File Transfer"]): | |
(rc,block) = handle_fx_ack(log, loadstr) | |
else: | |
log.info('Unsupported DPoE Opcode {:0>2X}'.format(dpoeOpcode)) | |
else: | |
log.info('Invalid OAM Header') | |
return respType | |
def mcastIp2McastMac(ip): | |
""" Convert a dot-notated IPv4 multicast address string into an multicast MAC address""" | |
digits = [int(d) for d in ip.split('.')] | |
return '01:00:5e:%02x:%02x:%02x' % (digits[1] & 0x7f, digits[2] & 0xff, digits[3] & 0xff) | |
def get_olt_queue(mac, mode = None): | |
resultOltQueue = "" | |
if mode: | |
# If the MAC is the Multicast LLID, then use EPON encoding regardless of the actual | |
# mode we are in. | |
if (mac == "FFFFFFFFFFFF"): | |
mode = "EPON" | |
if mode.upper()[0] == "G": #GPON | |
if mac[:4].upper() == ADTRAN_SHORTENED_VSSN: | |
vssn = "ADTN" | |
else: | |
vssn = "TBIT" | |
link = int(mac[4:12], 16) | |
resultOltQueue = "PortIngressRuleResultOLTQueue(unicastvssn=\"" + vssn + "\", unicastlink=" + str(link) + ")" | |
else: #EPON | |
vssn = int(mac[0:8].rjust(8,"0"), 16) | |
link = int((mac[8:12]).ljust(8,"0"), 16) | |
resultOltQueue = "PortIngressRuleResultOLTEPONQueue(unicastvssn=" + str(vssn) + ", unicastlink=" + str(link) + ")" | |
return resultOltQueue | |
def get_unicast_logical_link(mac, mode = None): | |
unicastLogicalLink = "" | |
if mode: | |
if mode.upper()[0] == "G": #GPON | |
if mac[:4].upper() == ADTRAN_SHORTENED_VSSN: | |
vssn = "ADTN" | |
else: | |
vssn = "TBIT" | |
link = int(mac[4:12], 16) | |
unicastLogicalLink = "OLTUnicastLogicalLink(unicastvssn=\"" + vssn + "\", unicastlink=" + str(link) + ")" | |
else: #EPON | |
vssn = int(mac[0:8].rjust(8,"0"), 16) | |
link = int((mac[8:12]).ljust(8,"0"), 16) | |
unicastLogicalLink = "OLTEPONUnicastLogicalLink(unicastvssn=" + str(vssn) + ", unicastlink=" + str(link) +")" | |
return unicastLogicalLink | |
if __name__ == "__main__": | |
pass |