#------------------------------------------------------------------------- # IAX2 Call Analyzer # IAX2 call analysis (draw bandwidth, jitter, delay, loss, plots) # # You may use this code freely in your commercial and non-commercial work. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # Copyright (c) Unleash Networks 2005, All rights reserved #---------------------------------------------------------------------------- # Tim.V 2/2/06 Unleash Networks http://www.unleashnetworks.com require 'rubygems' require 'win32ole' require 'fox16' include Fox require 'UnleashCharts' include UnleashCharts # global constants USEC_PER_SEC=1000000 MSEC_PER_SEC=1000 USEC_PER_MSEC=1000 IAX_COL_COUNT=9 DEBUG_MODE=false class ChartWindow < FXMainWindow @tabBook @table @analysisIndex @chartsFwd @chartsRev def initialize(theapp) # base class super(theapp, "IAX2 Call Analysis (c) Unleash Networks", nil, nil, DECOR_ALL, 0,0,700,600) labelfont = FXFont.new(getApp(), "verdana", 10, FONTWEIGHT_BOLD) # main parent frame @main = FXVerticalFrame.new(self, FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y) @label = FXLabel.new(@main, "IAX2 Calls (double click on a call to analyze)" , nil, LAYOUT_FILL_X) @label.justify = JUSTIFY_LEFT @label.font=labelfont @label2 = FXLabel.new(@main, "Currently showing" , nil, LAYOUT_FILL_X) # Main window interior @splitter = FXSplitter.new(@main, (LAYOUT_SIDE_TOP|LAYOUT_FILL_X| LAYOUT_FILL_Y|SPLITTER_VERTICAL|SPLITTER_TRACKING)) @tableFrame = FXVerticalFrame.new(@splitter, FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y) @tabBook = FXTabBook.new(@splitter, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y) # raw data @table = FXTable.new(@tableFrame,nil,0, TABLE_COL_SIZABLE|LAYOUT_FILL_X|LAYOUT_FILL_Y, 0,0,0,0, 10,10,10,10) @table.setTableSize(0, 4) @table.setBackColor(FXRGB(255, 255, 255)) @table.setCellColor(0, 0, FXRGB(255, 255, 255)) @table.setCellColor(0, 1, FXRGB(255, 255, 255)) @table.setCellColor(1, 0, FXRGB(210, 210, 210)) @table.setCellColor(1, 1, FXRGB(210, 210, 215)) @splitter.setSplit(0, 130) @table.connect(SEL_DOUBLECLICKED,method(:onDblclicked)) @chartsFwd=Hash.new @chartsRev=Hash.new end def create super show(PLACEMENT_SCREEN) end def setModel (mod) @canvas.setModel( mod) end def addChart (name, model) tab = FXTabItem.new(@tabBook, name, nil) canvasFrame = FXVerticalFrame.new(@tabBook, FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y, 0,0,0,0, 0,0,0,0) canvas = UnTimeSeriesChart.new(canvasFrame) canvas.setModel( model) canvas.title="Both Directions" @chartsFwd[name]=canvas end def addChartBoth (name, modelFwd, modelRev) tab = FXTabItem.new(@tabBook, name, nil) canvasFrame = FXVerticalFrame.new(@tabBook, FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y, 0,0,0,0, 0,0,0,0) canvas1 = UnTimeSeriesChart.new(canvasFrame) # canvas1.backColor = FXRGB(255,255,255) # canvas1.drawColor = FXRGB(0,0,0) canvas1.setModel( modelFwd) canvas1.title="Forward Direction" canvas2 = UnTimeSeriesChart.new(canvasFrame) # canvas2.backColor = FXRGB(255,255,255) # canvas2.drawColor = FXRGB(0,0,0) canvas2.setModel( modelRev) canvas2.title="Reverse Direction" @chartsFwd[name]=canvas1 @chartsRev[name]=canvas2 end def initTable @table.rowHeaderMode=LAYOUT_NORMAL @table.setColumnText(0,"From->To IP, Call#") @table.setColumnText(1,"Duration") @table.setColumnText(2,"Calling #") @table.setColumnText(3,"Clng Nm") @table.setColumnText(4,"Calld #") @table.setColumnText(5,"DNID") @table.setColumnText(6,"User") @table.setColumnText(7,"Codec") @table.setColumnText(8,"Started") @table.setColumnWidth(0,280) @table.setColumnWidth(1,50) @table.setColumnWidth(2,80) @table.setColumnWidth(3,80) @table.setColumnWidth(4,80) @table.setColumnWidth(5,40) @table.setColumnWidth(6,60) @table.setColumnWidth(7,100) @table.setColumnWidth(8,120) end def loadCalls(ana) tableSize(ana.numValidCalls) initTable ana.loadCalls(@table) @analysisIndex=ana end def tableSize(rows) @table.setTableSize(rows,IAX_COL_COUNT) end def onDblclicked(sender,sel,pos) showCall(pos.row) end def showCall (callnum) call = @analysisIndex.getCompletedCall(callnum) print "showing call #{callnum}\n" @label2.text = "Currently Showing " + call.callDescr + " (# #{callnum + 1} in list)" if ( @chartsFwd.size()==0) addChartBoth("Call Bandwidth",CallBandwidthModel.new(call,0,InputFile),CallBandwidthModel.new(call,1,InputFile)) addChartBoth("Delay",DelayModel.new(call,0,InputFile),DelayModel.new(call,1,InputFile)) addChartBoth("Jitter",JitterModel.new(call,0,InputFile),JitterModel.new(call,1,InputFile)) addChartBoth("Loss",LossModel.new(call,0,InputFile),LossModel.new(call,1,InputFile)) addChart("IAX2 Events",CallEventModel.new(call,0,InputFile)) else @chartsFwd["Call Bandwidth"].setModel(CallBandwidthModel.new(call,0,InputFile)) @chartsFwd["Delay"].setModel(DelayModel.new(call,0,InputFile)) @chartsFwd["Jitter"].setModel(JitterModel.new(call,0,InputFile)) @chartsFwd["Loss"].setModel(LossModel.new(call,0,InputFile)) @chartsFwd["IAX2 Events"].setModel(CallEventModel.new(call,0,InputFile)) @chartsRev["Call Bandwidth"].setModel(CallBandwidthModel.new(call,1,InputFile)) @chartsRev["Delay"].setModel(DelayModel.new(call,1,InputFile)) @chartsRev["Jitter"].setModel(JitterModel.new(call,1,InputFile)) @chartsRev["Loss"].setModel(LossModel.new(call,1,InputFile)) end end end #----------------------------------------- # IAXCallLeg class #----------------------------------------- class IAXCallLeg attr_accessor :src_ip attr_accessor :src_call_no end #------------------------------------------ # IAXCall class #------------------------------------------ class IAXCall @callLegs attr_accessor :calling_no, :calling_name attr_accessor :called_no, :dnid attr_accessor :username attr_accessor :codec attr_accessor :startTimeSecs, :startTimeUSecs attr_accessor :endTimeSecs,:endTimeUSecs attr_accessor :startPacketID, :endPacketID attr_accessor :fHungUp def initialize @callLegs = Array.new @fHungUp = false end def match (ip , callno) @callLegs.each do |c| if (c.src_ip == ip && c.src_call_no==callno) return true end end return false end def createCallLeg(sip, scall) leg = IAXCallLeg.new leg.src_ip = sip leg.src_call_no = scall @callLegs << leg end def isCompleted @callLegs.size == 2 end def isActive? return !fHungUp end def matchLeg (legno, ip, call) if legno >= @callLegs.size return false end leg = @callLegs[legno] return leg.src_ip == ip && leg.src_call_no = call end def matchAnyLeg (ip, call) if @callLegs.size == 2 return matchLeg(0,ip,call) || matchLeg(1,ip,call) else return matchLeg(0,ipcall) end return false end def printCall print "----- IAX Call --------\n" if isCompleted print "Start Time: #{Time.at(@startTimeSecs,@startTimeUSecs)}\n" end print "Calling No: #{@calling_no} Calling Name: #{@calling_name}\n" print "Called No: #{@called_no} DNID=#{@dnid}\n" print "Username: #{@username}\n" print "Codec used: #{@codec}\n" @callLegs.each do |leg| print "\tCall Leg " print "\tIP Address: #{leg.src_ip} Call No: #{leg.src_call_no}\n" end print "------------------------\n\n" end def addToTable(tab,row) i=row #call description calldescr = callDescr duration_s = (@endTimeSecs-@startTimeSecs).to_s + " secs" tab.setRowText(i,(row+1).to_s) tab.setItemText(i,0,calldescr) # ipfrom tab.setItemText(i,1,duration_s) # analysis tab.setItemText(i,2,@calling_no) # callingname tab.setItemText(i,3,@calling_name) # payload bytes tab.setItemText(i,4,@called_no) # was RTT updated due to this segment tab.setItemText(i,5,@dnid) # analysis tab.setItemText(i,6,@username) # analysis tab.setItemText(i,7,@codec) # analysis tab.setItemText(i,8,Time.at(@startTimeSecs,@startTimeUSecs).to_s) # analysis (0..8).each do|col| tab.getItem(i,col).justify = FXTableItem::LEFT end end def callDescr leg1 = @callLegs[0] leg2 = @callLegs[1] "#{leg1.src_ip} -> #{leg2.src_ip} [#{leg1.src_call_no} -> #{leg2.src_call_no}]" end def sampleInterval codecProps = { "G.723.1" => 30, "GSM" => 20, "G.711u" => 20, "G.711a" => 20, "G.726" => 20, "IMA ADPCM" => 20, "16bit LLE" => 20, "LPC10" => 20, "G.729" => 10, "SPEEX" => 20, "ILBC" => 30, } codecProps[codec] end end #---------------------------------- # IAXAnalyzer class #---------------------------------- class IAXAnalyzer @capfile # capture file name @calls # all calls found in capture file def initialize (filename) @capfile = filename @calls = Array.new @callCacheCheck=nil # to speed up matching calls end # - scan the capture file looking for completed calls (IAX messages NEW and ACCEPT def extract_calls unsniffDB = WIN32OLE.new("Unsniff.Database") unsniffDB.OpenForRead(@capfile) allPackets = unsniffDB.PacketIndex count = allPackets.Count (0..count-1).each do |i| packet = allPackets.Item(i) iaxlayer = packet.FindLayer("IAX2") if (iaxlayer) findIAXCall(packet) end end # need to end incomplete calls endIncompleteCalls(allPackets.Item(count-1)) unsniffDB.Close end # - use the incoming iax2 packet for call information def findIAXCall(iaxpacket) iaxlayer = iaxpacket.FindLayer("IAX2") # only IAX control messages make sense to us frametype = iaxlayer.FindField("Frametype") if (!frametype) # we dont care about mini frames at the moment return end if frametype.value =~ /IAX Control/ sclass = iaxlayer.FindField(">FULL FRAME>SC_IAX>IAX Subclass") if (!sclass) print "Error in capture file, expecting subclass value\n" return end case sclass.value when /NEW/ procIAXCtlNew(iaxpacket) print "Found New Frame #{iaxpacket.ID}\n" if DEBUG_MODE when /ACCEPT/ procIAXCtlAccept(iaxpacket) print "Found ACCEPT Frame #{iaxpacket.ID}\n" if DEBUG_MODE when /HANGUP/ procIAXCtlHangup(iaxpacket) print "Found HANGUP Frame #{iaxpacket.ID}\n" if DEBUG_MODE else return end end end # a new call is being initiated, check if one already exists if not insert call def procIAXCtlNew(iaxpacket) iplayer = iaxpacket.FindLayer("IP") iaxlayer = iaxpacket.FindLayer("IAX2") ipfrom = iplayer.FindField("Source IP") iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number") if (! findActiveCall(ipfrom.value,iaxscall.value) ) initiateNewCall(iplayer, iaxlayer) end end # a new call was initiated as a result of the IAX control NEW message def initiateNewCall(iplayer,iaxlayer) c = IAXCall.new f_calling_no = iaxlayer.FindField("CALLING NUMBER") f_calling_name = iaxlayer.FindField("CALLING NAME") f_called_no = iaxlayer.FindField("CALLED NUMBER") f_dnid = iaxlayer.FindField("DNID") f_username = iaxlayer.FindField("USERNAME") c.calling_no = f_calling_no.value if f_calling_no c.called_no = f_called_no.value if f_called_no c.dnid = f_dnid.value if f_dnid c.username = f_username.value if f_username c.calling_name = f_calling_name.value if f_calling_name ipfrom = iplayer.FindField("Source IP") iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number") c.createCallLeg(ipfrom.value, iaxscall.value) @calls << c end # a call was completed via an ACCEPT message def procIAXCtlAccept(iaxpacket) iplayer = iaxpacket.FindLayer("IP") iaxlayer = iaxpacket.FindLayer("IAX2") ipfrom = iplayer.FindField("Source IP") iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number") ipto= iplayer.FindField("Dest IP") iaxdcall = iaxlayer.FindField(">FULL FRAME>Dest Call Number>Destination Call Number") c = findActiveCall(ipto.value,iaxdcall.value) if (!c) # found an accept with no corresponding new return end if (! c.isCompleted() ) completeCall(c,iaxpacket) end c.startPacketID = iaxpacket.ID end # complete call as a result of IAX ctrl ACCEPT message def completeCall(c,iaxpacket) iplayer = iaxpacket.FindLayer("IP") iaxlayer = iaxpacket.FindLayer("IAX2") f_format = iaxlayer.FindField("FORMAT") f_flags = f_format.SubFields f_flags.each do |f| if f.value =~ /1/ c.codec = f.name end end c.startTimeSecs = iaxpacket.TimestampSecs c.startTimeUSecs = iaxpacket.TimestampUSecs ipfrom = iplayer.FindField("Source IP") iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number") c.createCallLeg(ipfrom.value, iaxscall.value) end # hangup call as a result of IAX ctrl HANGUP message def procIAXCtlHangup(iaxpacket) iplayer = iaxpacket.FindLayer("IP") iaxlayer = iaxpacket.FindLayer("IAX2") ipfrom = iplayer.FindField("Source IP") iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number") c = findActiveCall(ipfrom.value,iaxscall.value) if ( c ) c.endTimeSecs = iaxpacket.TimestampSecs c.endTimeUSecs = iaxpacket.TimestampUSecs c.endPacketID = iaxpacket.ID c.fHungUp=true end end # callExists (with perfomance enhancement cache) def findActiveCall(sourceip, sourcecall) # check the last checked call first (mostly we will find a hit here) if @callCacheCheck if @callCacheCheck.isActive? && @callCacheCheck.match(sourceip,sourcecall) return @callCacheCheck end end # search the call list @calls.each do |c| if (c.isActive? && c.match(sourceip,sourcecall)) @callCacheCheck=c return c end end @callCacheCheck=nil return nil end # print all calls def printCalls @calls.each do|c| c.printCall end end # load all calls into the table def loadCalls(tab) r=0 @calls.each do |c| if (c.isCompleted()) c.addToTable(tab,r) r=r+1 end end end # get completed call by index def getCompletedCall(idx) r=0 @calls.each do |c| if c.isCompleted() if r==idx return c end r=r+1 end end print "getCompletedCall: cannot find call number #{idx}\n" nil end # num valid calls def numValidCalls r=0 @calls.each do |c| r=r+1 if c.isCompleted() end r end # first call def demoCall @calls[6] end # set end time to last packet if we cant find Hangup message def endIncompleteCalls (lastpacket) @calls.each do|c| if ! c.endTimeSecs c.endTimeSecs = lastpacket.TimestampSecs c.endTimeUSecs = lastpacket.TimestampUSecs c.endPacketID = lastpacket.ID end end end end # --------------------------------- # Statistics # --------------------------------- class ModelBase CHART_FLAG_NORMAL=0 CHART_FLAG_VOICE=1 CHART_FLAG_CONTROL=2 @captureFile @activeCall @sliceus @activeCallLeg @chartflag attr_reader :startTime,:endTime def initialize(call, legno, filename) @captureFile=filename @activeCall=call @sliceus = 200000 @activeCallLeg = legno # set start timestamp @startTime = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs) # set end timestamp @endTime = Time.at(@activeCall.endTimeSecs,@activeCall.endTimeUSecs) @chartflag = CHART_FLAG_NORMAL end def totalTimeUSecs secdiff = @endTime.tv_sec - @startTime.tv_sec usdiff = @endTime.tv_usec - @startTime.tv_usec if usdiff < 0 usdiff = usdiff + ::USECPERSEC secdiff = secdiff-1 end totalTimeUSecs = (secdiff * ::USECPERSEC) + usdiff end def yscalelabel "Bandwidth" end def xscalelabel "Time" end def valunits "bps" end def matchCall (iaxpacket,iaxlayer) # ignore all retransmissions f_retran = iaxlayer.FindField(">FULL FRAME>Dest Call Number>R") if f_retran return f_retran.value == 1 end # ignore non voice full frames iplayer = iaxpacket.FindLayer("IP") ipfrom = iplayer.FindField("Source IP") iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number") if (iaxscall) f_frametype = iaxlayer.FindField("Frametype") if f_frametype.value =~ /Voice/ @chartFlag = CHART_FLAG_VOICE else @chartFlag = CHART_FLAG_CONTROL # print "skipping non voice control frame #{iaxpacket.Description}\n" return false end else iaxscall = iaxlayer.FindField(">MINI FRAME>Source Call Number>Source Call Number") end return nil if iaxscall.nil? return @activeCall.matchLeg(@activeCallLeg, ipfrom.value, iaxscall.value) end end #------------------------------- # CallBandwidth # bandwidth used by the codec of this call in a direction class CallBandwidthModel < ModelBase def initialize(call, legno, filename) super(call,legno,filename) end def each_val unsniffDB = WIN32OLE.new("Unsniff.Database") unsniffDB.OpenForRead(@captureFile) allPackets = unsniffDB.PacketIndex print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n" byteCount =0 currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs) (@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid| packet = allPackets.Item(pid) @chartFlag = CHART_FLAG_NORMAL iaxlayer = packet.FindLayer("IAX2") if iaxlayer && matchCall(packet,iaxlayer) pkttime = Time.at(packet.TimestampSecs, packet.TimestampUSecs) difft = pkttime - currSecs if difft*USEC_PER_SEC < @sliceus byteCount += packet.Length else case @chartFlag when CHART_FLAG_NORMAL yield pkttime, 8*(byteCount / difft), DataPointStyle.new(DataPointStyle::LINEHEIGHT,"o") when CHART_FLAG_VOICE yield pkttime, 8*(byteCount / difft), DataPointStyle.new(DataPointStyle::POINTCHAR,"V",FXRGB(0,0,255)) when CHART_FLAG_CONTROL yield pkttime, 8*(byteCount / difft), DataPointStyle.new(DataPointStyle::POINTCHAR,"C",FXRGB(255,0,255)) end byteCount = 0 currSecs = pkttime end end end unsniffDB.Close end def maxval 128000 end def minval 0 end def yscalelabel "Bandwidth" end def xscalelabel "Time" end def valunits "bps" end end # -------------- # Delay Model # calculate delay how are recieving packets spaced wrt to IAX2 timestamps class DelayModel < ModelBase @Curr_IAX_Msecs @Curr_Capt_Msecs def initialize(call, legno, filename) super(call,legno,filename) @Curr_IAX_Msecs=0 @Curr_Capt_Msecs=0 end def each_val unsniffDB = WIN32OLE.new("Unsniff.Database") unsniffDB.OpenForRead(@captureFile) allPackets = unsniffDB.PacketIndex print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n" byteCount =0 currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs) (@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid| packet = allPackets.Item(pid) @chartFlag = CHART_FLAG_NORMAL iaxlayer = packet.FindLayer("IAX2") if iaxlayer && matchCall(packet,iaxlayer) if @Curr_Capt_Msecs == 0 @Curr_IAX_Msecs = getIAXTimestamp(iaxlayer) @Curr_Capt_Msecs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC print "First frame = #{@Curr_IAX_Msecs}\n" else newIAXMs = getIAXTimestamp(iaxlayer) newCaptMs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC deltaIAX = newIAXMs - @Curr_IAX_Msecs deltaCapt = newCaptMs - @Curr_Capt_Msecs delayMs = deltaCapt - deltaIAX delayMs = -delayMs if delayMs < 0 @Curr_IAX_Msecs = newIAXMs @Curr_Capt_Msecs = newCaptMs print "Delay calculated = #{delayMs}\n" if DEBUG_MODE yield Time.at(packet.TimestampSecs,packet.TimestampUSecs), delayMs , DataPointStyle.new(DataPointStyle::LINECONNECT,"o") end end end unsniffDB.Close end # given a full or mini frame calculate the 32 - bit timestamp (in milliseconds) def getIAXTimestamp(iaxlayer) # full frame timestamp f_timestamp = iaxlayer.FindField(">FULL FRAME>Timestamp") if (f_timestamp) return f_timestamp.value.to_i else f_timestamp = iaxlayer.FindField(">MINI FRAME>Timestamp") if (f_timestamp) return (@Curr_IAX_Msecs & 0x00000000 ) + f_timestamp.value.to_i end end print "IAXTimestamp Error: Cannot find timestamp field in full or mini frames\n" return nil end def maxval 50 end def minval 0 end def yscalelabel "Delay (ms)" end def xscalelabel "Time" end def valunits "ms" end end # -------------- # Jitter Model # calculate jitter as per RFC3550 (is this ok for IAX vs RTP ?) class JitterModel < ModelBase @Curr_IAX_Msecs @Curr_Capt_Msecs @Curr_Jitter def initialize(call, legno, filename) super(call,legno,filename) @Curr_IAX_Msecs=0 @Curr_Capt_Msecs=0 @Curr_Jitter=0 end def each_val unsniffDB = WIN32OLE.new("Unsniff.Database") unsniffDB.OpenForRead(@captureFile) allPackets = unsniffDB.PacketIndex print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n" byteCount =0 currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs) (@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid| packet = allPackets.Item(pid) @chartFlag = CHART_FLAG_NORMAL iaxlayer = packet.FindLayer("IAX2") if iaxlayer && matchCall(packet,iaxlayer) if @Curr_Capt_Msecs == 0 @Curr_IAX_Msecs = getIAXTimestamp(iaxlayer) @Curr_Capt_Msecs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC print "First frame = #{@Curr_IAX_Msecs}\n" else newIAXMs = getIAXTimestamp(iaxlayer) newCaptMs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC deltaIAX = newIAXMs - @Curr_IAX_Msecs deltaCapt = newCaptMs - @Curr_Capt_Msecs delayMs = deltaCapt - deltaIAX delayMs = -delayMs if delayMs < 0 @Curr_Jitter += 1.0/16.0*(delayMs - @Curr_Jitter) @Curr_IAX_Msecs = newIAXMs @Curr_Capt_Msecs = newCaptMs print "Jitter calculated = #{@Curr_Jitter}\n" if DEBUG_MODE yield Time.at(packet.TimestampSecs,packet.TimestampUSecs), @Curr_Jitter , DataPointStyle.new(DataPointStyle::LINECONNECT,"o") end end end unsniffDB.Close end # given a full or mini frame calculate the 32 - bit timestamp (in milliseconds) def getIAXTimestamp(iaxlayer) # full frame timestamp f_timestamp = iaxlayer.FindField(">FULL FRAME>Timestamp") if (f_timestamp) return f_timestamp.value.to_i else f_timestamp = iaxlayer.FindField(">MINI FRAME>Timestamp") if (f_timestamp) return (@Curr_IAX_Msecs & 0x00000000 ) + f_timestamp.value.to_i end end print "IAXTimestamp Error: Cannot find timestamp field in full or mini frames\n" return nil end def maxval 50 end def minval 0 end def yscalelabel "Jitter (ms)" end def xscalelabel "Time" end def valunits "ms" end end # -------------- # Loss Model # calculate packet loss (packets expected - packet received) class LossModel < ModelBase @Curr_IAX_Msecs def initialize(call, legno, filename) super(call,legno,filename) @Curr_IAX_Msecs=0 end def each_val unsniffDB = WIN32OLE.new("Unsniff.Database") unsniffDB.OpenForRead(@captureFile) allPackets = unsniffDB.PacketIndex print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n" packetCount =0 sampleInterval = @activeCall.sampleInterval if !sampleInterval print "Sorry the codec is not supported for packet loss calculation #{@activeCall.codec}\n" return end # calculate packetization interval (depends on the codec used) currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs) (@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid| packet = allPackets.Item(pid) @chartFlag = CHART_FLAG_NORMAL iaxlayer = packet.FindLayer("IAX2") if iaxlayer && matchCall(packet,iaxlayer) @Curr_IAX_Msecs = getIAXTimestamp(iaxlayer) packetCount = packetCount + 1 packetCountExpected = @Curr_IAX_Msecs / sampleInterval lossPercent = (100.0 * (packetCountExpected -packetCount )) / (1.0 * packetCountExpected) #lossAbsolute = (packetCountExpected -packetCount ) print "expected = #{packetCountExpected} got = #{packetCount}\n" if DEBUG_MODE yield Time.at(packet.TimestampSecs,packet.TimestampUSecs), lossPercent, DataPointStyle.new(DataPointStyle::LINECONNECT,"o") end end unsniffDB.Close end # given a full or mini frame calculate the 32 - bit timestamp (in milliseconds) def getIAXTimestamp(iaxlayer) # full frame timestamp f_timestamp = iaxlayer.FindField(">FULL FRAME>Timestamp") if (f_timestamp) return f_timestamp.value.to_i else f_timestamp = iaxlayer.FindField(">MINI FRAME>Timestamp") if (f_timestamp) return (@Curr_IAX_Msecs & 0x00000000 ) + f_timestamp.value.to_i end end print "IAXTimestamp Error: Cannot find timestamp field in full or mini frames\n" return nil end def maxval 100 end def minval 0 end def yscalelabel "Packet Loss (percent)" end def xscalelabel "Time" end def valunits "%" end end #--------------------- # Call Event Model # plot call events ,mini frames, full frames, control commands, ping, pong , DTMF and everything else class CallEventModel < ModelBase @dataChar @dataColor @dataHeight @oppositeLeg @retran def initialize(call, legno, filename) super(call,legno,filename) @dataChar = "." @dataColor=FXRGB(255,255,255) @dataHeight=50 @oppositeLeg=false @retran=false end def matchCall (iaxpacket,iaxlayer) # ignore non voice full frames iplayer = iaxpacket.FindLayer("IP") ipfrom = iplayer.FindField("Source IP") iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number") if (!iaxscall) iaxscall = iaxlayer.FindField(">MINI FRAME>Source Call Number>Source Call Number") end return false if iaxscall.nil? @retran=false @oppositeLeg=false bMatch = @activeCall.matchLeg(@activeCallLeg, ipfrom.value, iaxscall.value) if (! bMatch) bMatch = @activeCall.matchLeg(1-@activeCallLeg,ipfrom.value,iaxscall.value) if (!bMatch) return false end @oppositeLeg=true end # retransmission (R) - ref f_retran = iaxlayer.FindField(">FULL FRAME>Dest Call Number>R") if f_retran if (f_retran.value == "1") @retran=true end else # mini frame @dataChar = "." @dataHeight=55 if @oppositeLeg # yellow color lower line @dataColor = FXRGB(255,255,0) else # white color upper line @dataColor = FXRGB(255,255,255) end return true end # voice,video,dtmf frame types f_frametype = iaxlayer.FindField("Frametype") if (f_frametype) @dataColor = FXRGB(255,255,0) @dataHeight = 60 case f_frametype.value when /Voice/ @dataChar = "V" @dataHeight = @dataHeight + 0 return true when /DTMF/ @dataChar="#" @dataHeight = @dataHeight + 5 return true end end # IAX control frame type sclass = iaxlayer.FindField(">FULL FRAME>SC_IAX>IAX Subclass") if (sclass) @dataColor = FXRGB(255,0,255) @dataHeight = 70 case sclass.value when /NEW/ @dataChar="N" @dataHeight = @dataHeight + 0 when /ACCEPT/ @dataChar="A" @dataHeight = @dataHeight + 3 when /HANGUP/ @dataChar="H" @dataHeight = @dataHeight + 6 when /REG/ @dataChar="R" @dataHeight = @dataHeight + 8 when /ACK/ @dataChar="+" @dataHeight = @dataHeight + 12 when /PING/ @dataChar=">" @dataHeight = @dataHeight + 16 when /PONG/ @dataChar="<" @dataHeight = @dataHeight + 16 when /LAG/ @dataChar="L" @dataHeight = @dataHeight + 19 else @dataChar="O" @dataHeight = @dataHeight + 0 end return true end # control frame types ctrltype = iaxlayer.FindField(">FULL FRAME>SC_Control>Control Subclass") if (ctrltype) @dataColor = FXRGB(255,128,255) @dataHeight=90 case ctrltype.value when /Ringing/ @dataChar="*" @dataHeight = @dataHeight + 0 when /Answer/ @dataChar="@" @dataHeight = @dataHeight + 3 else @dataChar="U" @dataHeight = @dataHeight + 6 end end return true end def each_val unsniffDB = WIN32OLE.new("Unsniff.Database") unsniffDB.OpenForRead(@captureFile) allPackets = unsniffDB.PacketIndex print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n" (@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid| packet = allPackets.Item(pid) iaxlayer = packet.FindLayer("IAX2") if iaxlayer && matchCall(packet,iaxlayer) pkttime = Time.at(packet.TimestampSecs, packet.TimestampUSecs) @dataColor = FXRGB(255,0,0) if @retran==true if @oppositeLeg @dataHeight = 100-@dataHeight end yield pkttime, @dataHeight, DataPointStyle.new(DataPointStyle::POINTCHAR,@dataChar,@dataColor) end end unsniffDB.Close end def maxval 90 end def minval 0 end def yscalelabel "Event" end def xscalelabel "Time" end def valunits "-" end end #--------------------------------------------------------- # Drive the IAX2 analysis #--------------------------------------------------------- USAGE = "iax2ana <capture-filename> " if ARGV.length != 1 puts USAGE exit 1 end # Scan the capture and build an index of all IAX2 calls InputFile = ARGV[0] ana = IAXAnalyzer.new(InputFile) ana.extract_calls ana.printCalls if DEBUG_MODE # A new Fox Application and MainWindow object theApp = FXApp.new theMainWindow = ChartWindow.new(theApp) theMainWindow.loadCalls(ana) theMainWindow.showCall(0) # Run application theApp.create theMainWindow.show theApp.run