This is merely a historical archive of years 2008-2021, before the migration to mailman3.
A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.
Neels Hofmeyr gerrit-no-reply at lists.osmocom.orgNeels Hofmeyr has uploaded this change for review. ( https://gerrit.osmocom.org/13610
Change subject: contrib/voicecall-shark
......................................................................
contrib/voicecall-shark
Change-Id: Ia3cdefe16b9e929d2836ca837db6f6107f7ed9eb
---
A contrib/voicecall-shark
1 file changed, 669 insertions(+), 0 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/libosmocore refs/changes/10/13610/1
diff --git a/contrib/voicecall-shark b/contrib/voicecall-shark
new file mode 100755
index 0000000..54600d0
--- /dev/null
+++ b/contrib/voicecall-shark
@@ -0,0 +1,669 @@
+#!/usr/bin/env python3
+
+doc = '''voicecall-shark: get grips on voice call MGCP and RTP'''
+
+import pyshark
+import pprint
+
+verbose = False
+
+def vprint(*args):
+ if not verbose:
+ return
+ print(*args)
+
+def error(*args):
+ raise Exception(''.join(*args))
+
+def pget(p, *field, ifnone=None):
+ tokens = ('.'.join(field)).split('.')
+ if p is None or len(tokens) < 1:
+ return ifnone
+ first = tokens[0]
+ if not hasattr(p, first):
+ return ifnone
+ p_field = getattr(p, first)
+ if p_field is None:
+ return ifnone
+ if len(tokens) > 1:
+ return pget(p_field, *tokens[1:])
+ return p_field
+
+
+class LogEntry:
+ def __init__(s, p, message, obj):
+ s.p = p
+ s.message = message
+ s.obj = obj
+
+
+class HasLog:
+ def __init__(s, parent=None):
+ s.log_entries = []
+ s.parents = []
+ s.children = []
+ if parent is not None:
+ s.log_parent(parent)
+
+ def log_child(s, child):
+ s.children.append(child)
+ child.parents.append(s)
+
+ def log_parent(s, parent):
+ parent.log_child(s)
+
+ def log(s, p, message):
+ s.log_entries.append(LogEntry(p, message, s))
+
+class Domino(HasLog):
+ def __init__(s, left, right, *refs):
+ s.left = left
+ s.right = right
+ s.refs = list(refs)
+ s.reversed = [ False ] * len(s.refs)
+
+ def absorb(s, domino):
+ s.refs.extend(domino.refs)
+ s.reversed.extend(domino.reversed)
+
+ def reverse(s):
+ x = s.left
+ s.left = s.right
+ s.right = x
+ s.reversed = list(map(lambda x: not x, s.reversed))
+
+ def matches(s, label):
+ if s.left == label:
+ return -1
+ if s.right == label:
+ return 1
+ return 0
+
+ def __str__(s):
+ return s.str('[','|',']')
+
+ def __repr__(s):
+ return str(s)
+
+ def str(s, left='[', mid='|', right=']', reverse=False):
+ if reverse:
+ return '%s%s%s%s%s' % (left, s.right, mid, s.left, right)
+ else:
+ return '%s%s%s%s%s' % (left, s.left, mid, s.right, right)
+
+ def __eq__(s, other):
+ o = other.str()
+ return s.str() == o or s.str(reverse=True) == o
+
+class Dominoes(HasLog):
+ def __init__(s, *pieces):
+ s.pieces = list(pieces)
+
+ def has(s, piece):
+ for p in s.pieces:
+ if p is piece:
+ return True
+ return False
+
+ def add(s, piece):
+ if s.has(piece):
+ return
+ s.pieces.append(piece)
+
+ def find_piece(s, label, in_list=None):
+ if in_list is None:
+ in_list = s.pieces
+ for piece in in_list:
+ if piece.matches(label):
+ return piece
+ return None
+
+ def find_match(s, for_piece, in_list=None):
+ if in_list is None:
+ in_list = s.pieces
+ match = s.find_piece(for_piece.left, in_list)
+ if not match:
+ match = s.find_piece(for_piece.right, in_list)
+ return match
+
+ def dedup(loose_pieces):
+ pieces = []
+ loose_pieces = list(loose_pieces)
+ while loose_pieces:
+ l = loose_pieces.pop(0)
+ for p in pieces:
+ if l == p:
+ p.absorb(l)
+ l = None
+ break
+
+ if l:
+ pieces.append(l)
+ return pieces
+
+ def arrange(loose_pieces):
+ if not loose_pieces:
+ return []
+ pieces_remain = list(loose_pieces)
+ groups = [Dominoes(pieces_remain.pop(0))]
+
+ which_end = -1
+ failed = 0
+
+ while pieces_remain:
+ which_end = 0 if which_end == -1 else -1
+ found_match = 0
+ p = None
+ for group in groups:
+ p = group.pieces[which_end]
+ match = group.find_match(p, pieces_remain)
+ if not match:
+ continue
+ if which_end == -1:
+ found_match = match.matches(p.right)
+ if found_match == 1:
+ match.reverse()
+ else:
+ found_match = match.matches(p.left)
+ if found_match == -1:
+ match.reverse()
+ if found_match:
+ break
+
+ if found_match:
+ failed = 0
+ pieces_remain.remove(match)
+ if which_end == -1:
+ group.pieces.append(match)
+ else:
+ group.pieces.insert(0, match)
+ else:
+ failed += 1
+ if failed < 2:
+ continue
+ # chain is broken. Create another group.
+ groups.append(Dominoes(pieces_remain.pop(0)))
+
+ return groups
+
+ def str(s, piece_left_edge='[', piece_mid='|', piece_right_edge=']'):
+ r = []
+ for p in s.pieces:
+ r.append(p.str(piece_left_edge, piece_mid, piece_right_edge))
+ return ''.join(r)
+
+ def __str__(s):
+ return s.str()
+
+ def chain_str(s, sep=', '):
+ if not s.pieces:
+ return ''
+ r = [s.pieces[0].left]
+ for p in s.pieces:
+ if r[-1] != p.left:
+ r.append(p.left)
+ if r[-1] != p.right:
+ r.append(p.right)
+ return sep.join(r)
+
+class RtpIpPort(HasLog):
+ def __init__(s, ip=None, port=None):
+ s.ip = ip
+ s.port = port
+
+ def from_sdp(p):
+ ip = pget(p, 'sdp.connection_info_address')
+ port = pget(p, 'sdp.media_port')
+ return RtpIpPort(ip, port)
+
+ def from_rtp_source(p):
+ ip = pget(p, 'ip.src')
+ port = pget(p, 'udp.srcport')
+ return RtpIpPort(ip, port)
+
+ def from_rtp_dest(p):
+ ip = pget(p, 'ip.dst')
+ port = pget(p, 'udp.dstport')
+ return RtpIpPort(ip, port)
+
+ def __str__(s):
+ return '%s:%s' % (s.ip, s.port)
+
+ def __eq__(s, other):
+ return str(s) == str(other)
+
+
+class MgcpConn(HasLog):
+ def __init__(s, crcx_p, endpoint, conn_id=None):
+ s.endpoint = None
+ endpoint.add_conn(s)
+ s.conn_id = conn_id
+ s.crcx = crcx_p
+ s.crcx_ok = None
+ s.mdcx = []
+ s.dlcx = None
+ s.dlcx_ok = None
+
+ s.rtp_local = None
+ s.rtp_remote = None
+ s.payload_type = None
+ s.codec = None
+
+ s.rtp_tx = 0
+ s.rtp_tx_err = 0
+ s.rtp_rx = 0
+ s.rtp_rx_err = 0
+ super().__init__(endpoint)
+
+ def rx_verb(s, p):
+ v = p.mgcp.req_verb
+ vprint("VERB %r" % v)
+ if v == 'CRCX':
+ if hasattr(p, 'sdp'):
+ s.rtp_remote = RtpIpPort.from_sdp(p)
+ elif v == 'MDCX':
+ s.mdcx.append(p)
+ new_remote = RtpIpPort.from_sdp(p)
+ if s.rtp_remote != new_remote:
+ s.rtp_remote = RtpIpPort.from_sdp(p)
+ s.log(p, 'MDCX to %s' % new_remote)
+ else:
+ s.log(p, 'MDCX')
+
+ elif v == 'DLCX':
+ s.log(p, 'DLCX')
+ s.dlcx = p
+
+ s.get_media(p)
+
+ def get_media(s, p):
+ s.codec = pget(p, 'sdp.media_proto', ifnone=s.codec)
+ s.payload_type = pget(p, 'sdp.media_format', ifnone=s.payload_type)
+
+ def rx_verb_ok(s, p, verb_p):
+ verb = verb_p.mgcp.req_verb
+ vprint("VERB OK %r" % verb)
+ if verb == 'CRCX':
+ s.crcx_ok = p
+ if hasattr(p.mgcp, 'param_specificendpointid'):
+ s.endpoint.name = p.mgcp.param_specificendpointid
+ s.conn_id = p.mgcp.param_connectionid
+ s.rtp_local = RtpIpPort.from_sdp(p)
+ s.log(p, "CRCX OK")
+ elif verb == 'MDCX':
+ s.mdcx.append(p)
+ s.log(p, "MDCX OK")
+ elif verb == 'DLCX':
+ s.dlcx_ok = p
+ s.log(p, "DLCX OK")
+
+ def is_open(s):
+ return s.dlcx is None
+
+ def summary(s, remote_first=False):
+ label = ''
+ if not s.is_open():
+ label = 'DLCXd '
+ rtp1 = s.rtp_local
+ rtp2 = s.rtp_remote
+ if remote_first:
+ rtpx = rtp1
+ rtp1 = rtp2
+ rtp2 = rtpx
+ err_label = ''
+ if s.rtp_tx_err:
+ err_label = err_label + ' tx-ERR:%d' % s.rtp_tx_err
+ if s.rtp_rx_err:
+ err_label = err_label + ' rx-ERR:%d' % s.rtp_rx_err
+ return '%s%s: %s <-> %s %r %r tx:%d rx:%d' % (
+ label, s.conn_id, rtp1, rtp2, s.payload_type, s.codec, s.rtp_tx, s.rtp_rx)
+
+
+class MgcpEndpoint(HasLog):
+ def __init__(s, name):
+ s.name = name
+ s.conns = []
+ super().__init__()
+
+ def name_is(s, name):
+ return s.name == name
+
+ def add_conn(s, mgcp_conn):
+ s.conns.append(mgcp_conn)
+ mgcp_conn.endpoint = s
+
+ def is_open(s):
+ return any(c.is_open() for c in s.conns)
+
+ def get_conn(s, p):
+ conn_id = pget(p, 'mgcp.param_connectionid')
+ if not conn_id:
+ return None
+ vprint('get conn_id %r' % conn_id)
+ for c in s.conns:
+ vprint(' conn_id %r' % c.conn_id)
+ if c.conn_id == conn_id:
+ return c
+ print('ERROR: unknown conn id %r' % conn_id)
+ return None
+
+ def as_dominoes(s):
+ pieces = []
+ for i in range(len(s.conns)):
+ c = s.conns[i]
+ if not c.is_open():
+ continue
+
+ left = str(c.rtp_remote)
+ right = str(c.rtp_local)
+ pieces.append(Domino(left, right, s))
+
+ for j in range(i+1, len(s.conns)):
+ c2 = s.conns[j]
+ if not c2.is_open():
+ continue
+ left = str(c.rtp_local)
+ right = str(c2.rtp_local)
+ pieces.append(Domino(left, right, s))
+ return pieces
+
+ def reverse(s):
+ s.conns = list(reversed(s.conns))
+
+ def seen_rtp(s, src, dst):
+ handled = False
+ for c in s.conns:
+ if c.rtp_local == src:
+ handled = True
+ if c.rtp_remote == dst:
+ c.rtp_tx += 1
+ else:
+ c.rtp_tx_err += 1
+ if c.rtp_remote == src:
+ handled = True
+ if c.rtp_local == dst:
+ c.rtp_rx += 1
+ else:
+ c.rtp_rx_err += 1
+
+ return handled
+
+ def summary(s):
+ r = [s.name]
+ remote_first = True
+ for c in s.conns:
+ r.append(' | %s' % c.summary(remote_first))
+ remote_first = False
+ return '\n'.join(r)
+
+class MgcpTrans:
+ def __init__(s, p, obj):
+ s.p = p
+ s.obj = obj
+
+class RtpStream(HasLog):
+ all_rtp_streams = []
+
+ def find(rtp_ip_port):
+ for rtps in all_rtp_streams:
+ if rtps.has(rtp_ip_port):
+ return rtps
+ return None
+
+ def find_or_create(rtp_ip_ports):
+ rtps = None
+ for rtp_ip_port in rtp_ip_ports:
+ rtps = RtpStream.find(rtp_ip_port)
+ if rtps:
+ break
+ rtps.add(*rtp_ip_ports)
+ return rtps
+
+ def __init__(s, rtp_ip_ports=[]):
+ s.call_leg = None
+ s.rtp_ip_ports = rtp_ip_ports
+ s.rtp_packets_left = 0
+ s.rtp_packets_right = 0
+ super().__init__()
+
+ def has(s, rtp_ip_port):
+ for ipp in s.rtp_ip_ports:
+ if ipp == rtp_ip_port:
+ return True
+ return False
+
+ def add(s, *rtp_ip_ports):
+ for rtp_ip_port in rtp_ip_ports:
+ if s.has(rtp_ip_port):
+ continue
+ if len(s.rtp_ip_ports) > 1:
+ error('RtpStream can have only two RtpIpPorts')
+ s.rtp_ip_ports.append(rtp_ip_port)
+
+ def reverse(s):
+ s.rtp_ip_ports = list(reversed(s.rtp_ip_ports))
+
+ def left(s):
+ if len(s.rtp_ip_ports):
+ return s.rtp_ip_ports[0]
+ return None
+
+ def right(s):
+ if len(s.rtp_ip_ports) > 1:
+ return s.rtp_ip_ports[1]
+ return None
+
+
+class CallLeg(HasLog):
+ def tie(rtp_ip_port_a, rtp_ip_port_b):
+ rtps_a = RtpStream.find(rtp_ip_port_a)
+ rtps_b = RtpStream.find(rtp_ip_port_b)
+
+
+ def __init__(s):
+ self.rtp_streams = []
+ super().__init__()
+
+ def add_rtp_stream(s, tie_to_rtp_ip_port, rtp_stream):
+ self.rtp_streams.append(rtp_stream)
+ s.log_child(edge)
+
+
+class Results:
+ def __init__(s, call_legs = []):
+ s.call_legs = call_legs
+ s.mgw_endpoints = []
+ s.mgcp_transactions = []
+ s.stray_rtp = 0
+
+ def mgcp_trans_new(s, p, obj):
+ s.mgcp_transactions.append(MgcpTrans(p, obj))
+
+ def mgcp_trans_res(s, p):
+ for t in s.mgcp_transactions:
+ if t.p.mgcp.transid == p.mgcp.transid:
+ o = t.obj
+ s.mgcp_transactions.remove(t)
+ return t
+
+ def new_endpoint(s, p):
+ ep = MgcpEndpoint(p.mgcp.req_endpoint)
+ s.mgw_endpoints.append(ep)
+ return ep
+
+ def find_endpoint(s, endpoint, still_open=False):
+ for ep in s.mgw_endpoints:
+ if not ep.name_is(endpoint):
+ continue
+ if still_open and not ep.is_open():
+ continue
+ return ep
+
+ def process_mgcp(s, p):
+ m = p.mgcp
+ if verbose:
+ print('----')
+ print(p.pretty_print())
+ print('MGCP:')
+ pprint.pprint(p.mgcp.field_names)
+ if hasattr(p, 'sdp'):
+ print('SDP:')
+ pprint.pprint(p.sdp.field_names)
+
+ ep = None
+ label = None
+
+ if 'req_verb' in m.field_names:
+ v = m.req_verb
+ label = v
+ ci = None
+
+ ep = s.find_endpoint(m.req_endpoint, True)
+ if ep is None:
+ ep = s.new_endpoint(p)
+ vprint('VERB ep %r' % ep.name)
+ if ci is None:
+ ci = ep.get_conn(p)
+ if ci is None:
+ ci = MgcpConn(p, ep)
+ ci.rx_verb(p)
+
+ s.mgcp_trans_new(p, ci)
+
+ elif 'rsp' in m.field_names:
+ t = s.mgcp_trans_res(p)
+ ci = t.obj
+ ci.rx_verb_ok(p, t.p)
+ label = '%s OK' % t.p.mgcp.req_verb
+ ep = ci.endpoint
+
+ if ep is not None:
+ print('----- %s' % label)
+ print(ep.summary())
+
+ def process_rtp(s, p):
+ src = RtpIpPort.from_rtp_source(p)
+ dst = RtpIpPort.from_rtp_dest(p)
+
+ handled = False
+ for ep in s.mgw_endpoints:
+ if ep.seen_rtp(src, dst):
+ handled = True
+
+ if not handled:
+ s.stray_rtp += 1
+ print("Stray RTP: %s -> %s" % (src, dst))
+
+
+ def process_cap(s, cap):
+ last_time = None
+ refresh = False
+ last_stray_rtp = 0
+ for p in cap:
+ if hasattr(p, 'mgcp'):
+ s.process_mgcp(p)
+ refresh = True
+
+ if hasattr(p, 'rtp'):
+ s.process_rtp(p)
+
+ if hasattr(p, 'udp'):
+ time = float(pget(p, 'udp.time_relative', ifnone='0'))
+ if last_time is None or time > last_time + 1:
+ last_time = time
+ refresh = True
+
+ if refresh:
+ refresh = False
+ s.correlate_endpoints()
+ if last_stray_rtp != s.stray_rtp:
+ print('stray_rtp: %d' % (s.stray_rtp - last_stray_rtp))
+ last_stray_rtp = s.stray_rtp
+ print(time)
+
+
+
+
+ def correlate_endpoints(s):
+ pieces = []
+ for ep in s.mgw_endpoints:
+ pieces.extend(ep.as_dominoes())
+ groups = Dominoes.arrange(Dominoes.dedup(pieces))
+
+ i = 0
+ for g in groups:
+ i += 1
+ print('\n--- Call chain %d' % i)
+ print(g.chain_str('<->'))
+
+ endp_chain = []
+ for piece in g.pieces:
+ for i in range(len(piece.refs)):
+ ref = piece.refs[i]
+ reversd = piece.reversed[i]
+ if type(ref) is not MgcpEndpoint:
+ continue
+ if ref in endp_chain:
+ continue
+ endp_chain.append(ref)
+
+ prev_rtp = g.pieces[0].left
+ for ep in endp_chain:
+ if not len(ep.conns):
+ continue
+ if str(ep.conns[0].rtp_remote) != prev_rtp:
+ ep.reverse()
+ print(ep.summary())
+ prev_rtp = str(ep.conns[-1].rtp_local)
+
+ print('\n')
+
+ def process_file(s, path):
+ vprint(repr(path))
+ cap = pyshark.FileCapture(path)
+ s.process_cap(cap)
+
+ def summary(s):
+ r = []
+ still_open = 0
+
+ for ep in s.mgw_endpoints:
+ if not ep.is_open():
+ continue
+ r.append(ep.summary())
+ still_open += 1
+ r.append('open mgcp endpoints: %d' % still_open)
+ return '\n'.join(r)
+
+
+def parse_args():
+ import argparse
+ parser = argparse.ArgumentParser(description=doc)
+ parser.add_argument('--pcap-file', '-f')
+ return parser.parse_args()
+
+def dominoes_test():
+
+ d = [
+ Domino('hat', 'hut'),
+ Domino('ape', 'dog'),
+ Domino('moo', 'foo'),
+ Domino('cat', 'dog'),
+ Domino('ape', 'cow'),
+ Domino('axe', 'hop'),
+ Domino('hat', 'cat'),
+ Domino('moo', 'foo'),
+ Domino('axe', 'bin'),
+ Domino('axe', 'dog'),
+ ]
+ groups = Dominoes.arrange(d)
+
+ for g in groups:
+ print(str(g))
+
+if __name__ == '__main__':
+ opts = parse_args()
+
+ r = Results()
+ r.process_file(opts.pcap_file)
+ print(r.summary())
--
To view, visit https://gerrit.osmocom.org/13610
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: libosmocore
Gerrit-Branch: master
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia3cdefe16b9e929d2836ca837db6f6107f7ed9eb
Gerrit-Change-Number: 13610
Gerrit-PatchSet: 1
Gerrit-Owner: Neels Hofmeyr <nhofmeyr at sysmocom.de>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20190412/db418968/attachment.htm>