laforge has uploaded this change for review. (
https://gerrit.osmocom.org/c/pysim/+/37456?usp=email )
Change subject: pySim-smpp2sim.py: Simulate SMSC+CN+RAN+UE for OTA testing
......................................................................
pySim-smpp2sim.py: Simulate SMSC+CN+RAN+UE for OTA testing
The pySim-smpp2sim.py program exposes two interfaces:
* SMPP server-side port, so external programs can rx/tx SMS
* APDU interface towards the SIM card
It therefore emulates the SMSC, Core Network, RAND and UE parts
that would normally be encountered in an OTA setup.
Change-Id: Ie5bae9d823bca6f6c658bd455303f63bace2258c
---
M docs/index.rst
A docs/smpp2sim.rst
A pySim-smpp2sim.py
M requirements.txt
M setup.py
5 files changed, 375 insertions(+), 0 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/56/37456/1
diff --git a/docs/index.rst b/docs/index.rst
index bcbc7f0..b747b7c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -41,6 +41,7 @@
shell
trace
legacy
+ smpp2sim
library
osmo-smdpp
diff --git a/docs/smpp2sim.rst b/docs/smpp2sim.rst
new file mode 100644
index 0000000..963dc9f
--- /dev/null
+++ b/docs/smpp2sim.rst
@@ -0,0 +1,83 @@
+pySim-smpp2sim
+==============
+
+This is a program to emulate the entire communication path SMSC-CN-RAN-ME
+that is usually between an OTA backend and the SIM card. This allows
+to play with SIM OTA technology without using a mobile network or even
+a mobile phone.
+
+An external application can act as SMPP ESME and must encode (and
+encrypt/sign) the OTA SMS and submit them via SMPP to this program, just
+like it would submit it normally to a SMSC (SMS Service Centre). The
+program then re-formats the SMPP-SUBMIT into a SMS DELIVER TPDU and
+passes it via an ENVELOPE APDU to the SIM card that is locally inserted
+into a smart card reader.
+
+The path from SIM to external OTA application works the opposite way.
+
+The default SMPP system_id is `test`. Likewise, the default SMPP
+password is `test`
+
+Running pySim-smpp2sim
+----------------------
+
+The command accepts the same command line arguments for smart card interface device
selection as pySim-shell,
+as well as a few SMPP specific arguments:
+
+.. argparse::
+ :module: pySim-smpp2sim
+ :func: option_parser
+ :prog: pySim-smpp2sim.py
+
+
+Example execution with sample output
+------------------------------------
+
+So for a simple system with a single PC/SC device, you would typically use something like
+`./pySim-smpp2sim.py -p0` to start the program. You will see output like this at
start-up
+::
+
+ Using reader PCSC[HID Global OMNIKEY 3x21 Smart Card Reader [OMNIKEY 3x21 Smart Card
Reader] 00 00]
+ INFO root: Binding Virtual SMSC to TCP Port 2775 at ::
+ -> 00a4000402 2f00
+ <- 611c:
+ -> 00c000001c
+ <- 9000: 621a8205422100240283022f008a01058b032f0604800200488801f0
+ -> 00a4000402 2f00
+ <- 611c:
+ -> 00c000001c
+ <- 9000: 621a8205422100240283022f008a01058b032f0604800200488801f0
+ -> 00b2010424
+ <- 9000: 61184f10a0000000871002ff33ffff890101010050045553494dffffffffffffffffffff
+ -> 00a4000402 2f00
+ <- 611c:
+ -> 00c000001c
+ <- 9000: 621a8205422100240283022f008a01058b032f0604800200488801f0
+ -> 00b2020424
+ <- 9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ -> 00a4040410 a0000000871002ff33ffff8901010100
+ <- 6143:
+ -> 00c0000043
+ <- 9000:
62418202782183027f518410a0000000871002ff33ffff8901010100a50c8001718304000333008701018a01058b032f0608c60f90017083010183018183010a83010c
+ -> 8010000003 ffffff
+ <- 9000:
+
+The application has hence bound to local TCP port 2775 and expects your SMS-sending
applications to send their
+SMS there. Once you do, you will see log output like below:
+::
+
+ WARNING smpp.twisted.protocol: SMPP connection established from ::ffff:127.0.0.1 to
port 2775
+ INFO smpp.twisted.server: Added CommandId.bind_transceiver bind for 'test'.
Active binds: CommandId.bind_transceiver: 1, CommandId.bind_transmitter: 0,
CommandId.bind_receiver: 0. Max binds: 2
+ INFO smpp.twisted.protocol: Bind request succeeded for test. 1 active binds
+
+And once your external program is sending SMS to the simulated SMSC, it will log
something like
+::
+
+ SMS_DELIVER(MTI=0, MMS=False, LP=False, RP=False, UDHI=True, SRI=False,
OA=AddressField(TON=international, NPI=unknown, 12), PID=7f, DCS=f6,
SCTS=bytearray(b'"pR\x00\x00\x00\x00'), UDL=45,
UD=b"\x02p\x00\x00(\x15\x16\x19\x12\x12\xb0\x00\x01'\xfa(\xa5\xba\xc6\x9d<^\x9d\xf2\xc7\x15]\xfd\xdeD\x9c\x82k#b\x15Ve0x{0\xe8\xbe]")
+ SMSPPDownload(DeviceIdentities({'source_dev_id': 'network',
'dest_dev_id': 'uicc'}),Address({'ton_npi': 0,
'call_number': '0123456'}),SMS_TPDU({'tpdu':
'400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d'}))
+ ENVELOPE:
d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
+ -> 80c2000049
d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
+ <- 6129:
+ -> 80c0000029
+ <- 9000:
027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c
+ SW 9000:
027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c
diff --git a/pySim-smpp2sim.py b/pySim-smpp2sim.py
new file mode 100755
index 0000000..2675380
--- /dev/null
+++ b/pySim-smpp2sim.py
@@ -0,0 +1,272 @@
+#!/usr/bin/env python3
+#
+# Program to emulate the entire communication path SMSC-MSC-BSC-BTS-ME
+# that is usually between an OTA backend and the SIM card. This allows
+# to play with SIM OTA technology without using a mobile network or even
+# a mobile phone.
+#
+# An external application must encode (and encrypt/sign) the OTA SMS
+# and submit them via SMPP to this program, just like it would submit
+# it normally to a SMSC (SMS Service Centre). The program then re-formats
+# the SMPP-SUBMIT into a SMS DELIVER TPDU and passes it via an ENVELOPE
+# APDU to the SIM card that is locally inserted into a smart card reader.
+#
+# The path from SIM to external OTA application works the opposite way.
+
+# (C) 2023-2024 by Harald Welte <laforge(a)osmocom.org>
+#
+# 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 2 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, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import logging
+import colorlog
+from pprint import pprint as pp
+
+from twisted.protocols import basic
+from twisted.internet import defer, endpoints, protocol, reactor, task
+from twisted.cred.portal import IRealm
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+from twisted.cred.portal import Portal
+from zope.interface import implementer
+
+from smpp.twisted.config import SMPPServerConfig
+from smpp.twisted.server import SMPPServerFactory, SMPPBindManager
+from smpp.twisted.protocol import SMPPSessionStates, DataHandlerResponse
+
+from smpp.pdu import pdu_types, operations, pdu_encoding
+
+from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
+
+from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args,
init_reader, ApduTracer
+from pySim.commands import SimCardCommands
+from pySim.cards import UiccCardBase
+from pySim.exceptions import *
+from pySim.cat import ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload
+from pySim.cat import DeviceIdentities, Address
+from pySim.utils import b2h, h2b
+
+logger = logging.getLogger(__name__)
+
+# MSISDNs to use when generating proactive SMS messages
+SIM_MSISDN='23'
+ESME_MSISDN='12'
+
+# HACK: we need some kind of mapping table between system_id and card-reader
+# or actually route based on MSISDNs
+hackish_global_smpp = None
+
+class MyApduTracer(ApduTracer):
+ def trace_response(self, cmd, sw, resp):
+ print("-> %s %s" % (cmd[:10], cmd[10:]))
+ print("<- %s: %s" % (sw, resp))
+
+class Proact(ProactiveHandler):
+ #def __init__(self, smpp_factory):
+ # self.smpp_factory = smpp_factory
+
+ @staticmethod
+ def _find_first_element_of_type(instlist, cls):
+ for i in instlist:
+ if isinstance(i, cls):
+ return i
+ return None
+
+ """Call-back which the pySim transport core calls whenever it receives
a
+ proactive command from the SIM."""
+ def handle_SendShortMessage(self, data):
+ """Card requests sending a SMS."""
+ print("SendShortMessage")
+ pp(data)
+ # Relevant parts in data: Address, SMS_TPDU
+ addr_ie = Proact._find_first_element_of_type(data.children, Address)
+ sms_tpdu_ie = Proact._find_first_element_of_type(data.children, SMS_TPDU)
+ raw_tpdu = sms_tpdu_ie.decoded['tpdu']
+ submit = SMS_SUBMIT.fromBytes(raw_tpdu)
+ self.send_sms_via_smpp(data)
+ def handle_OpenChannel(self, data):
+ """Card requests opening a new channel via a UDP/TCP
socket."""
+ print("OpenChannel")
+ pp(data)
+ pass
+ def handle_CloseChannel(self, data):
+ print("CloseChannel")
+ """Close a channel."""
+ pp(data)
+ pass
+ def handleReceiveData(self, data):
+ print("ReceiveData")
+ """Receive/read data from the socket."""
+ pp(data)
+ pass
+ def handleSendData(self, data):
+ print("SendData")
+ """Send/write data to the socket."""
+ pp(data)
+ pass
+ def getChannelStatus(self, data):
+ print("GetChannelStatus")
+ pp(data)
+ pass
+
+ def send_sms_via_smpp(self, data):
+ # while in a normal network the phone/ME would *submit* a message to the SMSC,
+ # we are actually emulating the SMSC itself, so we must *deliver* the message
+ # to the ESME
+ dcs = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
+ pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
+ esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT,
pdu_types.EsmClassType.DEFAULT,
+
gsmFeatures=[pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET])
+ deliver = operations.DeliverSM(source_addr=SIM_MSISDN,
+ destination_addr=ESME_MSISDN,
+ esm_class=esm_class,
+ protocol_id=0x7F,
+ data_coding=dcs,
+ short_message=h2b(data))
+ hackish_global_smpp.sendDataRequest(deliver)
+# # obtain the connection/binding of system_id to be used for delivering MO-SMS to
the ESME
+# connection =
smpp_server.getBoundConnections[system_id].getNextBindingForDelivery()
+# connection.sendDataRequest(deliver)
+
+
+
+def dcs_is_8bit(dcs):
+ if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
+ pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
+ return True
+ if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
+
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED_COMMON):
+ return True
+ if dcs.scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS and
dcs.schemeData['msgCoding'] == pdu_types.DataCodingGsmMsgCoding.DATA_8BIT:
+ return True
+ else:
+ return False
+
+
+class MyServer:
+
+ @implementer(IRealm)
+ class SmppRealm:
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ return ('SMPP', avatarId, lambda: None)
+
+ def __init__(self, tcp_port:int = 2775, bind_ip = '::'):
+ smpp_config = SMPPServerConfig(msgHandler=self._msgHandler,
+ systems={'test': {'max_bindings':
2}})
+ portal = Portal(self.SmppRealm())
+ credential_checker = InMemoryUsernamePasswordDatabaseDontUse()
+ credential_checker.addUser('test', 'test')
+ portal.registerChecker(credential_checker)
+ self.factory = SMPPServerFactory(smpp_config, auth_portal=portal)
+ logger.info('Binding Virtual SMSC to TCP Port %u at %s' % (tcp_port,
bind_ip))
+ smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port,
interface=bind_ip)
+ smppEndpoint.listen(self.factory)
+ self.tp = self.scc = self.card = None
+
+ def connect_to_card(self, tp: LinkBase):
+ self.tp = tp
+ self.scc = SimCardCommands(self.tp)
+ self.card = UiccCardBase(self.scc)
+ # this should be part of UiccCardBase, but FairewavesSIM breaks with that :/
+ self.scc.cla_byte = "00"
+ self.scc.sel_ctrl = "0004"
+ self.card.read_aids()
+ self.card.select_adf_by_aid(adf='usim')
+ # FIXME: create a more realistic profile than ffffff
+ self.scc.terminal_profile('ffffff')
+
+ def _msgHandler(self, system_id, smpp, pdu):
+ # HACK: we need some kind of mapping table between system_id and card-reader
+ # or actually route based on MSISDNs
+ global hackish_global_smpp
+ hackish_global_smpp = smpp
+ #pp(pdu)
+ if pdu.id == pdu_types.CommandId.submit_sm:
+ return self.handle_submit_sm(system_id, smpp, pdu)
+ else:
+ logging.warning('Rejecting non-SUBMIT commandID')
+ return pdu_types.CommandStatus.ESME_RINVCMDID
+
+ def handle_submit_sm(self, system_id, smpp, pdu):
+ # check for valid data coding scheme + PID
+ if not dcs_is_8bit(pdu.params['data_coding']):
+ logging.warning('Rejecting non-8bit DCS')
+ return pdu_types.CommandStatus.ESME_RINVDCS
+ if pdu.params['protocol_id'] != 0x7f:
+ logging.warning('Rejecting non-SIM PID')
+ return pdu_types.CommandStatus.ESME_RINVDCS
+
+ # 1) build a SMS-DELIVER (!) from the SMPP-SUBMIT
+ tpdu = SMS_DELIVER.from_smpp_submit(pdu)
+ print(tpdu)
+ # 2) wrap into the CAT ENVELOPE for SMS-PP-Download
+ tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})
+ addr_ie = Address(decoded={'ton_npi': 0x00, 'call_number':
'0123456'})
+ dev_ids = DeviceIdentities(decoded={'source_dev_id': 'network',
'dest_dev_id': 'uicc'})
+ sms_dl = SMSPPDownload(children=[dev_ids, addr_ie, tpdu_ie])
+ # 3) send to the card
+ envelope_hex = b2h(sms_dl.to_tlv())
+ print("ENVELOPE: %s" % envelope_hex)
+ (data, sw) = self.scc.envelope(envelope_hex)
+ print("SW %s: %s" % (sw, data))
+ if sw in ['9200', '9300']:
+ # TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit
Busy'
+ return pdu_types.CommandStatus.ESME_RSUBMITFAIL
+ elif sw == '9000' or sw[0:2] in ['6f', '62',
'63'] and len(data):
+ # data something like 027100000e0ab000110000000000000001612f or
+ # 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
+ # which is the user-data portion of the SMS starting with the UDH (027100)
+ # TODO: return the response back to the sender in an RP-ACK; PID/DCS like in
CMD
+ deliver =
operations.DeliverSM(service_type=pdu.params['service_type'],
+
source_addr_ton=pdu.params['dest_addr_ton'],
+
source_addr_npi=pdu.params['dest_addr_npi'],
+
source_addr=pdu.params['destination_addr'],
+
dest_addr_ton=pdu.params['source_addr_ton'],
+
dest_addr_npi=pdu.params['source_addr_npi'],
+
destination_addr=pdu.params['source_addr'],
+ esm_class=pdu.params['esm_class'],
+
protocol_id=pdu.params['protocol_id'],
+
priority_flag=pdu.params['priority_flag'],
+
data_coding=pdu.params['data_coding'],
+ short_message=h2b(data))
+ smpp.sendDataRequest(deliver)
+ return pdu_types.CommandStatus.ESME_ROK
+ else:
+ return pdu_types.CommandStatus.ESME_RSUBMITFAIL
+
+
+option_parser =
argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+argparse_add_reader_args(option_parser)
+smpp_group = option_parser.add_argument_group('SMPP Options')
+smpp_group.add_argument('--smpp-bind-port', type=int, default=2775,
+ help='TCP Port to bind the SMPP socket to')
+smpp_group.add_argument('--smpp-bind-ip', default='::',
+ help='IPv4/IPv6 address to bind the SMPP socket to')
+
+if __name__ == '__main__':
+ log_format='%(log_color)s%(levelname)-8s%(reset)s %(name)s: %(message)s'
+ colorlog.basicConfig(level=logging.INFO, format = log_format)
+ logger = colorlog.getLogger()
+
+ opts = option_parser.parse_args()
+
+ tp = init_reader(opts, proactive_handler = Proact())
+ if tp is None:
+ exit(1)
+ tp.connect()
+ tp.apdu_tracer = MyApduTracer()
+
+ ms = MyServer(opts.smpp_bind_port, opts.smpp_bind_ip)
+ ms.connect_to_card(tp)
+ reactor.run()
+
diff --git a/requirements.txt b/requirements.txt
index c12daf8..ec04b6e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,3 +14,4 @@
git+https://github.com/osmocom/asn1tools
packaging
git+https://github.com/hologram-io/smpp.pdu
+git+https://github.com/jookies/smpp.twisted
diff --git a/setup.py b/setup.py
index 1a41d4f..e9ffd97 100644
--- a/setup.py
+++ b/setup.py
@@ -31,11 +31,13 @@
"pycryptodomex",
"packaging",
"smpp.pdu @
git+https://github.com/hologram-io/smpp.pdu"quot;,
+ "smpp.twisted @
git+https://github.com/jookies/smpp.twisted"quot;,
],
scripts=[
'pySim-prog.py',
'pySim-read.py',
'pySim-shell.py',
'pySim-trace.py',
+ 'pySim-smpp2sim.py',
]
)
--
To view, visit
https://gerrit.osmocom.org/c/pysim/+/37456?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: Ie5bae9d823bca6f6c658bd455303f63bace2258c
Gerrit-Change-Number: 37456
Gerrit-PatchSet: 1
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-MessageType: newchange