aboutsummaryrefslogtreecommitdiffstats
path: root/openbsc/contrib/twisted_ipa.py
blob: e6d7b1a167bbf98b18e3f78971ffb6cdcf7d284c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
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.")