diff options
Diffstat (limited to 'contrib')
41 files changed, 2983 insertions, 1 deletions
diff --git a/contrib/a-link/sccp-split-by-con.lua b/contrib/a-link/sccp-split-by-con.lua new file mode 100644 index 000000000..f5d5502ae --- /dev/null +++ b/contrib/a-link/sccp-split-by-con.lua @@ -0,0 +1,170 @@ +-- Split trace based on SCCP Source +-- There are still bugs to find... bugs bugs bugs... hmm +do + local function init_listener() + print("CREATED LISTENER") + local tap = Listener.new("ip", "sccp && (ip.src == 172.16.1.81 || ip.dst == 172.16.1.81)") + local sccp_type_field = Field.new("sccp.message_type") + local sccp_src_field = Field.new("sccp.slr") + local sccp_dst_field = Field.new("sccp.dlr") + local msg_type_field = Field.new("gsm_a.dtap_msg_mm_type") + local lu_rej_field = Field.new("gsm_a.dtap.rej_cause") + local ip_src_field = Field.new("ip.src") + local ip_dst_field = Field.new("ip.dst") + + -- + local bssmap_msgtype_field = Field.new("gsm_a.bssmap_msgtype") + -- assignment failure 0x03 + -- + + -- + local dtap_cause_field = Field.new("gsm_a_dtap.cause") + local dtap_cc_field = Field.new("gsm_a.dtap_msg_cc_type") + + local connections = {} + + function check_failure(con) + check_lu_reject(con) + check_disconnect(con) + check_failures(con) + end + + -- cipher mode reject + function check_failures(con) + local msgtype = bssmap_msgtype_field() + if not msgtype then + return + end + + msgtype = tonumber(msgtype) + if msgtype == 89 then + print("Cipher mode reject") + con[4] = true + elseif msgtype == 0x03 then + print("Assignment failure") + con[4] = true + elseif msgtype == 0x22 then + print("Clear Request... RF failure?") + con[4] = true + end + end + + -- check if a DISCONNECT is normal + function check_disconnect(con) + local msg_type = dtap_cc_field() + if not msg_type then + return + end + + if tonumber(msg_type) ~= 0x25 then + return + end + + local cause = dtap_cause_field() + if not cause then + return + end + + cause = tonumber(cause) + if cause ~= 0x10 then + print("DISCONNECT != Normal") + con[4] = true + end + end + + -- check if we have a LU Reject + function check_lu_reject(con) + local msg_type = msg_type_field() + if not msg_type then + return + end + + msg_type = tonumber(tostring(msg_type)) + if msg_type == 0x04 then + print("LU REJECT with " .. tostring(lu_rej_field())) + con[4] = true + end + end + + function tap.packet(pinfo,tvb,ip) + local ip_src = tostring(ip_src_field()) + local ip_dst = tostring(ip_dst_field()) + local sccp_type = tonumber(tostring(sccp_type_field())) + local sccp_src = sccp_src_field() + local sccp_dst = sccp_dst_field() + + local con + + if sccp_type == 0x01 then + elseif sccp_type == 0x2 then + local src = string.format("%s-%s", ip_src, tostring(sccp_src)) + local dst = string.format("%s-%s", ip_dst, tostring(sccp_dst)) + local datestring = os.date("%Y%m%d%H%M%S") + local pcap_name = string.format("alink_trace_%s-%s_%s.pcap", src, dst, datestring) + local dumper = Dumper.new_for_current(pcap_name) + + local con = { ip_src, tostring(sccp_src), tostring(sccp_dst), false, dumper, pcap_name } + + dumper:dump_current() + connections[src] = con + connections[dst] = con + elseif sccp_type == 0x4 then + -- close a connection... remove it from the list + local src = string.format("%s-%s", ip_src, tostring(sccp_src)) + local dst = string.format("%s-%s", ip_dst, tostring(sccp_dst)) + + local con = connections[src] + if not con then + return + end + + con[5]:dump_current() + con[5]:flush() + + -- this causes a crash on unpacted wireshark + con[5]:close() + + -- the connection had a failure + if con[4] == true then + local datestring = os.date("%Y%m%d%H%M%S") + local new_name = string.format("alink_failure_%s_%s-%s.pcap", datestring, con[2], con[3]) + os.rename(con[6], new_name) + else + os.remove(con[6]) + end + + + -- clear the old connection + connections[src] = nil + connections[dst] = nil + + elseif sccp_type == 0x5 then + -- not handled yet... we should verify stuff here... + local dst = string.format("%s-%s", ip_dst, tostring(sccp_dst)) + local con = connections[dst] + if not con then + return + end + con[5]:dump_current() + elseif sccp_type == 0x6 then + local dst = string.format("%s-%s", ip_dst, tostring(sccp_dst)) + local con = connections[dst] + if not con then + print("DON'T KNOW THIS CONNECTION for " .. ip_dst) + return + end + con[5]:dump_current() + check_failure(con) + end + + end + function tap.draw() + print("DRAW") + end + function tap.reset() + print("RESET") + end + end + + init_listener() +end diff --git a/contrib/bsc-test/README b/contrib/bsc-test/README new file mode 100644 index 000000000..adb222e21 --- /dev/null +++ b/contrib/bsc-test/README @@ -0,0 +1 @@ +Some crazy scripts call testing... and MSC link failure simulation diff --git a/contrib/bsc-test/all_dial b/contrib/bsc-test/all_dial new file mode 100644 index 000000000..96e5f00b3 --- /dev/null +++ b/contrib/bsc-test/all_dial @@ -0,0 +1,8 @@ +ABORT BUSY +ABORT 'NO CARRIER' +ABORT 'OK' + +'' AT +SAY "Dialing a number\n" +'OK' ATD05660066; + diff --git a/contrib/bsc-test/dial.sh b/contrib/bsc-test/dial.sh new file mode 100755 index 000000000..e5e19f63e --- /dev/null +++ b/contrib/bsc-test/dial.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Evil dial script.. + +while true; +do + chat -v -f all_dial < /dev/ttyACM0 > /dev/ttyACM0 + sleep 5s + chat -v -f hangup < /dev/ttyACM0 > /dev/ttyACM0 + sleep 2s +done + diff --git a/contrib/bsc-test/drop-oml.sh b/contrib/bsc-test/drop-oml.sh new file mode 100755 index 000000000..84eead7b7 --- /dev/null +++ b/contrib/bsc-test/drop-oml.sh @@ -0,0 +1,6 @@ +#!/bin/sh +sleep 3 +echo "enable" +sleep 1 +echo "drop bts connection 0 oml" +sleep 1 diff --git a/contrib/bsc-test/drop.sh b/contrib/bsc-test/drop.sh new file mode 100755 index 000000000..c7b66ba72 --- /dev/null +++ b/contrib/bsc-test/drop.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +while true; +do + echo "Going to drop the OML connection" + ./drop-oml.sh | telnet 127.0.0.1 4242 + sleep 58m +done diff --git a/contrib/bsc-test/hangup b/contrib/bsc-test/hangup new file mode 100644 index 000000000..cad6870fd --- /dev/null +++ b/contrib/bsc-test/hangup @@ -0,0 +1,4 @@ +TIMEOUT 10 +'' ^Z +SAY "Waiting for hangup confirm\n" +'' ATH; diff --git a/contrib/bsc-test/msc.sh b/contrib/bsc-test/msc.sh new file mode 100755 index 000000000..bec011d4c --- /dev/null +++ b/contrib/bsc-test/msc.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +while true; +do + echo "Kill the osmo-bsc" + /usr/bin/kill -s SIGUSR2 `pidof osmo-bsc` + sleep 58s +done diff --git a/contrib/bsc_control.py b/contrib/bsc_control.py new file mode 100755 index 000000000..c1b09ce74 --- /dev/null +++ b/contrib/bsc_control.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# -*- mode: python-mode; py-indent-tabs-mode: nil -*- +""" +/* + * Copyright (C) 2016 sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +""" + +from optparse import OptionParser +from ipa import Ctrl +import socket + +verbose = False + +def connect(host, port): + if verbose: + print "Connecting to host %s:%i" % (host, port) + + sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sck.setblocking(1) + sck.connect((host, port)) + return sck + +def do_set_get(sck, var, value = None): + (r, c) = Ctrl().cmd(var, value) + sck.send(c) + answer = Ctrl().rem_header(sck.recv(4096)) + return (answer,) + Ctrl().verify(answer, r, var, value) + +def set_var(sck, var, val): + (a, _, _) = do_set_get(sck, var, val) + return a + +def get_var(sck, var): + (_, _, v) = do_set_get(sck, var) + return v + +def _leftovers(sck, fl): + """ + Read outstanding data if any according to flags + """ + try: + data = sck.recv(1024, fl) + except socket.error as (s_errno, strerror): + return False + if len(data) != 0: + tail = data + while True: + (head, tail) = Ctrl().split_combined(tail) + print "Got message:", Ctrl().rem_header(head) + if len(tail) == 0: + break + return True + return False + +if __name__ == '__main__': + parser = OptionParser("Usage: %prog [options] var [value]") + parser.add_option("-d", "--host", dest="host", + help="connect to HOST", metavar="HOST") + parser.add_option("-p", "--port", dest="port", type="int", + help="use PORT", metavar="PORT", default=4249) + parser.add_option("-g", "--get", action="store_true", + dest="cmd_get", help="perform GET operation") + parser.add_option("-s", "--set", action="store_true", + dest="cmd_set", help="perform SET operation") + parser.add_option("-v", "--verbose", action="store_true", + dest="verbose", help="be verbose", default=False) + parser.add_option("-m", "--monitor", action="store_true", + dest="monitor", help="monitor the connection for traps", default=False) + + (options, args) = parser.parse_args() + + verbose = options.verbose + + if options.cmd_set and options.cmd_get: + parser.error("Get and set options are mutually exclusive!") + + if not (options.cmd_get or options.cmd_set or options.monitor): + parser.error("One of -m, -g, or -s must be set") + + if not (options.host): + parser.error("Destination host and port required!") + + sock = connect(options.host, options.port) + + if options.cmd_set: + if len(args) < 2: + parser.error("Set requires var and value arguments") + _leftovers(sock, socket.MSG_DONTWAIT) + print "Got message:", set_var(sock, args[0], ' '.join(args[1:])) + + if options.cmd_get: + if len(args) != 1: + parser.error("Get requires the var argument") + _leftovers(sock, socket.MSG_DONTWAIT) + (a, _, _) = do_set_get(sock, args[0]) + print "Got message:", a + + if options.monitor: + while True: + if not _leftovers(sock, 0): + print "Connection is gone." + break + sock.close() diff --git a/contrib/bt.py b/contrib/bt.py new file mode 100755 index 000000000..1b111efc8 --- /dev/null +++ b/contrib/bt.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +import os + +f = open("unbalanced") +lines = [] +for line in f: + lines.append(line) + +filenames = {} + +output = [] +for line in lines: + if "[0x" in line: + start = line.find("[") + end = line.find("]") + addr = line[start+1:end] + try: + file = filenames[addr] + except KeyError: + r = os.popen("addr2line -fs -e ./bsc_hack %s" % addr) + all = r.read().replace("\n", ",") + file = all + filenames[addr] = file + + line = line.replace(addr, file) + output.append(line) + +g = open("unbalanced.2", "w") +g.write("".join(output)) + + + diff --git a/contrib/convert_to_enum.py b/contrib/convert_to_enum.py new file mode 100755 index 000000000..bcd6f2cee --- /dev/null +++ b/contrib/convert_to_enum.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +# +# Convert ETSI documents to an enum +# + +import re, sys + +def convert(string): + string = string.strip().replace(" ", "").rjust(8, "0") + var = 0 + offset = 7 + for char in string: + assert offset >= 0 + var = var | (int(char) << offset) + offset = offset - 1 + + return var + +def string(name): + name = name.replace(" ", "_") + name = name.replace('"', "") + name = name.replace('/', '_') + name = name.replace('(', '_') + name = name.replace(')', '_') + return "%s_%s" % (sys.argv[2], name.upper()) + +file = open(sys.argv[1]) + + +for line in file: + m = re.match(r"[ \t]*(?P<value>[01 ]+)[ ]+(?P<name>[a-zA-Z /0-9()]+)", line[:-1]) + + if m: + print "\t%s\t\t= %d," % (string(m.groupdict()["name"]), convert(m.groupdict()["value"])) + else: + print line[:-1] diff --git a/contrib/ctrl2sse.py b/contrib/ctrl2sse.py new file mode 100755 index 000000000..8b630ecfd --- /dev/null +++ b/contrib/ctrl2sse.py @@ -0,0 +1,147 @@ +#!/usr/bin/python2 + +mod_license = ''' +/* + * Copyright (C) 2016 sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +''' + +import sys, argparse, random, logging, tornado.ioloop, tornado.web, tornado.tcpclient, tornado.httpclient, eventsource, bsc_control +from eventsource import listener, request + +''' +N. B: this is not an example of building proper REST API or building secure web application. +It's only purpose is to illustrate conversion of Osmocom's Control Interface to web-friendly API. +Exposing this to Internet while connected to production network might lead to all sorts of mischief and mayhem +from NSA' TAO breaking into your network to zombie apocalypse. Do NOT do that. +''' + +token = None +stream = None +url = None + +''' +Returns json according to following schema - see http://json-schema.org/documentation.html for details: +{ + "title": "Ctrl Schema", + "type": "object", + "properties": { + "variable": { + "type": "string" + }, + "varlue": { + "type": "string" + } + }, + "required": ["interface", "variable", "value"] +} +Example validation from command-line: +json validate --schema-file=schema.json --document-file=data.json +The interface is represented as string because it might look different for IPv4 vs v6. +''' + +def read_header(data): + t_length = bsc_control.ipa_ctrl_header(data) + if (t_length): + stream.read_bytes(t_length - 1, callback = read_trap) + else: + print >> sys.stderr, "protocol error: length missing in %s!" % data + +@tornado.gen.coroutine +def read_trap(data): + (t, z, v, p) = data.split() + if (t != 'TRAP' or int(z) != 0): + print >> sys.stderr, "protocol error: TRAP != %s or 0! = %d" % (t, int(z)) + else: + yield tornado.httpclient.AsyncHTTPClient().fetch(tornado.httpclient.HTTPRequest(url = "%s/%s/%s" % (url, "ping", token), + method = 'POST', + headers = {'Content-Type': 'application/json'}, + body = tornado.escape.json_encode({ 'variable' : v, 'value' : p }))) + stream.read_bytes(4, callback = read_header) + +@tornado.gen.coroutine +def trap_setup(host, port, target_host, target_port, tk): + global stream + global url + global token + token = tk + url = "http://%s:%s/sse" % (host, port) + stream = yield tornado.tcpclient.TCPClient().connect(target_host, target_port) + stream.read_bytes(4, callback = read_header) + +def get_v(s, v): + return { 'variable' : v, 'value' : bsc_control.get_var(s, tornado.escape.native_str(v)) } + +class CtrlHandler(tornado.web.RequestHandler): + def initialize(self): + self.skt = bsc_control.connect(self.settings['ctrl_host'], self.settings['ctrl_port']) + + def get(self, v): + self.write(get_v(self.skt, v)) + + def post(self): + self.write(get_v(self.skt, self.get_argument("variable"))) + +class SetCtrl(CtrlHandler): + def get(self, var, val): + bsc_control.set_var(self.skt, tornado.escape.native_str(var), tornado.escape.native_str(val)) + super(SetCtrl, self).get(tornado.escape.native_str(var)) + + def post(self): + bsc_control.set_var(self.skt, tornado.escape.native_str(self.get_argument("variable")), tornado.escape.native_str(self.get_argument("value"))) + super(SetCtrl, self).post() + +class Slash(tornado.web.RequestHandler): + def get(self): + self.write('<html><head><title>%s</title></head><body>Using Tornado framework v%s' + '<form action="/get" method="POST">' + '<input type="text" name="variable">' + '<input type="submit" value="GET">' + '</form>' + '<form action="/set" method="POST">' + '<input type="text" name="variable">' + '<input type="text" name="value">' + '<input type="submit" value="SET">' + '</form>' + '</body></html>' % ("Osmocom Control Interface Proxy", tornado.version)) + +if __name__ == '__main__': + p = argparse.ArgumentParser(description='Osmocom Control Interface proxy.') + p.add_argument('-c', '--control-port', type = int, default = 4252, help = "Target Control Interface port") + p.add_argument('-a', '--control-host', default = 'localhost', help = "Target Control Interface adress") + p.add_argument('-b', '--host', default = 'localhost', help = "Adress to bind proxy's web interface") + p.add_argument('-p', '--port', type = int, default = 6969, help = "Port to bind proxy's web interface") + p.add_argument('-d', '--debug', action='store_true', help = "Activate debugging (default off)") + p.add_argument('-t', '--token', default = 'osmocom', help = "Token to be used by SSE client in URL e. g. http://127.0.0.1:8888/poll/osmocom where 'osmocom' is default token value") + p.add_argument('-k', '--keepalive', type = int, default = 5000, help = "Timeout betwwen keepalive messages, in milliseconds, defaults to 5000") + args = p.parse_args() + random.seed() + tornado.netutil.Resolver.configure('tornado.netutil.ThreadedResolver') # Use non-blocking resolver + logging.basicConfig() + application = tornado.web.Application([ + (r"/", Slash), + (r"/get", CtrlHandler), + (r"/get/(.*)", CtrlHandler), + (r"/set", SetCtrl), + (r"/set/(.*)/(.*)", SetCtrl), + (r"/sse/(.*)/(.*)", listener.EventSourceHandler, dict(event_class = listener.JSONIdEvent, keepalive = args.keepalive)), + ], debug = args.debug, ctrl_host = args.control_host, ctrl_port = args.control_port) + application.listen(address = args.host, port = args.port) + trap_setup(args.host, args.port, application.settings['ctrl_host'], application.settings['ctrl_port'], args.token) + tornado.ioloop.IOLoop.instance().start() diff --git a/contrib/gprs/gb-proxy-unblock-bug.py b/contrib/gprs/gb-proxy-unblock-bug.py new file mode 100755 index 000000000..0cd4b871f --- /dev/null +++ b/contrib/gprs/gb-proxy-unblock-bug.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +""" +demonstrate a unblock bug on the GB Proxy.. +""" + +bts_ns_reset = "\x02\x00\x81\x01\x01\x82\x1f\xe7\x04\x82\x1f\xe7" +ns_reset_ack = "\x03\x01\x82\x1f\xe7\x04\x82\x1f\xe7" + +bts_ns_unblock = "\x06" +ns_unblock_ack = "\x07" + +bts_bvc_reset_0 = "\x00\x00\x00\x00\x22\x04\x82\x00\x00\x07\x81\x03\x3b\x81\x02" +ns_bvc_reset_0_ack = "\x00\x00\x00\x00\x23\x04\x82\x00\x00" + +bts_bvc_reset_8167 = "\x00\x00\x00\x00\x22\x04\x82\x1f\xe7\x07\x81\x08\x08\x88\x72\xf4\x80\x10\x1c\x00\x9c\x40" + + +import socket +socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +socket.bind(("0.0.0.0", 0)) +socket.setblocking(1) + + +import sys +port = int(sys.argv[1]) +print "Sending data to port: %d" % port + +def send_and_receive(packet): + socket.sendto(packet, ("127.0.0.1", port)) + + try: + data, addr = socket.recvfrom(4096) + except socket.error, e: + print "ERROR", e + import sys + sys.exit(0) + return data + +#send stuff once + +to_send = [ + (bts_ns_reset, ns_reset_ack, "reset ack"), + (bts_ns_unblock, ns_unblock_ack, "unblock ack"), + (bts_bvc_reset_0, ns_bvc_reset_0_ack, "BVCI=0 reset ack"), +] + + +for (out, inp, type) in to_send: + res = send_and_receive(out) + if res != inp: + print "Failed to get the %s" % type + sys.exit(-1) + +import time +time.sleep(3) +res = send_and_receive(bts_bvc_reset_8167) +print "Sent all messages... check wireshark for the last response" diff --git a/contrib/gprs/gprs-bssgp-histogram.lua b/contrib/gprs/gprs-bssgp-histogram.lua new file mode 100644 index 000000000..b1ab5df7f --- /dev/null +++ b/contrib/gprs/gprs-bssgp-histogram.lua @@ -0,0 +1,78 @@ +-- Simple LUA script to print the size of BSSGP messages over their type... + +do + local ip_bucket = {} + + local pdu_types = {} + pdu_types[ 6] = "PAGING" + pdu_types[11] = "SUSPEND" + pdu_types[12] = "SUSPEND-ACK" + pdu_types[32] = "BVC-BLOCK" + pdu_types[33] = "BVC-BLOCK-ACK" + pdu_types[34] = "BVC-RESET" + pdu_types[35] = "BVC-RESET-ACK" + pdu_types[36] = "UNBLOCK" + pdu_types[37] = "UNBLOCK-ACK" + pdu_types[38] = "FLOW-CONTROL-BVC" + pdu_types[39] = "FLOW-CONTROL-BVC-ACK" + pdu_types[40] = "FLOW-CONTROL-MS" + pdu_types[41] = "FLOW-CONTROL-MS-ACK" + pdu_types[44] = "LLC-DISCARDED" + + local function init_listener() + -- handle the port as NS over IP + local udp_port_table = DissectorTable.get("udp.port") + local gprs_ns_dis = Dissector.get("gprs_ns") + udp_port_table:add(23000,gprs_ns_dis) + + -- bssgp filters + local bssgp_pdu_get = Field.new("bssgp.pdu_type") + local udp_length_get = Field.new("udp.length") + + local tap = Listener.new("ip", "udp.port == 23000") + function tap.packet(pinfo,tvb,ip) + local pdu = bssgp_pdu_get() + local len = udp_length_get() + + -- only handle bssgp, but we also want the IP frame + if not pdu then + return + end + + pdu = tostring(pdu) + if tonumber(pdu) == 0 or tonumber(pdu) == 1 then + return + end + + local ip_src = tostring(ip.ip_src) + local bssgp_histo = ip_bucket[ip_src] + if not bssgp_histo then + bssgp_histo = {} + ip_bucket[ip_src] = bssgp_histo + end + + local key = pdu + local bucket = bssgp_histo[key] + if not bucket then + bucket = {} + bssgp_histo[key] = bucket + end + + table.insert(bucket, tostring(len)) + print("IP: " .. ip_src .. " PDU: " .. pdu_types[tonumber(pdu)] .. " Length: " .. tostring(len)) + end + + function tap.draw() + -- well... this will not be called... +-- for ip,bssgp_histo in pairs(dumpers) do +-- print("IP " .. ip) +-- end + end + + function tap.reset() + -- well... this will not be called... + end + end + + init_listener() +end diff --git a/contrib/gprs/gprs-buffer-count.lua b/contrib/gprs/gprs-buffer-count.lua new file mode 100644 index 000000000..ca8864ad1 --- /dev/null +++ b/contrib/gprs/gprs-buffer-count.lua @@ -0,0 +1,80 @@ +-- I count the buffer space needed for LLC PDUs in the worse case and print it + +do + local function init_listener() + -- handle the port as NS over IP + local udp_port_table = DissectorTable.get("udp.port") + local gprs_ns_dis = Dissector.get("gprs_ns") + udp_port_table:add(23000,gprs_ns_dis) + + -- bssgp filters + local bssgp_pdu_get = Field.new("bssgp.pdu_type") + local bssgp_delay_get = Field.new("bssgp.delay_val") + local llcgprs_get = Field.new("llcgprs") + local pdus = nil + + print("START...") + + local tap = Listener.new("ip", "udp.port == 23000 && bssgp.pdu_type == 0") + function tap.packet(pinfo,tvb,ip) + local pdu = bssgp_pdu_get() + local len = llcgprs_get().len + local delay = bssgp_delay_get() + + -- only handle bssgp, but we also want the IP frame + if not pdu then + return + end + + if tonumber(tostring(delay)) == 65535 then + pdus = { next = pdus, + len = len, + expires = -1 } + else + local off = tonumber(tostring(delay)) / 100.0 + pdus = { next = pdus, + len = len, + expires = pinfo.rel_ts + off } + end + local now_time = tonumber(tostring(pinfo.rel_ts)) + local now_size = 0 + local l = pdus + local prev = nil + local count = 0 + while l do + if now_time < l.expires or l.expires == -1 then + now_size = now_size + l.len + prev = l + l = l.next + count = count + 1 + else + -- delete things + if prev == nil then + pdus = nil + l = nil + else + prev.next = l.next + l = l.next + end + end + end +-- print("TOTAL: " .. now_time .. " PDU_SIZE: " .. now_size) + print(now_time .. " " .. now_size / 1024.0 .. " " .. count) +-- print("NOW: " .. tostring(pinfo.rel_ts) .. " Delay: " .. tostring(delay) .. " Length: " .. tostring(len)) + end + + function tap.draw() + -- well... this will not be called... +-- for ip,bssgp_histo in pairs(dumpers) do +-- print("IP " .. ip) +-- end + print("END") + end + + function tap.reset() + -- well... this will not be called... + end + end + + init_listener() +end diff --git a/contrib/gprs/gprs-split-trace-by-tlli.lua b/contrib/gprs/gprs-split-trace-by-tlli.lua new file mode 100644 index 000000000..018c377c5 --- /dev/null +++ b/contrib/gprs/gprs-split-trace-by-tlli.lua @@ -0,0 +1,46 @@ +-- Create a file named by_ip/''ip_addess''.cap with all ip traffic of each ip host. (works for tshark only) +-- Dump files are created for both source and destination hosts +do + local dir = "by_tlli" + local dumpers = {} + local function init_listener() + local udp_port_table = DissectorTable.get("udp.port") + local gprs_ns_dis = Dissector.get("gprs_ns") + udp_port_table:add(23000,gprs_ns_dis) + + local field_tlli = Field.new("bssgp.tlli") + local tap = Listener.new("ip", "udp.port == 23000") + + -- we will be called once for every IP Header. + -- If there's more than one IP header in a given packet we'll dump the packet once per every header + function tap.packet(pinfo,tvb,ip) + local tlli = field_tlli() + if not tlli then + return + end + + local tlli_str = tostring(tlli) + tlli_dmp = dumpers[tlli_str] + if not tlli_dmp then + local tlli_hex = string.format("0x%x", tonumber(tlli_str)) + print("Creating dump for TLLI " .. tlli_hex) + tlli_dmp = Dumper.new_for_current(dir .. "/" .. tlli_hex .. ".pcap") + dumpers[tlli_str] = tlli_dmp + end + tlli_dmp:dump_current() + tlli_dmp:flush() + end + function tap.draw() + for tlli,dumper in pairs(dumpers) do + dumper:flush() + end + end + function tap.reset() + for tlli,dumper in pairs(dumpers) do + dumper:close() + end + dumpers = {} + end + end + init_listener() +end diff --git a/contrib/gprs/gprs-verify-nu.lua b/contrib/gprs/gprs-verify-nu.lua new file mode 100644 index 000000000..e44fdd16f --- /dev/null +++ b/contrib/gprs/gprs-verify-nu.lua @@ -0,0 +1,59 @@ +-- This script verifies that the N(U) is increasing... +-- +do + local nu_state_src = {} + + local function init_listener() + -- handle the port as NS over IP + local udp_port_table = DissectorTable.get("udp.port") + local gprs_ns_dis = Dissector.get("gprs_ns") + udp_port_table:add(23000,gprs_ns_dis) + + -- we want to look here... + local llc_sapi_get = Field.new("llcgprs.sapib") + local llc_nu_get = Field.new("llcgprs.nu") + local bssgp_tlli_get = Field.new("bssgp.tlli") + + local tap = Listener.new("ip", "udp.port == 23000") + function tap.packet(pinfo,tvb,ip) + local llc_sapi = llc_sapi_get() + local llc_nu = llc_nu_get() + local bssgp_tlli = bssgp_tlli_get() + + if not llc_sapi or not llc_nu or not bssgp_tlli then + return + end + + local ip_src = tostring(ip.ip_src) + local bssgp_tlli = tostring(bssgp_tlli) + local llc_nu = tostring(llc_nu) + local llc_sapi = tostring(llc_sapi) + + local src_key = ip_src .. "-" .. bssgp_tlli .. "-" .. llc_sapi + local last_nu = nu_state_src[src_key] + if not last_nu then + -- print("Establishing mapping for " .. src_key) + nu_state_src[src_key] = llc_nu + return + end + + local function tohex(number) + return string.format("0x%x", tonumber(number)) + end + + nu_state_src[src_key] = llc_nu + if tonumber(last_nu) + 1 ~= tonumber(llc_nu) then + print("JUMP in N(U) on TLLI " .. tohex(bssgp_tlli) .. " and SAPI: " .. llc_sapi .. " src: " .. ip_src) + print("\t last: " .. last_nu .. " now: " .. llc_nu) + end + end + + function tap.draw() + end + + function tap.reset() + end + end + init_listener() +end + diff --git a/contrib/hlr-remove-old.sql b/contrib/hlr-remove-old.sql new file mode 100644 index 000000000..626a331e1 --- /dev/null +++ b/contrib/hlr-remove-old.sql @@ -0,0 +1,18 @@ +-- Remove old data from the database +DELETE FROM Subscriber + WHERE id != 1 AND datetime('now', '-10 days') > updated AND authorized != 1; +DELETE FROM Equipment + WHERE datetime('now', '-10 days') > updated; +DELETE FROM EquipmentWatch + WHERE datetime('now', '-10 days') > updated; +DELETE FROM SMS + WHERE datetime('now', '-10 days') > created; +DELETE FROM VLR + WHERE datetime('now', '-10 days') > updated; +DELETE FROM ApduBlobs + WHERE datetime('now', '-10 days') > created; +DELETE FROM Counters + WHERE datetime('now', '-10 days') > timestamp; +DELETE FROM RateCounters + WHERE datetime('now', '-10 days') > timestamp; +VACUUM; diff --git a/contrib/hlrsync/hlrsync.py b/contrib/hlrsync/hlrsync.py new file mode 100755 index 000000000..e4a495555 --- /dev/null +++ b/contrib/hlrsync/hlrsync.py @@ -0,0 +1,125 @@ +#!/usr/bin/python2.5 + +from __future__ import with_statement + +from pysqlite2 import dbapi2 as sqlite3 +import sys + +hlr = sqlite3.connect(sys.argv[1]) +web = sqlite3.connect(sys.argv[2]) + +# switch to autocommit +hlr.isolation_level = None +web.isolation_level = None + +hlr.row_factory = sqlite3.Row +web.row_factory = sqlite3.Row + +with hlr: + hlr_subscrs = hlr.execute(""" + SELECT * FROM Subscriber + """).fetchall() + hlr_tokens = hlr.execute(""" + SELECT * FROM AuthToken + """).fetchall() + +with web: + web_tokens = web.execute(""" + SELECT * FROM reg_tokens + """).fetchall() + web_sms = web.execute(""" + SELECT * FROM sms_queue + """).fetchall() + +# index by subscr id +hlr_subscrs_by_id = {} +hlr_subscrs_by_ext = {} +hlr_tokens_by_subscr_id = {} +for x in hlr_subscrs: + hlr_subscrs_by_id[x['id']] = x + hlr_subscrs_by_ext[x['extension']] = x +del hlr_subscrs +for x in hlr_tokens: + hlr_tokens_by_subscr_id[x['subscriber_id']] = x +del hlr_tokens + +web_tokens_by_subscr_id = {} +for x in web_tokens: + web_tokens_by_subscr_id[x['subscriber_id']] = x +del web_tokens + +# remove leftover web_tokens and correct inconsistent fields +with web: + for x in web_tokens_by_subscr_id.values(): + subscr = hlr_subscrs_by_id.get(x['subscriber_id'], None) + if subscr is None: + web.execute(""" + DELETE FROM reg_tokens WHERE subscriber_id = ? + """, (x['subscriber_id'],)) + del web_tokens_by_subscr_id[x['subscriber_id']] + continue + if str(x['imsi']) != str(subscr['imsi']) or \ + x['extension'] != subscr['extension'] or \ + x['tmsi'] != subscr['tmsi'] or \ + x['lac'] != subscr['lac']: + web.execute(""" + UPDATE reg_tokens + SET imsi = ?, extension = ?, tmsi = ?, lac = ? + WHERE subscriber_id = ? + """, (str(subscr['imsi']), subscr['extension'], + subscr['tmsi'], subscr['lac'], x['subscriber_id'])) + +# add missing web_tokens +with web: + for x in hlr_tokens_by_subscr_id.values(): + subscr = hlr_subscrs_by_id.get(x['subscriber_id'], None) + if subscr is None: + hlr.execute(""" + DELETE FROM AuthToken WHERE subscriber_id = ? + """, (x['subscriber_id'],)) + del hlr_tokens_by_subscr_id[x['subscriber_id']] + continue + webtoken = web_tokens_by_subscr_id.get(x['subscriber_id'], None) + if webtoken is None: + web.execute(""" + INSERT INTO reg_tokens + (subscriber_id, extension, reg_completed, name, email, lac, imsi, token, tmsi) + VALUES + (?, ?, 0, ?, '', ?, ?, ?, ?) + """, (x['subscriber_id'], subscr['extension'], subscr['name'], + subscr['lac'], str(subscr['imsi']), x['token'], subscr['tmsi'])) + +# authorize subscribers +with hlr: + for x in web_tokens_by_subscr_id.values(): + subscr = hlr_subscrs_by_id.get(x['subscriber_id'], None) + if x['reg_completed'] and not subscr['authorized']: + hlr.execute(""" + UPDATE Subscriber + SET authorized = 1 + WHERE id = ? + """, (x['subscriber_id'],)) + +# Sync SMS from web to hlr +with hlr: + for sms in web_sms: + subscr = hlr_subscrs_by_ext.get(sms['receiver_ext']) + if subscr is None: + print '%s not found' % sms['receiver_ext'] + continue + hlr.execute(""" + INSERT INTO SMS + (created, sender_id, receiver_id, reply_path_req, status_rep_req, protocol_id, data_coding_scheme, ud_hdr_ind, text) + VALUES + (?, 1, ?, 0, 0, 0, 0, 0, ?) + """, (sms['created'], subscr['id'], sms['text'])) +with web: + for sms in web_sms: + web.execute(""" + DELETE FROM sms_queue WHERE id = ? + """, (sms['id'],)) + + +hlr.close() +web.close() + diff --git a/contrib/ipa.py b/contrib/ipa.py new file mode 100755 index 000000000..71cbf45a4 --- /dev/null +++ b/contrib/ipa.py @@ -0,0 +1,278 @@ +#!/usr/bin/python3 +# -*- mode: python-mode; py-indent-tabs-mode: nil -*- +""" +/* + * Copyright (C) 2016 sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +""" + +import struct, random, sys + +class IPA(object): + """ + Stateless IPA protocol multiplexer: add/remove/parse (extended) header + """ + version = "0.0.5" + TCP_PORT_OML = 3002 + TCP_PORT_RSL = 3003 + # OpenBSC extensions: OSMO, MGCP_OLD + PROTO = dict(RSL=0x00, CCM=0xFE, SCCP=0xFD, OML=0xFF, OSMO=0xEE, MGCP_OLD=0xFC) + # ...OML Router Control, GSUP GPRS extension, Osmocom Authn Protocol + EXT = dict(CTRL=0, MGCP=1, LAC=2, SMSC=3, ORC=4, GSUP=5, OAP=6) + # OpenBSC extension: SCCP_OLD + MSGT = dict(PING=0x00, PONG=0x01, ID_GET=0x04, ID_RESP=0x05, ID_ACK=0x06, SCCP_OLD=0xFF) + _IDTAG = dict(SERNR=0, UNITNAME=1, LOCATION=2, TYPE=3, EQUIPVERS=4, SWVERSION=5, IPADDR=6, MACADDR=7, UNIT=8) + CTRL_GET = 'GET' + CTRL_SET = 'SET' + CTRL_REP = 'REPLY' + CTRL_ERR = 'ERR' + CTRL_TRAP = 'TRAP' + + def _l(self, d, p): + """ + Reverse dictionary lookup: return key for a given value + """ + if p is None: + return 'UNKNOWN' + return list(d.keys())[list(d.values()).index(p)] + + def _tag(self, t, v): + """ + Create TAG as TLV data + """ + return struct.pack(">HB", len(v) + 1, t) + v + + def proto(self, p): + """ + Lookup protocol name + """ + return self._l(self.PROTO, p) + + def ext(self, p): + """ + Lookup protocol extension name + """ + return self._l(self.EXT, p) + + def msgt(self, p): + """ + Lookup message type name + """ + return self._l(self.MSGT, p) + + def idtag(self, p): + """ + Lookup ID tag name + """ + return self._l(self._IDTAG, p) + + def ext_name(self, proto, exten): + """ + Return proper extension byte name depending on the protocol used + """ + if self.PROTO['CCM'] == proto: + return self.msgt(exten) + if self.PROTO['OSMO'] == proto: + return self.ext(exten) + return None + + def add_header(self, data, proto, ext=None): + """ + Add IPA header (with extension if necessary), data must be represented as bytes + """ + if ext is None: + return struct.pack(">HB", len(data) + 1, proto) + data + return struct.pack(">HBB", len(data) + 1, proto, ext) + data + + def del_header(self, data): + """ + Strip IPA protocol header correctly removing extension if present + Returns data length, IPA protocol, extension (or None if not defined for a give protocol) and the data without header + """ + if not len(data): + return None, None, None, None + (dlen, proto) = struct.unpack('>HB', data[:3]) + if self.PROTO['OSMO'] == proto or self.PROTO['CCM'] == proto: # there's extension which we have to unpack + return struct.unpack('>HBB', data[:4]) + (data[4:], ) # length, protocol, extension, data + return dlen, proto, None, data[3:] # length, protocol, _, data + + def split_combined(self, data): + """ + Split the data which contains multiple concatenated IPA messages into tuple (first, rest) where rest contains remaining messages, first is the single IPA message + """ + (length, _, _, _) = self.del_header(data) + return data[:(length + 3)], data[(length + 3):] + + def tag_serial(self, data): + """ + Make TAG for serial number + """ + return self._tag(self._IDTAG['SERNR'], data) + + def tag_name(self, data): + """ + Make TAG for unit name + """ + return self._tag(self._IDTAG['UNITNAME'], data) + + def tag_loc(self, data): + """ + Make TAG for location + """ + return self._tag(self._IDTAG['LOCATION'], data) + + def tag_type(self, data): + """ + Make TAG for unit type + """ + return self._tag(self._IDTAG['TYPE'], data) + + def tag_equip(self, data): + """ + Make TAG for equipment version + """ + return self._tag(self._IDTAG['EQUIPVERS'], data) + + def tag_sw(self, data): + """ + Make TAG for software version + """ + return self._tag(self._IDTAG['SWVERSION'], data) + + def tag_ip(self, data): + """ + Make TAG for IP address + """ + return self._tag(self._IDTAG['IPADDR'], data) + + def tag_mac(self, data): + """ + Make TAG for MAC address + """ + return self._tag(self._IDTAG['MACADDR'], data) + + def tag_unit(self, data): + """ + Make TAG for unit ID + """ + return self._tag(self._IDTAG['UNIT'], data) + + def identity(self, unit=b'', mac=b'', location=b'', utype=b'', equip=b'', sw=b'', name=b'', serial=b''): + """ + Make IPA IDENTITY tag list, by default returns empty concatenated bytes of tag list + """ + return self.tag_unit(unit) + self.tag_mac(mac) + self.tag_loc(location) + self.tag_type(utype) + self.tag_equip(equip) + self.tag_sw(sw) + self.tag_name(name) + self.tag_serial(serial) + + def ping(self): + """ + Make PING message + """ + return self.add_header(b'', self.PROTO['CCM'], self.MSGT['PING']) + + def pong(self): + """ + Make PONG message + """ + return self.add_header(b'', self.PROTO['CCM'], self.MSGT['PONG']) + + def id_ack(self): + """ + Make ID_ACK CCM message + """ + return self.add_header(b'', self.PROTO['CCM'], self.MSGT['ID_ACK']) + + def id_get(self): + """ + Make ID_GET CCM message + """ + return self.add_header(self.identity(), self.PROTO['CCM'], self.MSGT['ID_GET']) + + def id_resp(self, data): + """ + Make ID_RESP CCM message + """ + return self.add_header(data, self.PROTO['CCM'], self.MSGT['ID_RESP']) + +class Ctrl(IPA): + """ + Osmocom CTRL protocol implemented on top of IPA multiplexer + """ + def __init__(self): + random.seed() + + def add_header(self, data): + """ + Add CTRL header + """ + return super(Ctrl, self).add_header(data.encode('utf-8'), IPA.PROTO['OSMO'], IPA.EXT['CTRL']) + + def rem_header(self, data): + """ + Remove CTRL header, check for appropriate protocol and extension + """ + (_, proto, ext, d) = super(Ctrl, self).del_header(data) + if self.PROTO['OSMO'] != proto or self.EXT['CTRL'] != ext: + return None + return d + + def parse(self, data, op=None): + """ + Parse Ctrl string returning (var, value) pair + var could be None in case of ERROR message + value could be None in case of GET message + """ + (s, i, v) = data.split(' ', 2) + if s == self.CTRL_ERR: + return None, v + if s == self.CTRL_GET: + return v, None + (s, i, var, val) = data.split(' ', 3) + if s == self.CTRL_TRAP and i != '0': + return None, '%s with non-zero id %s' % (s, i) + if op is not None and i != op: + if s == self.CTRL_GET + '_' + self.CTRL_REP or s == self.CTRL_SET + '_' + self.CTRL_REP: + return None, '%s with unexpected id %s' % (s, i) + return var, val + + def trap(self, var, val): + """ + Make TRAP message with given (vak, val) pair + """ + return self.add_header("%s 0 %s %s" % (self.CTRL_TRAP, var, val)) + + def cmd(self, var, val=None): + """ + Make SET/GET command message: returns (r, m) tuple where r is random operation id and m is assembled message + """ + r = random.randint(1, sys.maxsize) + if val is not None: + return r, self.add_header("%s %s %s %s" % (self.CTRL_SET, r, var, val)) + return r, self.add_header("%s %s %s" % (self.CTRL_GET, r, var)) + + def verify(self, reply, r, var, val=None): + """ + Verify reply to SET/GET command: returns (b, v) tuple where v is True/False verification result and v is the variable value + """ + (k, v) = self.parse(reply) + if k != var or (val is not None and v != val): + return False, v + return True, v + +if __name__ == '__main__': + print("IPA multiplexer v%s loaded." % IPA.version) diff --git a/contrib/jenkins.sh b/contrib/jenkins.sh index 068ee34e5..b315b9772 100755 --- a/contrib/jenkins.sh +++ b/contrib/jenkins.sh @@ -43,7 +43,6 @@ echo set -x cd "$base" -cd openbsc autoreconf --install --force ./configure --enable-osmo-bsc --enable-nat $SMPP $MGCP $IU --enable-vty-tests --enable-external-tests $MAKE $PARALLEL_MAKE diff --git a/contrib/mgcp_server.py b/contrib/mgcp_server.py new file mode 100755 index 000000000..05c489db5 --- /dev/null +++ b/contrib/mgcp_server.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# Simple server for mgcp... send audit, receive response.. + +import socket, time + +MGCP_GATEWAY_PORT = 2427 +MGCP_CALLAGENT_PORT = 2727 + +rsip_resp = """200 321321332\r\n""" +audit_packet = """AUEP %d 13@mgw MGCP 1.0\r\n""" +crcx_packet = """CRCX %d 14@mgw MGCP 1.0\r\nC: 4a84ad5d25f\r\nL: p:20, a:GSM-EFR, nt:IN\r\nM: recvonly\r\n""" +dlcx_packet = """DLCX %d 14@mgw MGCP 1.0\r\nC: 4a84ad5d25f\r\nI: %d\r\n""" +mdcx_packet = """MDCX %d 14@mgw MGCP 1.0\r\nC: 4a84ad5d25f\r\nI: %d\r\nL: p:20, a:GSM-EFR, nt:IN\r\nM: recvonly\r\n\r\nv=0\r\no=- 258696477 0 IN IP4 172.16.1.107\r\ns=-\r\nc=IN IP4 172.16.1.107\r\nt=0 0\r\nm=audio 6666 RTP/AVP 127\r\na=rtpmap:127 GSM-EFR/8000/1\r\na=ptime:20\r\na=recvonly\r\nm=image 4402 udptl t38\r\na=T38FaxVersion:0\r\na=T38MaxBitRate:14400\r\n""" + +def hexdump(src, length=8): + """Recipe is from http://code.activestate.com/recipes/142812/""" + result = [] + digits = 4 if isinstance(src, unicode) else 2 + for i in xrange(0, len(src), length): + s = src[i:i+length] + hexa = b' '.join(["%0*X" % (digits, ord(x)) for x in s]) + text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s]) + result.append( b"%04X %-*s %s" % (i, length*(digits + 1), hexa, text) ) + return b'\n'.join(result) + +server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +server_socket.bind(("127.0.0.1", MGCP_CALLAGENT_PORT)) +server_socket.setblocking(1) + +last_ci = 1 +def send_and_receive(packet): + global last_ci + server_socket.sendto(packet, ("127.0.0.1", MGCP_GATEWAY_PORT)) + try: + data, addr = server_socket.recvfrom(4096) + + # attempt to store the CI of the response + list = data.split("\n") + for item in list: + if item.startswith("I: "): + last_ci = int(item[3:]) + + print hexdump(data), addr + except socket.error, e: + print e + pass + +def generate_tid(): + import random + return random.randint(0, 65123) + + + +while True: + send_and_receive(audit_packet % generate_tid()) + send_and_receive(crcx_packet % generate_tid() ) + send_and_receive(mdcx_packet % (generate_tid(), last_ci)) + send_and_receive(dlcx_packet % (generate_tid(), last_ci)) + + time.sleep(3) diff --git a/contrib/nat/test_regexp.c b/contrib/nat/test_regexp.c new file mode 100644 index 000000000..808a703ca --- /dev/null +++ b/contrib/nat/test_regexp.c @@ -0,0 +1,30 @@ +/* make test_regexp */ +#include <sys/types.h> +#include <regex.h> +#include <stdio.h> + + +int main(int argc, char **argv) +{ + regex_t reg; + regmatch_t matches[2]; + + if (argc != 4) { + printf("Invoke with: test_regexp REGEXP REPLACE NR\n"); + return -1; + } + + if (regcomp(®, argv[1], REG_EXTENDED) != 0) { + fprintf(stderr, "Regexp '%s' is not valid.\n", argv[1]); + return -1; + } + + if (regexec(®, argv[3], 2, matches, 0) == 0 && matches[1].rm_eo != -1) + printf("New Number: %s%s\n", argv[2], &argv[3][matches[1].rm_so]); + else + printf("No match.\n"); + + regfree(®); + + return 0; +} diff --git a/contrib/nat/ussd_example.py b/contrib/nat/ussd_example.py new file mode 100644 index 000000000..8f7a58d3f --- /dev/null +++ b/contrib/nat/ussd_example.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python2.7 + +""" +AGPLv3+ 2016 Copyright Holger Hans Peter Freyther + +Example of how to connect to the USSD side-channel and how to respond +with a fixed message. +""" + +import socket +import struct + +ussdSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +ussdSocket.connect(('127.0.0.1', 5001)) + +def send_dt1(dstref, data): + dlen = struct.pack('B', len(data)).encode('hex') + hex = '06' + dstref.encode('hex') + '00' + '01' + dlen + data.encode('hex') + pdata = hex.decode('hex') + out = struct.pack('>HB', len(pdata), 0xfd) + pdata + ussdSocket.send(out) + +def send_rel(srcref, dstref): + hex = '04' + dstref.encode('hex') + srcref.encode('hex') + '000100' + pdata = hex.decode('hex') + out = struct.pack('>HB', len(pdata), 0xfd) + pdata + ussdSocket.send(out) + +def recv_one(): + plen = ussdSocket.recv(3) + (plen,ptype) = struct.unpack(">HB", plen) + data = ussdSocket.recv(plen) + + return ptype, data + +# Assume this is the ID request +data = ussdSocket.recv(4) +ussdSocket.send("\x00\x08\xfe\x05\x00" + "\x05\x01" + "ussd") +# ^len ^len of tag ... and ignore + +# Expect a fake message. see struct ipac_msgt_sccp_state +ptype, data = recv_one() +print("%d %s" % (ptype, data.encode('hex'))) +(srcref, dstref, transid, invokeid) = struct.unpack("<3s3sBB", data[1:9]) +print("New transID %d invoke %d" % (transid, invokeid)) + +# Expect a the invocation.. todo.. extract invoke id +ptype, data = recv_one() +print("%d %s" % (ptype, data.encode('hex'))) + +# Reply with BSSAP + GSM 04.08 + MAP portion +# 00 == invoke id 0f == DCS +res = "01002a9b2a0802e1901c22a220020100301b02013b301604010f041155e7d2f9bc3a41412894991c06a9c9a713" +send_dt1(dstref, res.decode('hex')) + +clear = "000420040109" +send_dt1(dstref, clear.decode('hex')) + +# should be the clear complete +send_rel(srcref, dstref) + +# Give it some time to handle connection shutdown properly +print("Gracefully sleeping") +import time +time.sleep(3) diff --git a/contrib/rtp/gen_rtp_header.erl b/contrib/rtp/gen_rtp_header.erl new file mode 100755 index 000000000..47839c1ca --- /dev/null +++ b/contrib/rtp/gen_rtp_header.erl @@ -0,0 +1,420 @@ +#!/usr/bin/env escript +%% -*- erlang -*- +%%! -smp disable +-module(gen_rtp_header). + +% -mode(compile). + +-define(VERSION, "0.1"). + +-export([main/1]). + +-record(rtp_packet, + { + version = 2, + padding = 0, + marker = 0, + payload_type = 0, + seqno = 0, + timestamp = 0, + ssrc = 0, + csrcs = [], + extension = <<>>, + payload = <<>>, + realtime + }). + + +main(Args) -> + DefaultOpts = [{format, state}, + {ssrc, 16#11223344}, + {rate, 8000}, + {pt, 98}], + {PosArgs, Opts} = getopts_checked(Args, DefaultOpts), + log(debug, fun (Dev) -> + io:format(Dev, "Initial options:~n", []), + dump_opts(Dev, Opts), + io:format(Dev, "~s: ~p~n", ["Args", PosArgs]) + end, [], Opts), + main(PosArgs, Opts). + +main([First | RemArgs], Opts) -> + try + F = list_to_integer(First), + Format = proplists:get_value(format, Opts, state), + PayloadData = proplists:get_value(payload, Opts, undef), + InFile = proplists:get_value(file, Opts, undef), + + Payload = case {PayloadData, InFile} of + {undef, undef} -> + % use default value + #rtp_packet{}#rtp_packet.payload; + {P, undef} -> P; + {_, File} -> + log(info, "Loading file '~s'~n", [File], Opts), + {ok, InDev} = file:open(File, [read]), + DS = [ Pl#rtp_packet.payload || {_T, Pl} <- read_packets(InDev, Opts)], + file:close(InDev), + log(debug, "File '~s' closed, ~w packets read.~n", [File, length(DS)], Opts), + DS + end, + Dev = standard_io, + write_packet_pre(Dev, Format), + do_groups(Dev, Payload, F, RemArgs, Opts), + write_packet_post(Dev, Format), + 0 + catch + _:_ -> + log(debug, "~p~n", [hd(erlang:get_stacktrace())], Opts), + usage(), + halt(1) + end + ; + +main(_, _Opts) -> + usage(), + halt(1). + +%%% group (count + offset) handling %%% + +do_groups(_Dev, _Pl, _F, [], _Opts) -> + ok; + +do_groups(Dev, Pl, F, [L], Opts) -> + do_groups(Dev, Pl, F, [L, 0], Opts); + +do_groups(Dev, Pl, First, [L, O | Args], Opts) -> + Ssrc = proplists:get_value(ssrc, Opts, #rtp_packet.ssrc), + PT = proplists:get_value(pt, Opts, #rtp_packet.payload_type), + Len = list_to_num(L), + Offs = list_to_num(O), + log(info, "Starting group: Ssrc=~.16B, PT=~B, First=~B, Len=~B, Offs=~B~n", + [Ssrc, PT, First, Len, Offs], Opts), + Pkg = #rtp_packet{ssrc = Ssrc, payload_type = PT}, + Pl2 = write_packets(Dev, Pl, Pkg, First, Len, Offs, Opts), + {Args2, Opts2} = getopts_checked(Args, Opts), + log(debug, fun (Io) -> + io:format(Io, "Changed options:~n", []), + dump_opts(Io, Opts2 -- Opts) + end, [], Opts), + do_groups(Dev, Pl2, First+Len, Args2, Opts2). + +%%% error handling helpers %%% + +getopts_checked(Args, Opts) -> + try + getopts(Args, Opts) + catch + C:R -> + log(error, "~s~n", + [explain_error(C, R, erlang:get_stacktrace(), Opts)], Opts), + usage(), + halt(1) + end. + +explain_error(error, badarg, [{erlang,list_to_integer,[S,B]} | _ ], _Opts) -> + io_lib:format("Invalid number '~s' (base ~B)", [S, B]); +explain_error(error, badarg, [{erlang,list_to_integer,[S]} | _ ], _Opts) -> + io_lib:format("Invalid decimal number '~s'", [S]); +explain_error(C, R, [Hd | _ ], _Opts) -> + io_lib:format("~p, ~p:~p", [Hd, C, R]); +explain_error(_, _, [], _Opts) -> + "". + +%%% usage and options %%% + +myname() -> + filename:basename(escript:script_name()). + +usage(Text) -> + io:format(standard_error, "~s: ~s~n", [myname(), Text]), + usage(). + +usage() -> + io:format(standard_error, + "Usage: ~s [Options] Start Count1 Offs1 [[Options] Count2 Offs2 ...]~n", + [myname()]). + +show_version() -> + io:format(standard_io, + "~s ~s~n", [myname(), ?VERSION]). + +show_help() -> + io:format(standard_io, + "Usage: ~s [Options] Start Count1 Offs1 [[Options] Count2 Offs2 ...]~n~n" ++ + "Options:~n" ++ + " -h, --help this text~n" ++ + " --version show version info~n" ++ + " -i, --file=FILE reads payload from file (state format by default)~n" ++ + " -f, --frame-size=N read payload as binary frames of size N instead~n" ++ + " -p, --payload=HEX set constant payload~n" ++ + " --verbose=N set verbosity~n" ++ + " -v increase verbosity~n" ++ + " --format=state use state format for output (default)~n" ++ + " -C, --format=c use simple C lines for output~n" ++ + " --format=carray use a C array for output~n" ++ + " -s, --ssrc=SSRC set the SSRC~n" ++ + " -t, --type=N set the payload type~n" ++ + " -r, --rate=N set the RTP rate [8000]~n" ++ + " -D, --duration=N set the packet duration in RTP time units [160]~n" ++ + " -d, --delay=FLOAT add offset to playout timestamp~n" ++ + "~n" ++ + "Arguments:~n" ++ + " Start initial packet (sequence) number~n" ++ + " Count number of packets~n" ++ + " Offs timestamp offset (in RTP units)~n" ++ + "", [myname()]). + +getopts([ "--file=" ++ File | R], Opts) -> + getopts(R, [{file, File} | Opts]); +getopts([ "-i" ++ T | R], Opts) -> + getopts_alias_arg("--file", T, R, Opts); +getopts([ "--frame-size=" ++ N | R], Opts) -> + Size = list_to_integer(N), + getopts(R, [{frame_size, Size}, {in_format, bin} | Opts]); +getopts([ "-f" ++ T | R], Opts) -> + getopts_alias_arg("--frame-size", T, R, Opts); +getopts([ "--duration=" ++ N | R], Opts) -> + Duration = list_to_integer(N), + getopts(R, [{duration, Duration} | Opts]); +getopts([ "-D" ++ T | R], Opts) -> + getopts_alias_arg("--duration", T, R, Opts); +getopts([ "--rate=" ++ N | R], Opts) -> + Rate = list_to_integer(N), + getopts(R, [{rate, Rate} | Opts]); +getopts([ "-r" ++ T | R], Opts) -> + getopts_alias_arg("--rate", T, R, Opts); +getopts([ "--version" | _], _Opts) -> + show_version(), + halt(0); +getopts([ "--help" | _], _Opts) -> + show_help(), + halt(0); +getopts([ "-h" ++ T | R], Opts) -> + getopts_alias_no_arg("--help", T, R, Opts); +getopts([ "--verbose=" ++ V | R], Opts) -> + Verbose = list_to_integer(V), + getopts(R, [{verbose, Verbose} | Opts]); +getopts([ "-v" ++ T | R], Opts) -> + Verbose = proplists:get_value(verbose, Opts, 0), + getopts_short_no_arg(T, R, [ {verbose, Verbose+1} | Opts]); +getopts([ "--format=state" | R], Opts) -> + getopts(R, [{format, state} | Opts]); +getopts([ "--format=c" | R], Opts) -> + getopts(R, [{format, c} | Opts]); +getopts([ "-C" ++ T | R], Opts) -> + getopts_alias_no_arg("--format=c", T, R, Opts); +getopts([ "--format=carray" | R], Opts) -> + getopts(R, [{format, carray} | Opts]); +getopts([ "--payload=" ++ Hex | R], Opts) -> + getopts(R, [{payload, hex_to_bin(Hex)} | Opts]); +getopts([ "--ssrc=" ++ Num | R], Opts) -> + getopts(R, [{ssrc, list_to_num(Num)} | Opts]); +getopts([ "-s" ++ T | R], Opts) -> + getopts_alias_arg("--ssrc", T, R, Opts); +getopts([ "--type=" ++ Num | R], Opts) -> + getopts(R, [{pt, list_to_num(Num)} | Opts]); +getopts([ "-t" ++ T | R], Opts) -> + getopts_alias_arg("--type", T, R, Opts); +getopts([ "--delay=" ++ Num | R], Opts) -> + getopts(R, [{delay, list_to_float(Num)} | Opts]); +getopts([ "-d" ++ T | R], Opts) -> + getopts_alias_arg("--delay", T, R, Opts); + +% parsing helpers +getopts([ "--" | R], Opts) -> + {R, normalize_opts(Opts)}; +getopts([ O = "--" ++ _ | _], _Opts) -> + usage("Invalid option: " ++ O), + halt(1); +getopts([ [ $-, C | _] | _], _Opts) when C < $0; C > $9 -> + usage("Invalid option: -" ++ [C]), + halt(1); + +getopts(R, Opts) -> + {R, normalize_opts(Opts)}. + +getopts_short_no_arg([], R, Opts) -> getopts(R, Opts); +getopts_short_no_arg(T, R, Opts) -> getopts([ "-" ++ T | R], Opts). + +getopts_alias_no_arg(A, [], R, Opts) -> getopts([A | R], Opts); +getopts_alias_no_arg(A, T, R, Opts) -> getopts([A, "-" ++ T | R], Opts). + +getopts_alias_arg(A, [], [T | R], Opts) -> getopts([A ++ "=" ++ T | R], Opts); +getopts_alias_arg(A, T, R, Opts) -> getopts([A ++ "=" ++ T | R], Opts). + +normalize_opts(Opts) -> + [ proplists:lookup(E, Opts) || E <- proplists:get_keys(Opts) ]. + +%%% conversions %%% + +bin_to_hex(Bin) -> [hd(integer_to_list(N,16)) || <<N:4>> <= Bin]. +hex_to_bin(Hex) -> << <<(list_to_integer([Nib],16)):4>> || Nib <- Hex>>. + +list_to_num("-" ++ Str) -> -list_to_num(Str); +list_to_num("0x" ++ Str) -> list_to_integer(Str, 16); +list_to_num("0b" ++ Str) -> list_to_integer(Str, 2); +list_to_num(Str = [ $0 | _ ]) -> list_to_integer(Str, 8); +list_to_num(Str) -> list_to_integer(Str, 10). + +%%% dumping data %%% + +dump_opts(Dev, Opts) -> + dump_opts2(Dev, Opts, proplists:get_keys(Opts)). + +dump_opts2(Dev, Opts, [OptName | R]) -> + io:format(Dev, " ~-10s: ~p~n", + [OptName, proplists:get_value(OptName, Opts)]), + dump_opts2(Dev, Opts, R); +dump_opts2(_Dev, _Opts, []) -> ok. + +%%% logging %%% + +log(L, Fmt, Args, Opts) when is_list(Opts) -> + log(L, Fmt, Args, proplists:get_value(verbose, Opts, 0), Opts). + +log(debug, Fmt, Args, V, Opts) when V > 2 -> log2("DEBUG", Fmt, Args, Opts); +log(info, Fmt, Args, V, Opts) when V > 1 -> log2("INFO", Fmt, Args, Opts); +log(notice, Fmt, Args, V, Opts) when V > 0 -> log2("NOTICE", Fmt, Args, Opts); +log(warn, Fmt, Args, _V, Opts) -> log2("WARNING", Fmt, Args, Opts); +log(error, Fmt, Args, _V, Opts) -> log2("ERROR", Fmt, Args, Opts); + +log(Lvl, Fmt, Args, V, Opts) when V >= Lvl -> log2("", Fmt, Args, Opts); + +log(_, _, _, _i, _) -> ok. + +log2(Type, Fmt, Args, _Opts) when is_list(Fmt) -> + io:format(standard_error, "~s: " ++ Fmt, [Type | Args]); +log2("", Fmt, Args, _Opts) when is_list(Fmt) -> + io:format(standard_error, Fmt, Args); +log2(_Type, Fun, _Args, _Opts) when is_function(Fun, 1) -> + Fun(standard_error). + +%%% RTP packets %%% + +make_rtp_packet(P = #rtp_packet{version = 2}) -> + << (P#rtp_packet.version):2, + 0:1, % P + 0:1, % X + 0:4, % CC + (P#rtp_packet.marker):1, + (P#rtp_packet.payload_type):7, + (P#rtp_packet.seqno):16, + (P#rtp_packet.timestamp):32, + (P#rtp_packet.ssrc):32, + (P#rtp_packet.payload)/bytes + >>. + +parse_rtp_packet( + << 2:2, % Version 2 + 0:1, % P (not supported yet) + 0:1, % X (not supported yet) + 0:4, % CC (not supported yet) + M:1, + PT:7, + SeqNo: 16, + TS:32, + Ssrc:32, + Payload/bytes >>) -> + #rtp_packet{ + version = 0, + marker = M, + payload_type = PT, + seqno = SeqNo, + timestamp = TS, + ssrc = Ssrc, + payload = Payload}. + +%%% payload generation %%% + +next_payload(F) when is_function(F) -> + {F(), F}; +next_payload({F, D}) when is_function(F) -> + {P, D2} = F(D), + {P, {F, D2}}; +next_payload([P | R]) -> + {P, R}; +next_payload([]) -> + undef; +next_payload(Bin = <<_/bytes>>) -> + {Bin, Bin}. + +%%% real writing work %%% + +write_packets(_Dev, DS, _P, _F, 0, _O, _Opts) -> + DS; +write_packets(Dev, DataSource, P = #rtp_packet{}, F, L, O, Opts) -> + Format = proplists:get_value(format, Opts, state), + Ptime = proplists:get_value(duration, Opts, 160), + Delay = proplists:get_value(delay, Opts, 0), + Rate = proplists:get_value(rate, Opts, 8000), + case next_payload(DataSource) of + {Payload, DataSource2} -> + write_packet(Dev, Ptime * F / Rate + Delay, + P#rtp_packet{seqno = F, timestamp = F*Ptime+O, + payload = Payload}, + Format), + write_packets(Dev, DataSource2, P, F+1, L-1, O, Opts); + Other -> Other + end. + +write_packet(Dev, Time, P = #rtp_packet{}, Format) -> + Bin = make_rtp_packet(P), + + write_packet_line(Dev, Time, P, Bin, Format). + +write_packet_pre(Dev, carray) -> + io:format(Dev, + "struct {float t; int len; char *data;} packets[] = {~n", []); + +write_packet_pre(_Dev, _) -> ok. + +write_packet_post(Dev, carray) -> + io:format(Dev, "};~n", []); + +write_packet_post(_Dev, _) -> ok. + +write_packet_line(Dev, Time, _P, Bin, state) -> + io:format(Dev, "~f ~s~n", [Time, bin_to_hex(Bin)]); + +write_packet_line(Dev, Time, #rtp_packet{seqno = N, timestamp = TS}, Bin, c) -> + ByteList = [ [ $0, $x | integer_to_list(Byte, 16) ] || <<Byte:8>> <= Bin ], + ByteStr = string:join(ByteList, ", "), + io:format(Dev, "/* time=~f, SeqNo=~B, TS=~B */ {~s}~n", [Time, N, TS, ByteStr]); + +write_packet_line(Dev, Time, #rtp_packet{seqno = N, timestamp = TS}, Bin, carray) -> + io:format(Dev, " /* RTP: SeqNo=~B, TS=~B */~n", [N, TS]), + io:format(Dev, " {~f, ~B, \"", [Time, size(Bin)]), + [ io:format(Dev, "\\x~2.16.0B", [Byte]) || <<Byte:8>> <= Bin ], + io:format(Dev, "\"},~n", []). + +%%% real reading work %%% + +read_packets(Dev, Opts) -> + Format = proplists:get_value(in_format, Opts, state), + + read_packets(Dev, Opts, Format). + +read_packets(Dev, Opts, Format) -> + case read_packet(Dev, Opts, Format) of + eof -> []; + Tuple -> [Tuple | read_packets(Dev, Opts, Format)] + end. + +read_packet(Dev, Opts, bin) -> + Size = proplists:get_value(frame_size, Opts), + case file:read(Dev, Size) of + {ok, Data} -> {0, #rtp_packet{payload = iolist_to_binary(Data)}}; + eof -> eof + end; +read_packet(Dev, _Opts, Format) -> + case read_packet_line(Dev, Format) of + {Time, Bin} -> {Time, parse_rtp_packet(Bin)}; + eof -> eof + end. + +read_packet_line(Dev, state) -> + case io:fread(Dev, "", "~f ~s") of + {ok, [Time, Hex]} -> {Time, hex_to_bin(Hex)}; + eof -> eof + end. diff --git a/contrib/rtp/rtp_replay.st b/contrib/rtp/rtp_replay.st new file mode 100644 index 000000000..e26d07388 --- /dev/null +++ b/contrib/rtp/rtp_replay.st @@ -0,0 +1,21 @@ +" +Simple UDP replay from the state files +" + +PackageLoader fileInPackage: #Sockets. +FileStream fileIn: 'rtp_replay_shared.st'. + + +Eval [ + | replay file host dport | + + file := Smalltalk arguments at: 1 ifAbsent: [ 'rtpstream.state' ]. + host := Smalltalk arguments at: 2 ifAbsent: [ '127.0.0.1' ]. + dport := (Smalltalk arguments at: 3 ifAbsent: [ '4000' ]) asInteger. + sport := (Smalltalk arguments at: 4 ifAbsent: [ '0' ]) asInteger. + + replay := RTPReplay on: file fromPort: sport. + + Transcript nextPutAll: 'Going to stream now'; nl. + replay streamAudio: host port: dport. +] diff --git a/contrib/rtp/rtp_replay_shared.st b/contrib/rtp/rtp_replay_shared.st new file mode 100644 index 000000000..7b68c0f5e --- /dev/null +++ b/contrib/rtp/rtp_replay_shared.st @@ -0,0 +1,118 @@ +" +Simple UDP replay from the state files +" + +PackageLoader fileInPackage: #Sockets. + +Object subclass: SDPUtils [ + "Look into using PetitParser." + SDPUtils class >> findPort: aSDP [ + aSDP linesDo: [:line | + (line startsWith: 'm=audio ') ifTrue: [ + | stream | + stream := line readStream + skip: 'm=audio ' size; + yourself. + ^ Number readFrom: stream. + ] + ]. + + ^ self error: 'Not found'. + ] + + SDPUtils class >> findHost: aSDP [ + aSDP linesDo: [:line | + (line startsWith: 'c=IN IP4 ') ifTrue: [ + | stream | + ^ stream := line readStream + skip: 'c=IN IP4 ' size; + upToEnd. + ] + ]. + + ^ self error: 'Not found'. + ] +] + +Object subclass: RTPReplay [ + | filename socket | + RTPReplay class >> on: aFile [ + ^ self new + initialize; + file: aFile; yourself + ] + + RTPReplay class >> on: aFile fromPort: aPort [ + ^ self new + initialize: aPort; + file: aFile; yourself + ] + + initialize [ + self initialize: 0. + ] + + initialize: aPort [ + socket := Sockets.DatagramSocket local: '0.0.0.0' port: aPort. + ] + + file: aFile [ + filename := aFile + ] + + localPort [ + ^ socket port + ] + + streamAudio: aHost port: aPort [ + | file last_time last_image udp_send dest | + + last_time := nil. + last_image := nil. + file := FileStream open: filename mode: #read. + + "Send the payload" + dest := Sockets.SocketAddress byName: aHost. + udp_send := [:payload | | datagram | + datagram := Sockets.Datagram data: payload contents address: dest port: aPort. + socket nextPut: datagram + ]. + + [file atEnd] whileFalse: [ + | lineStream time data now_image | + lineStream := file nextLine readStream. + + "Read the time, skip the blank, parse the data" + time := Number readFrom: lineStream. + lineStream skip: 1. + + data := WriteStream on: (ByteArray new: 30). + [lineStream atEnd] whileFalse: [ + | hex | + hex := lineStream next: 2. + data nextPut: (Number readFrom: hex readStream radix: 16). + ]. + + last_time isNil + ifTrue: [ + "First time, send it right now" + last_time := time. + last_image := Time millisecondClockValue. + udp_send value: data. + ] + ifFalse: [ + | wait_image new_image_time | + + "How long to wait?" + wait_image := last_image + ((time - last_time) * 1000). + [ wait_image > Time millisecondClockValue ] + whileTrue: [Processor yield]. + + udp_send value: data. + last_time := time. + last_image := wait_image. + ] + ] + ] +] + diff --git a/contrib/rtp/rtp_replay_sip.st b/contrib/rtp/rtp_replay_sip.st new file mode 100644 index 000000000..5f844df1d --- /dev/null +++ b/contrib/rtp/rtp_replay_sip.st @@ -0,0 +1,87 @@ +""" +Create a SIP connection and then stream... +""" + +PackageLoader + fileInPackage: #OsmoSIP. + +"Load for the replay code" +FileStream fileIn: 'rtp_replay_shared.st'. + + +Osmo.SIPCall subclass: StreamCall [ + | sem stream | + + createCall: aSDP [ + | sdp | + stream := RTPReplay on: 'rtp_ssrc6976010.240.240.1_to_10.240.240.50.state'. + sdp := aSDP % {stream localPort}. + ^ super createCall: sdp. + ] + + sem: aSemaphore [ + sem := aSemaphore + ] + + sessionNew [ + | host port | + Transcript nextPutAll: 'The call has started'; nl. + Transcript nextPutAll: sdp_result; nl. + + host := SDPUtils findHost: sdp_result. + port := SDPUtils findPort: sdp_result. + + [ + stream streamAudio: host port: port. + Transcript nextPutAll: 'Streaming has finished.'; nl. + ] fork. + ] + + sessionFailed [ + sem signal + ] + + sessionEnd [ + sem signal + ] +] + +Eval [ + | transport agent call sem sdp_fr sdp_amr | + + + sdp_fr := (WriteStream on: String new) + nextPutAll: 'v=0'; cr; nl; + nextPutAll: 'o=twinkle 1739517580 1043400482 IN IP4 127.0.0.1'; cr; nl; + nextPutAll: 's=-'; cr; nl; + nextPutAll: 'c=IN IP4 127.0.0.1'; cr; nl; + nextPutAll: 't=0 0'; cr; nl; + nextPutAll: 'm=audio %1 RTP/AVP 0 101'; cr; nl; + nextPutAll: 'a=rtpmap:0 PCMU/8000'; cr; nl; + nextPutAll: 'a=rtpmap:101 telephone-event/8000'; cr; nl; + nextPutAll: 'a=fmtp:101 0-15'; cr; nl; + nextPutAll: 'a=ptime:20'; cr; nl; + contents. + + sem := Semaphore new. + transport := Osmo.SIPUdpTransport + startOn: '0.0.0.0' port: 5066. + agent := Osmo.SIPUserAgent createOn: transport. + transport start. + + call := (StreamCall + fromUser: 'sip:1000@sip.zecke.osmocom.org' + host: '127.0.0.1' + port: 5060 + to: 'sip:123456@127.0.0.1' + on: agent) + sem: sem; yourself. + + call createCall: sdp_fr. + + + "Wait for the stream to have ended" + sem wait. + + (Delay forSeconds: 4) wait. +] diff --git a/contrib/rtp/timestamp_rtp.lua b/contrib/rtp/timestamp_rtp.lua new file mode 100644 index 000000000..c18a06bed --- /dev/null +++ b/contrib/rtp/timestamp_rtp.lua @@ -0,0 +1,28 @@ +print("Ni hao") + + +do + local tap = Listener.new("ip", "rtp") + local rtp_ssrc = Field.new("rtp.ssrc") + local frame_time = Field.new("frame.time_relative") + local rtp = Field.new("rtp") + + function tap.packet(pinfo, tvb, ip) + local ip_src, ip_dst = tostring(ip.ip_src), tostring(ip.ip_dst) + local rtp_data = rtp() + local filename = "rtp_ssrc" .. rtp_ssrc() "_src_" .. ip_src .. "_to_" .. ip_dst .. ".state" + local f = io.open(filename, "a") + + f:write(tostring(frame_time()) .. " ") + f:write(tostring(rtp_data.value)) + f:write("\n") + f:close() + end + + function tap.draw() + print("DRAW") + end + function tap.reset() + print("RESET") + end +end diff --git a/contrib/sms/fill-hlr.st b/contrib/sms/fill-hlr.st new file mode 100644 index 000000000..da0643ecf --- /dev/null +++ b/contrib/sms/fill-hlr.st @@ -0,0 +1,66 @@ +"I create output for some simple SQL statements for the HLR db" + + +Eval [ + +"Create tables if they don't exist" +Transcript show: 'CREATE TABLE SMS ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TIMESTAMP NOT NULL, + sent TIMESTAMP, + sender_id INTEGER NOT NULL, + receiver_id INTEGER NOT NULL, + deliver_attempts INTEGER NOT NULL DEFAULT 0, + valid_until TIMESTAMP, + reply_path_req INTEGER NOT NULL, + status_rep_req INTEGER NOT NULL, + protocol_id INTEGER NOT NULL, + data_coding_scheme INTEGER NOT NULL, + ud_hdr_ind INTEGER NOT NULL, + dest_addr TEXT, + user_data BLOB, + header BLOB, + text TEXT);'; nl; + show: 'CREATE TABLE Subscriber ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL, + imsi NUMERIC UNIQUE NOT NULL, + name TEXT, + extension TEXT UNIQUE, + authorized INTEGER NOT NULL DEFAULT 0, + tmsi TEXT UNIQUE, + lac INTEGER NOT NULL DEFAULT 0);'; nl. + +"Create some dummy subscribers" +num_sub := 1000. +num_sms := 30. +lac := 1. + +Transcript show: 'BEGIN;'; nl. + +1 to: num_sub do: [:each | + Transcript show: 'INSERT INTO Subscriber + (imsi, created, updated, authorized, lac, extension) + VALUES + (%1, datetime(''now''), datetime(''now''), 1, %2, %3);' % + {(274090000000000 + each). lac. each}; nl. +]. + +1 to: num_sms do: [:sms | + 1 to: num_sub do: [:sub | + Transcript show: 'INSERT INTO SMS + (created, sender_id, receiver_id, valid_until, + reply_path_req, status_rep_req, protocol_id, + data_coding_scheme, ud_hdr_ind, dest_addr, + text) VALUES + (datetime(''now''), 1, %1, ''2222-2-2'', + 0, 0, 0, + 0, 0, ''123456'', + ''abc'');' % {sub}; nl. + ] +]. + +Transcript show: 'COMMIT;'; nl. + +] diff --git a/contrib/sms/hlr-query.st b/contrib/sms/hlr-query.st new file mode 100644 index 000000000..bd3f97a4a --- /dev/null +++ b/contrib/sms/hlr-query.st @@ -0,0 +1,10 @@ +"Query for one SMS" + +Eval [ +1 to: 100 do: [:each | + Transcript show: 'SELECT SMS.* FROM SMS + JOIN Subscriber ON SMS.receiver_id = Subscriber.id + WHERE SMS.id >= 1 AND SMS.sent IS NULL AND Subscriber.lac > 0 + ORDER BY SMS.id LIMIT 1;'; nl. +]. +] diff --git a/contrib/sms/sqlite-probe.tap.d b/contrib/sms/sqlite-probe.tap.d new file mode 100644 index 000000000..e75cdfcfa --- /dev/null +++ b/contrib/sms/sqlite-probe.tap.d @@ -0,0 +1,5 @@ +probe process("/usr/lib/libsqlite3.so.0.8.6").function("sqlite3_get_table") +{ + a = user_string($zSql); + printf("sqlite3_get_table called '%s'\n", a); +} diff --git a/contrib/soap.py b/contrib/soap.py new file mode 100755 index 000000000..4d0a023f9 --- /dev/null +++ b/contrib/soap.py @@ -0,0 +1,188 @@ +#!/usr/bin/python3 +# -*- mode: python-mode; py-indent-tabs-mode: nil -*- +""" +/* + * Copyright (C) 2016 sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +""" + +__version__ = "v0.7" # bump this on every non-trivial change + +from twisted.internet import defer, reactor +from twisted_ipa import CTRL, IPAFactory, __version__ as twisted_ipa_version +from ipa import Ctrl +from treq import post, collect +from suds.client import Client +from functools import partial +from distutils.version import StrictVersion as V # FIXME: use NormalizedVersion from PEP-386 when available +import argparse, datetime, signal, sys, os, logging, logging.handlers + +# we don't support older versions of TwistedIPA module +assert V(twisted_ipa_version) > V('0.4') + +# keys from OpenBSC openbsc/src/libbsc/bsc_rf_ctrl.c, values SOAP-specific +oper = { 'inoperational' : 0, 'operational' : 1 } +admin = { 'locked' : 0, 'unlocked' : 1 } +policy = { 'off' : 0, 'on' : 1, 'grace' : 2, 'unknown' : 3 } + +# keys from OpenBSC openbsc/src/libbsc/bsc_vty.c +fix = { 'invalid' : 0, 'fix2d' : 1, 'fix3d' : 1 } # SOAP server treats it as boolean but expects int + + +def handle_reply(p, f, log, r): + """ + Reply handler: takes function p to process raw SOAP server reply r, function f to run for each command and verbosity flag v + """ + repl = p(r) # result is expected to have both commands[] array and error string (could be None) + bsc_id = repl.commands[0].split()[0].split('.')[3] # we expect 1st command to have net.0.bsc.666.bts.2.trx.1 location prefix format + log.info("Received SOAP response for BSC %s with %d commands, error status: %s" % (bsc_id, len(repl.commands), repl.error)) + log.debug("BSC %s commands: %s" % (bsc_id, repl.commands)) + for t in repl.commands: # Process OpenBscCommands format from .wsdl + (_, m) = Ctrl().cmd(*t.split()) + f(m) + + +class Trap(CTRL): + """ + TRAP handler (agnostic to factory's client object) + """ + def ctrl_TRAP(self, data, op_id, v): + """ + Parse CTRL TRAP and dispatch to appropriate handler after normalization + """ + (l, r) = v.split() + loc = l.split('.') + t_type = loc[-1] + p = partial(lambda a, i: a[i] if len(a) > i else None, loc) # parse helper + method = getattr(self, 'handle_' + t_type.replace('-', ''), lambda: "Unhandled %s trap" % t_type) + method(p(1), p(3), p(5), p(7), r) # we expect net.0.bsc.666.bts.2.trx.1 format for trap prefix + + def ctrl_SET_REPLY(self, data, _, v): + """ + Debug log for replies to our commands + """ + self.factory.log.debug('SET REPLY %s' % v) + + def ctrl_ERROR(self, data, op_id, v): + """ + We want to know if smth went wrong + """ + self.factory.log.debug('CTRL ERROR [%s] %s' % (op_id, v)) + + def connectionMade(self): + """ + Logging wrapper, calling super() is necessary not to break reconnection logic + """ + self.factory.log.info("Connected to CTRL@%s:%d" % (self.factory.host, self.factory.port)) + super(CTRL, self).connectionMade() + + @defer.inlineCallbacks + def handle_locationstate(self, net, bsc, bts, trx, data): + """ + Handle location-state TRAP: parse trap content, build SOAP context and use treq's routines to post it while setting up async handlers + """ + (ts, fx, lat, lon, height, opr, adm, pol, mcc, mnc) = data.split(',') + tstamp = datetime.datetime.fromtimestamp(float(ts)).isoformat() + self.factory.log.debug('location-state@%s.%s.%s.%s (%s) [%s/%s] => %s' % (net, bsc, bts, trx, tstamp, mcc, mnc, data)) + ctx = self.factory.client.registerSiteLocation(bsc, float(lon), float(lat), fix.get(fx, 0), tstamp, oper.get(opr, 2), admin.get(adm, 2), policy.get(pol, 3)) + d = post(self.factory.location, ctx.envelope) + d.addCallback(collect, partial(handle_reply, ctx.process_reply, self.transport.write, self.factory.log)) # treq's collect helper is handy to get all reply content at once using closure on ctx + d.addErrback(lambda e, bsc: self.factory.log.critical("HTTP POST error %s while trying to register BSC %s" % (e, bsc)), bsc) # handle HTTP errors + # Ensure that we run only limited number of requests in parallel: + yield self.factory.semaphore.acquire() + yield d # we end up here only if semaphore is available which means it's ok to fire the request without exceeding the limit + self.factory.semaphore.release() + + def handle_notificationrejectionv1(self, net, bsc, bts, trx, data): + """ + Handle notification-rejection-v1 TRAP: just an example to show how more message types can be handled + """ + self.factory.log.debug('notification-rejection-v1@bsc-id %s => %s' % (bsc, data)) + + +class TrapFactory(IPAFactory): + """ + Store SOAP client object so TRAP handler can use it for requests + """ + location = None + log = None + semaphore = None + client = None + host = None + port = None + def __init__(self, host, port, proto, semaphore, log, wsdl=None, location=None): + self.host = host # for logging only, + self.port = port # seems to be no way to get it from ReconnectingClientFactory + self.log = log + self.semaphore = semaphore + soap = Client(wsdl, location=location, nosend=True) # make async SOAP client + self.location = location.encode() if location else soap.wsdl.services[0].ports[0].location # necessary for dispatching HTTP POST via treq + self.client = soap.service + level = self.log.getEffectiveLevel() + self.log.setLevel(logging.WARNING) # we do not need excessive debug from lower levels + super(TrapFactory, self).__init__(proto, self.log) + self.log.setLevel(level) + self.log.debug("Using IPA %s, SUDS client: %s" % (Ctrl.version, soap)) + + +def reloader(path, script, log, dbg1, dbg2, signum, _): + """ + Signal handler: we have to use execl() because twisted's reactor is not restartable due to some bug in twisted implementation + """ + log.info("Received Signal %d - restarting..." % signum) + if signum == signal.SIGUSR1 and dbg1 not in sys.argv and dbg2 not in sys.argv: + sys.argv.append(dbg1) # enforce debug + if signum == signal.SIGUSR2 and (dbg1 in sys.argv or dbg2 in sys.argv): # disable debug + if dbg1 in sys.argv: + sys.argv.remove(dbg1) + if dbg2 in sys.argv: + sys.argv.remove(dbg2) + os.execl(path, script, *sys.argv[1:]) + + +if __name__ == '__main__': + p = argparse.ArgumentParser(description='Proxy between given SOAP service and Osmocom CTRL protocol.') + p.add_argument('-v', '--version', action='version', version=("%(prog)s " + __version__)) + p.add_argument('-p', '--port', type=int, default=4250, help="Port to use for CTRL interface, defaults to 4250") + p.add_argument('-c', '--ctrl', default='localhost', help="Adress to use for CTRL interface, defaults to localhost") + p.add_argument('-w', '--wsdl', required=True, help="WSDL URL for SOAP") + p.add_argument('-n', '--num', type=int, default=5, help="Max number of concurrent HTTP requests to SOAP server") + p.add_argument('-d', '--debug', action='store_true', help="Enable debug log") + p.add_argument('-o', '--output', action='store_true', help="Log to STDOUT in addition to SYSLOG") + p.add_argument('-l', '--location', help="Override location found in WSDL file (don't use unless you know what you're doing)") + args = p.parse_args() + + log = logging.getLogger('CTRL2SOAP') + if args.debug: + log.setLevel(logging.DEBUG) + else: + log.setLevel(logging.INFO) + log.addHandler(logging.handlers.SysLogHandler('/dev/log')) + if args.output: + log.addHandler(logging.StreamHandler(sys.stdout)) + + reboot = partial(reloader, os.path.abspath(__file__), os.path.basename(__file__), log, '-d', '--debug') # keep in sync with add_argument() call above + signal.signal(signal.SIGHUP, reboot) + signal.signal(signal.SIGQUIT, reboot) + signal.signal(signal.SIGUSR1, reboot) # restart and enabled debug output + signal.signal(signal.SIGUSR2, reboot) # restart and disable debug output + + log.info("SOAP proxy %s starting with PID %d ..." % (__version__, os.getpid())) + reactor.connectTCP(args.ctrl, args.port, TrapFactory(args.ctrl, args.port, Trap, defer.DeferredSemaphore(args.num), log, args.wsdl, args.location)) + reactor.run() diff --git a/contrib/systemd/osmo-bsc-mgcp.service b/contrib/systemd/osmo-bsc-mgcp.service new file mode 100644 index 000000000..c040e6078 --- /dev/null +++ b/contrib/systemd/osmo-bsc-mgcp.service @@ -0,0 +1,11 @@ +[Unit] +Description=OpenBSC MGCP + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/osmo-bsc_mgcp -s -c /etc/osmocom/osmo-bsc-mgcp.cfg +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/contrib/systemd/osmo-bsc.service b/contrib/systemd/osmo-bsc.service new file mode 100644 index 000000000..4047fef48 --- /dev/null +++ b/contrib/systemd/osmo-bsc.service @@ -0,0 +1,12 @@ +[Unit] +Description=OpenBSC BSC +Wants=osmo-bsc-mgcp.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/osmo-bsc -c /etc/osmocom/osmo-bsc.cfg -s +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/contrib/systemd/osmo-gbproxy.service b/contrib/systemd/osmo-gbproxy.service new file mode 100644 index 000000000..a0b7829db --- /dev/null +++ b/contrib/systemd/osmo-gbproxy.service @@ -0,0 +1,12 @@ +[Unit] +Description=Osmocom Gb proxy + +[Service] +Type=simple +ExecStart=/usr/bin/osmo-gbproxy -c /etc/osmocom/osmo-gbproxy.cfg +Restart=always +RestartSec=2 +RestartPreventExitStatus=1 + +[Install] +WantedBy=multi-user.target diff --git a/contrib/systemd/osmo-nitb.service b/contrib/systemd/osmo-nitb.service new file mode 100644 index 000000000..377497ee5 --- /dev/null +++ b/contrib/systemd/osmo-nitb.service @@ -0,0 +1,11 @@ +[Unit] +Description=OpenBSC Network In the Box (NITB) + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/osmo-nitb -s -C -c /etc/osmocom/osmo-nitb.cfg -l /var/lib/osmocom/hlr.sqlite3 +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/contrib/systemd/osmo-sgsn.service b/contrib/systemd/osmo-sgsn.service new file mode 100644 index 000000000..674d78656 --- /dev/null +++ b/contrib/systemd/osmo-sgsn.service @@ -0,0 +1,11 @@ +[Unit] +Description=OpenBSC SGSN + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/osmo-sgsn -c /etc/osmocom/osmo-sgsn.cfg +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/contrib/testconv/Makefile b/contrib/testconv/Makefile new file mode 100644 index 000000000..bb856f750 --- /dev/null +++ b/contrib/testconv/Makefile @@ -0,0 +1,16 @@ + +OBJS = testconv_main.o + +CC = gcc +CFLAGS = -O0 -ggdb -Wall +LDFLAGS = +CPPFLAGS = -I../.. -I../../include $(shell pkg-config --cflags libosmocore) $(shell pkg-config --cflags libbcg729) +LIBS = ../../src/libmgcp/libmgcp.a ../../src/libcommon/libcommon.a $(shell pkg-config --libs libosmocore) $(shell pkg-config --libs libbcg729) -lgsm -lrt + +testconv: $(OBJS) + $(CC) -o $@ $^ $(LDFLAGS) $(LIBS) + +testconv_main.o: testconv_main.c + +$(OBJS): + $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< diff --git a/contrib/testconv/testconv_main.c b/contrib/testconv/testconv_main.c new file mode 100644 index 000000000..6c95c5542 --- /dev/null +++ b/contrib/testconv/testconv_main.c @@ -0,0 +1,133 @@ +#include <stdlib.h> +#include <unistd.h> +#include <stdio.h> +#include <string.h> +#include <err.h> + +#include <osmocom/core/talloc.h> +#include <osmocom/core/application.h> + +#include <openbsc/debug.h> +#include <openbsc/gsm_data.h> +#include <openbsc/mgcp.h> +#include <openbsc/mgcp_internal.h> + +#include "bscconfig.h" +#ifndef BUILD_MGCP_TRANSCODING +#error "Requires MGCP transcoding enabled (see --enable-mgcp-transcoding)" +#endif + +#include "openbsc/mgcp_transcode.h" + +static int audio_name_to_type(const char *name) +{ + if (!strcasecmp(name, "gsm")) + return 3; +#ifdef HAVE_BCG729 + else if (!strcasecmp(name, "g729")) + return 18; +#endif + else if (!strcasecmp(name, "pcma")) + return 8; + else if (!strcasecmp(name, "l16")) + return 11; + return -1; +} + +int mgcp_get_trans_frame_size(void *state_, int nsamples, int dst); + +int main(int argc, char **argv) +{ + char buf[4096] = {0x80, 0}; + int cc, rc; + struct mgcp_rtp_end *dst_end; + struct mgcp_rtp_end *src_end; + struct mgcp_trunk_config tcfg = {{0}}; + struct mgcp_endpoint endp = {0}; + struct mgcp_process_rtp_state *state; + int in_size; + int in_samples = 160; + int out_samples = 0; + uint32_t ts = 0; + uint16_t seq = 0; + + osmo_init_logging(&log_info); + + tcfg.endpoints = &endp; + tcfg.number_endpoints = 1; + endp.tcfg = &tcfg; + mgcp_initialize_endp(&endp); + + dst_end = &endp.bts_end; + src_end = &endp.net_end; + + if (argc <= 2) + errx(1, "Usage: {gsm|g729|pcma|l16} {gsm|g729|pcma|l16} [SPP]"); + + if ((src_end->codec.payload_type = audio_name_to_type(argv[1])) == -1) + errx(1, "invalid input format '%s'", argv[1]); + if ((dst_end->codec.payload_type = audio_name_to_type(argv[2])) == -1) + errx(1, "invalid output format '%s'", argv[2]); + if (argc > 3) + out_samples = atoi(argv[3]); + + if (out_samples) { + dst_end->codec.frame_duration_den = dst_end->codec.rate; + dst_end->codec.frame_duration_num = out_samples; + dst_end->frames_per_packet = 1; + } + + rc = mgcp_transcoding_setup(&endp, dst_end, src_end); + if (rc < 0) + errx(1, "setup failed: %s", strerror(-rc)); + + state = dst_end->rtp_process_data; + OSMO_ASSERT(state != NULL); + + in_size = mgcp_transcoding_get_frame_size(state, in_samples, 0); + OSMO_ASSERT(sizeof(buf) >= in_size + 12); + + buf[1] = src_end->codec.payload_type; + *(uint16_t*)(buf+2) = htons(1); + *(uint32_t*)(buf+4) = htonl(0); + *(uint32_t*)(buf+8) = htonl(0xaabbccdd); + + while ((cc = read(0, buf + 12, in_size))) { + int cont; + int len; + + if (cc != in_size) + err(1, "read"); + + *(uint16_t*)(buf+2) = htonl(seq); + *(uint32_t*)(buf+4) = htonl(ts); + + seq += 1; + ts += in_samples; + + cc += 12; /* include RTP header */ + + len = cc; + + do { + cont = mgcp_transcoding_process_rtp(&endp, dst_end, + buf, &len, sizeof(buf)); + if (cont == -EAGAIN) { + fprintf(stderr, "Got EAGAIN\n"); + break; + } + + if (cont < 0) + errx(1, "processing failed: %s", strerror(-cont)); + + len -= 12; /* ignore RTP header */ + + if (write(1, buf + 12, len) != len) + err(1, "write"); + + len = cont; + } while (len > 0); + } + return 0; +} + diff --git a/contrib/twisted_ipa.py b/contrib/twisted_ipa.py new file mode 100755 index 000000000..e6d7b1a16 --- /dev/null +++ b/contrib/twisted_ipa.py @@ -0,0 +1,384 @@ +#!/usr/bin/python3 +# -*- mode: python-mode; py-indent-tabs-mode: nil -*- +""" +/* + * Copyright (C) 2016 sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +""" + +__version__ = "0.6" # bump this on every non-trivial change + +from ipa import Ctrl, IPA +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet import reactor +from twisted.protocols import basic +import argparse, logging + +class IPACommon(basic.Int16StringReceiver): + """ + Generic IPA protocol handler: include some routines for simpler subprotocols. + It's not intended as full implementation of all subprotocols, rather common ground and example code. + """ + def dbg(self, line): + """ + Debug print helper + """ + self.factory.log.debug(line) + + def osmo_CTRL(self, data): + """ + OSMO CTRL protocol + Placeholder, see corresponding derived class + """ + pass + + def osmo_MGCP(self, data): + """ + OSMO MGCP extension + """ + self.dbg('OSMO MGCP received %s' % data) + + def osmo_LAC(self, data): + """ + OSMO LAC extension + """ + self.dbg('OSMO LAC received %s' % data) + + def osmo_SMSC(self, data): + """ + OSMO SMSC extension + """ + self.dbg('OSMO SMSC received %s' % data) + + def osmo_ORC(self, data): + """ + OSMO ORC extension + """ + self.dbg('OSMO ORC received %s' % data) + + def osmo_GSUP(self, data): + """ + OSMO GSUP extension + """ + self.dbg('OSMO GSUP received %s' % data) + + def osmo_OAP(self, data): + """ + OSMO OAP extension + """ + self.dbg('OSMO OAP received %s' % data) + + def osmo_UNKNOWN(self, data): + """ + OSMO defaul extension handler + """ + self.dbg('OSMO unknown extension received %s' % data) + + def handle_RSL(self, data, proto, extension): + """ + RSL protocol handler + """ + self.dbg('IPA RSL received message with extension %s' % extension) + + def handle_CCM(self, data, proto, msgt): + """ + CCM (IPA Connection Management) + Placeholder, see corresponding derived class + """ + pass + + def handle_SCCP(self, data, proto, extension): + """ + SCCP protocol handler + """ + self.dbg('IPA SCCP received message with extension %s' % extension) + + def handle_OML(self, data, proto, extension): + """ + OML protocol handler + """ + self.dbg('IPA OML received message with extension %s' % extension) + + def handle_OSMO(self, data, proto, extension): + """ + Dispatcher point for OSMO subprotocols based on extension name, lambda default should never happen + """ + method = getattr(self, 'osmo_' + IPA().ext(extension), lambda: "extension dispatch failure") + method(data) + + def handle_MGCP(self, data, proto, extension): + """ + MGCP protocol handler + """ + self.dbg('IPA MGCP received message with attribute %s' % extension) + + def handle_UNKNOWN(self, data, proto, extension): + """ + Default protocol handler + """ + self.dbg('IPA received message for %s (%s) protocol with attribute %s' % (IPA().proto(proto), proto, extension)) + + def process_chunk(self, data): + """ + Generic message dispatcher for IPA (sub)protocols based on protocol name, lambda default should never happen + """ + (_, proto, extension, content) = IPA().del_header(data) + if content is not None: + self.dbg('IPA received %s::%s [%d/%d] %s' % (IPA().proto(proto), IPA().ext_name(proto, extension), len(data), len(content), content)) + method = getattr(self, 'handle_' + IPA().proto(proto), lambda: "protocol dispatch failure") + method(content, proto, extension) + + def dataReceived(self, data): + """ + Override for dataReceived from Int16StringReceiver because of inherently incompatible interpretation of length + If default handler is used than we would always get off-by-1 error (Int16StringReceiver use equivalent of l + 2) + """ + if len(data): + (head, tail) = IPA().split_combined(data) + self.process_chunk(head) + self.dataReceived(tail) + + def connectionMade(self): + """ + We have to resetDelay() here to drop internal state to default values to make reconnection logic work + Make sure to call this via super() if overriding to keep reconnection logic intact + """ + addr = self.transport.getPeer() + self.dbg('IPA connected to %s:%d peer' % (addr.host, addr.port)) + self.factory.resetDelay() + + +class CCM(IPACommon): + """ + Implementation of CCM protocol for IPA multiplex + """ + def ack(self): + self.transport.write(IPA().id_ack()) + + def ping(self): + self.transport.write(IPA().ping()) + + def pong(self): + self.transport.write(IPA().pong()) + + def handle_CCM(self, data, proto, msgt): + """ + CCM (IPA Connection Management) + Only basic logic necessary for tests is implemented (ping-pong, id ack etc) + """ + if msgt == IPA.MSGT['ID_GET']: + self.transport.getHandle().sendall(IPA().id_resp(self.factory.ccm_id)) + # if we call + # self.transport.write(IPA().id_resp(self.factory.test_id)) + # instead, than we would have to also call + # reactor.callLater(1, self.ack) + # instead of self.ack() + # otherwise the writes will be glued together - hence the necessity for ugly hack with 1s timeout + # Note: this still might work depending on the IPA implementation details on the other side + self.ack() + # schedule PING in 4s + reactor.callLater(4, self.ping) + if msgt == IPA.MSGT['PING']: + self.pong() + + +class CTRL(IPACommon): + """ + Implementation of Osmocom control protocol for IPA multiplex + """ + def ctrl_SET(self, data, op_id, v): + """ + Handle CTRL SET command + """ + self.dbg('CTRL SET [%s] %s' % (op_id, v)) + + def ctrl_SET_REPLY(self, data, op_id, v): + """ + Handle CTRL SET reply + """ + self.dbg('CTRL SET REPLY [%s] %s' % (op_id, v)) + + def ctrl_GET(self, data, op_id, v): + """ + Handle CTRL GET command + """ + self.dbg('CTRL GET [%s] %s' % (op_id, v)) + + def ctrl_GET_REPLY(self, data, op_id, v): + """ + Handle CTRL GET reply + """ + self.dbg('CTRL GET REPLY [%s] %s' % (op_id, v)) + + def ctrl_TRAP(self, data, op_id, v): + """ + Handle CTRL TRAP command + """ + self.dbg('CTRL TRAP [%s] %s' % (op_id, v)) + + def ctrl_ERROR(self, data, op_id, v): + """ + Handle CTRL ERROR reply + """ + self.dbg('CTRL ERROR [%s] %s' % (op_id, v)) + + def osmo_CTRL(self, data): + """ + OSMO CTRL message dispatcher, lambda default should never happen + For basic tests only, appropriate handling routines should be replaced: see CtrlServer for example + """ + self.dbg('OSMO CTRL received %s::%s' % Ctrl().parse(data.decode('utf-8'))) + (cmd, op_id, v) = data.decode('utf-8').split(' ', 2) + method = getattr(self, 'ctrl_' + cmd, lambda: "CTRL unknown command") + method(data, op_id, v) + + +class IPAServer(CCM): + """ + Test implementation of IPA server + Demonstrate CCM opearation by overriding necessary bits from CCM + """ + def connectionMade(self): + """ + Keep reconnection logic working by calling routine from CCM + Initiate CCM upon connection + """ + addr = self.transport.getPeer() + self.factory.log.info('IPA server: connection from %s:%d client' % (addr.host, addr.port)) + super(IPAServer, self).connectionMade() + self.transport.write(IPA().id_get()) + + +class CtrlServer(CTRL): + """ + Test implementation of CTRL server + Demonstarte CTRL handling by overriding simpler routines from CTRL + """ + def connectionMade(self): + """ + Keep reconnection logic working by calling routine from CTRL + Send TRAP upon connection + Note: we can't use sendString() because of it's incompatibility with IPA interpretation of length prefix + """ + addr = self.transport.getPeer() + self.factory.log.info('CTRL server: connection from %s:%d client' % (addr.host, addr.port)) + super(CtrlServer, self).connectionMade() + self.transport.write(Ctrl().trap('LOL', 'what')) + self.transport.write(Ctrl().trap('rulez', 'XXX')) + + def reply(self, r): + self.transport.write(Ctrl().add_header(r)) + + def ctrl_SET(self, data, op_id, v): + """ + CTRL SET command: always succeed + """ + self.dbg('SET [%s] %s' % (op_id, v)) + self.reply('SET_REPLY %s %s' % (op_id, v)) + + def ctrl_GET(self, data, op_id, v): + """ + CTRL GET command: always fail + """ + self.dbg('GET [%s] %s' % (op_id, v)) + self.reply('ERROR %s No variable found' % op_id) + + +class IPAFactory(ReconnectingClientFactory): + """ + Generic IPA Client Factory which can be used to store state for various subprotocols and manage connections + Note: so far we do not really need separate Factory for acting as a server due to protocol simplicity + """ + protocol = IPACommon + log = None + ccm_id = IPA().identity(unit=b'1515/0/1', mac=b'b0:0b:fa:ce:de:ad:be:ef', utype=b'sysmoBTS', name=b'StingRay', location=b'hell', sw=IPA.version.encode('utf-8')) + + def __init__(self, proto=None, log=None, ccm_id=None): + if proto: + self.protocol = proto + if ccm_id: + self.ccm_id = ccm_id + if log: + self.log = log + else: + self.log = logging.getLogger('IPAFactory') + self.log.setLevel(logging.CRITICAL) + self.log.addHandler(logging.NullHandler) + + def clientConnectionFailed(self, connector, reason): + """ + Only necessary for as debugging aid - if we can somehow set parent's class noisy attribute then we can omit this method + """ + self.log.warning('IPAFactory connection failed: %s' % reason.getErrorMessage()) + ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) + + def clientConnectionLost(self, connector, reason): + """ + Only necessary for as debugging aid - if we can somehow set parent's class noisy attribute then we can omit this method + """ + self.log.warning('IPAFactory connection lost: %s' % reason.getErrorMessage()) + ReconnectingClientFactory.clientConnectionLost(self, connector, reason) + + +if __name__ == '__main__': + p = argparse.ArgumentParser("Twisted IPA (module v%s) app" % IPA.version) + p.add_argument('-v', '--version', action='version', version="%(prog)s v" + __version__) + p.add_argument('-p', '--port', type=int, default=4250, help="Port to use for CTRL interface") + p.add_argument('-d', '--host', default='localhost', help="Adress to use for CTRL interface") + cs = p.add_mutually_exclusive_group() + cs.add_argument("-c", "--client", action='store_true', help="asume client role") + cs.add_argument("-s", "--server", action='store_true', help="asume server role") + ic = p.add_mutually_exclusive_group() + ic.add_argument("--ipa", action='store_true', help="use IPA protocol") + ic.add_argument("--ctrl", action='store_true', help="use CTRL protocol") + args = p.parse_args() + test = False + + log = logging.getLogger('TwistedIPA') + log.setLevel(logging.DEBUG) + log.addHandler(logging.StreamHandler(sys.stdout)) + + if args.ctrl: + if args.client: + # Start osmo-bsc to receive TRAP messages when osmo-bts-* connects to it + print('CTRL client, connecting to %s:%d' % (args.host, args.port)) + reactor.connectTCP(args.host, args.port, IPAFactory(CTRL, log)) + test = True + if args.server: + # Use bsc_control.py to issue set/get commands + print('CTRL server, listening on port %d' % args.port) + reactor.listenTCP(args.port, IPAFactory(CtrlServer, log)) + test = True + if args.ipa: + if args.client: + # Start osmo-nitb which would initiate A-bis/IP session + print('IPA client, connecting to %s ports %d and %d' % (args.host, IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL)) + reactor.connectTCP(args.host, IPA.TCP_PORT_OML, IPAFactory(CCM, log)) + reactor.connectTCP(args.host, IPA.TCP_PORT_RSL, IPAFactory(CCM, log)) + test = True + if args.server: + # Start osmo-bts-* which would attempt to connect to us + print('IPA server, listening on ports %d and %d' % (IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL)) + reactor.listenTCP(IPA.TCP_PORT_RSL, IPAFactory(IPAServer, log)) + reactor.listenTCP(IPA.TCP_PORT_OML, IPAFactory(IPAServer, log)) + test = True + if test: + reactor.run() + else: + print("Please specify which protocol in which role you'd like to test.") |