aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--selftest/template_test.ok11
-rwxr-xr-xselftest/template_test.py13
-rw-r--r--selftest/template_test/osmo-nitb.cfg.tmpl18
-rw-r--r--src/osmo_gsm_tester/esme.py138
-rw-r--r--src/osmo_gsm_tester/osmo_msc.py5
-rw-r--r--src/osmo_gsm_tester/osmo_nitb.py5
-rw-r--r--src/osmo_gsm_tester/sms.py18
-rw-r--r--src/osmo_gsm_tester/smsc.py50
-rw-r--r--src/osmo_gsm_tester/suite.py10
-rw-r--r--src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl14
-rw-r--r--src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl14
-rw-r--r--src/osmo_gsm_tester/test.py6
-rwxr-xr-xsuites/aoip_smpp/esme_connect_policy_acceptall.py37
-rwxr-xr-xsuites/aoip_smpp/esme_connect_policy_closed.py57
-rwxr-xr-xsuites/aoip_smpp/esme_ms_sms.py56
-rw-r--r--suites/aoip_smpp/suite.conf10
-rwxr-xr-xsuites/smpp/esme_connect_policy_acceptall.py32
-rwxr-xr-xsuites/smpp/esme_connect_policy_closed.py51
-rwxr-xr-xsuites/smpp/esme_ms_sms.py49
-rw-r--r--suites/smpp/suite.conf10
20 files changed, 574 insertions, 30 deletions
diff --git a/selftest/template_test.ok b/selftest/template_test.ok
index 1267dac..688361f 100644
--- a/selftest/template_test.ok
+++ b/selftest/template_test.ok
@@ -136,10 +136,13 @@ network
phys_chan_config val_phys_chan_config_3
smpp
local-tcp-ip val_ip_address 2775
- system-id test
- policy closed
- esme test
- password test
+ system-id test-nitb
+ policy val_smsc_policy
+ esme val_system_id_esme0
+ password val_password_esme0
+ default-route
+ esme val_system_id_esme1
+ no password
default-route
ctrl
bind val_ip_address
diff --git a/selftest/template_test.py b/selftest/template_test.py
index 45347b6..f8c32a5 100755
--- a/selftest/template_test.py
+++ b/selftest/template_test.py
@@ -35,6 +35,11 @@ mock_bts = {
)
}
+mock_esme = {
+ 'system_id': 'val_system_id',
+ 'password': 'val_password'
+}
+
def clone_mod(d, val_ext):
c = dict(d)
for name in c.keys():
@@ -47,6 +52,10 @@ def clone_mod(d, val_ext):
mock_bts0 = clone_mod(mock_bts, '_bts0')
mock_bts1 = clone_mod(mock_bts, '_bts1')
+mock_esme0 = clone_mod(mock_esme, '_esme0')
+mock_esme1 = clone_mod(mock_esme, '_esme1')
+mock_esme1['password'] = ''
+
vals = dict(nitb=dict(
net=dict(
mcc='val_mcc',
@@ -59,6 +68,10 @@ vals = dict(nitb=dict(
),
ip_address=dict(addr='val_ip_address'),
),
+ smsc=dict(
+ policy='val_smsc_policy',
+ esme_list=(mock_esme0, mock_esme1)
+ ),
)
print(template.render('osmo-nitb.cfg', vals))
diff --git a/selftest/template_test/osmo-nitb.cfg.tmpl b/selftest/template_test/osmo-nitb.cfg.tmpl
index 3404b7f..7a76878 100644
--- a/selftest/template_test/osmo-nitb.cfg.tmpl
+++ b/selftest/template_test/osmo-nitb.cfg.tmpl
@@ -47,12 +47,18 @@ network
timer t3119 0
timer t3141 0
smpp
- local-tcp-ip ${smpp_bind_ip} 2775
- system-id test
- policy closed
- esme test
- password test
- default-route
+ local-tcp-ip ${nitb.ip_address.addr} 2775
+ system-id test-nitb
+ policy ${smsc.policy}
+ %for esme in esme_list:
+ esme ${esme.system_id}
+ % if esme.password == '':
+ no password
+ % else:
+ password ${esme.password}
+ % endif
+ default-route
+ %endfor
ctrl
bind ${ctrl_bind_ip}
%for bts in bts_list:
diff --git a/src/osmo_gsm_tester/esme.py b/src/osmo_gsm_tester/esme.py
new file mode 100644
index 0000000..f92863d
--- /dev/null
+++ b/src/osmo_gsm_tester/esme.py
@@ -0,0 +1,138 @@
+# osmo_gsm_tester: SMPP ESME to talk to SMSC
+#
+# Copyright (C) 2017 by sysmocom - s.f.m.c. GmbH
+#
+# Author: Pau Espin Pedrol <pespin@sysmocom.de>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import smpplib.gsm
+import smpplib.client
+import smpplib.consts
+import smpplib.exceptions
+
+from . import log, util, event_loop, sms
+
+# if you want to know what's happening inside python-smpplib
+#import logging
+#logging.basicConfig(level='DEBUG')
+
+MAX_SYS_ID_LEN = 16
+MAX_PASSWD_LEN = 16
+
+class Esme(log.Origin):
+ client = None
+ smsc = None
+
+ def __init__(self, msisdn):
+ self.msisdn = msisdn
+ # Get last characters of msisdn to stay inside MAX_SYS_ID_LEN. Similar to modulus operator.
+ self.set_system_id('esme-' + self.msisdn[-11:])
+ super().__init__(log.C_TST, self.system_id)
+ self.set_password('esme-pwd')
+ self.connected = False
+ self.bound = False
+ self.listening = False
+
+ def __del__(self):
+ try:
+ self.disconnect()
+ except smpplib.exceptions.ConnectionError:
+ pass
+
+ def set_smsc(self, smsc):
+ self.smsc = smsc
+
+ def set_system_id(self, name):
+ if len(name) > MAX_SYS_ID_LEN:
+ raise log.Error('Esme system_id too long! %d vs %d', len(name), MAX_SYS_ID_LEN)
+ self.system_id = name
+
+ def set_password(self, password):
+ if len(password) > MAX_PASSWD_LEN:
+ raise log.Error('Esme password too long! %d vs %d', len(password), MAX_PASSWD_LEN)
+ self.password = password
+
+ def conf_for_smsc(self):
+ config = { 'system_id': self.system_id, 'password': self.password }
+ return config
+
+ def poll(self):
+ self.client.poll()
+
+ def start_listening(self):
+ self.listening = True
+ event_loop.register_poll_func(self.poll)
+
+ def stop_listening(self):
+ if not self.listening:
+ return
+ self.listening = False
+ # Empty the queue before processing the unbind + disconnect PDUs
+ event_loop.unregister_poll_func(self.poll)
+ self.poll()
+
+ def connect(self):
+ host, port = self.smsc.addr_port
+ if self.client:
+ self.disconnect()
+ self.client = smpplib.client.Client(host, port, timeout=None)
+ self.client.set_message_sent_handler(
+ lambda pdu: self.dbg('message sent:', repr(pdu)) )
+ self.client.set_message_received_handler(
+ lambda pdu: self.dbg('message received:', repr(pdu)) )
+ self.client.connect()
+ self.connected = True
+ self.client.bind_transceiver(system_id=self.system_id, password=self.password)
+ self.bound = True
+ self.log('Connected and bound successfully. Starting to listen')
+ self.start_listening()
+
+ def disconnect(self):
+ self.stop_listening()
+ if self.bound:
+ self.client.unbind()
+ self.bound = False
+ if self.connected:
+ self.client.disconnect()
+ self.connected = False
+
+ def run_method_expect_failure(self, errcode, method, *args):
+ try:
+ method(*args)
+ #it should not succeed, raise an exception:
+ raise log.Error('SMPP Failure: %s should have failed with SMPP error %d (%s) but succeeded.' % (method, errcode, smpplib.consts.DESCRIPTIONS[errcode]))
+ except smpplib.exceptions.PDUError as e:
+ if e.args[1] != errcode:
+ raise e
+
+ def sms_send(self, sms_obj):
+ parts, encoding_flag, msg_type_flag = smpplib.gsm.make_parts(str(sms_obj))
+
+ self.log('Sending SMS "%s" to %s' % (str(sms_obj), sms_obj.dst_msisdn()))
+ for part in parts:
+ pdu = self.client.send_message(
+ source_addr_ton=smpplib.consts.SMPP_TON_INTL,
+ source_addr_npi=smpplib.consts.SMPP_NPI_ISDN,
+ source_addr=sms_obj.src_msisdn(),
+ dest_addr_ton=smpplib.consts.SMPP_TON_INTL,
+ dest_addr_npi=smpplib.consts.SMPP_NPI_ISDN,
+ destination_addr=sms_obj.dst_msisdn(),
+ short_message=part,
+ data_coding=encoding_flag,
+ esm_class=smpplib.consts.SMPP_MSGMODE_FORWARD,
+ registered_delivery=False,
+ )
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/osmo_msc.py b/src/osmo_gsm_tester/osmo_msc.py
index 063b477..2c9b1e3 100644
--- a/src/osmo_gsm_tester/osmo_msc.py
+++ b/src/osmo_gsm_tester/osmo_msc.py
@@ -20,7 +20,7 @@
import os
import pprint
-from . import log, util, config, template, process, osmo_ctrl, pcap_recorder
+from . import log, util, config, template, process, osmo_ctrl, pcap_recorder, smsc
class OsmoMsc(log.Origin):
suite_run = None
@@ -30,6 +30,7 @@ class OsmoMsc(log.Origin):
process = None
hlr = None
config = None
+ smsc = None
def __init__(self, suite_run, hlr, mgcpgw, ip_address):
super().__init__(log.C_RUN, 'osmo-msc_%s' % ip_address.get('addr'))
@@ -37,6 +38,7 @@ class OsmoMsc(log.Origin):
self.ip_address = ip_address
self.hlr = hlr
self.mgcpgw = mgcpgw
+ self.smsc = smsc.Smsc((ip_address.get('addr'), 2775))
def start(self):
self.log('Starting osmo-msc')
@@ -73,6 +75,7 @@ class OsmoMsc(log.Origin):
config.overlay(values, dict(msc=dict(ip_address=self.ip_address)))
config.overlay(values, self.mgcpgw.conf_for_msc())
config.overlay(values, self.hlr.conf_for_msc())
+ config.overlay(values, self.smsc.get_config())
self.config = values
self.dbg('MSC CONFIG:\n' + pprint.pformat(values))
diff --git a/src/osmo_gsm_tester/osmo_nitb.py b/src/osmo_gsm_tester/osmo_nitb.py
index 484358e..3ef5276 100644
--- a/src/osmo_gsm_tester/osmo_nitb.py
+++ b/src/osmo_gsm_tester/osmo_nitb.py
@@ -21,7 +21,7 @@ import os
import re
import pprint
-from . import log, util, config, template, process, osmo_ctrl, pcap_recorder
+from . import log, util, config, template, process, osmo_ctrl, pcap_recorder, smsc
class OsmoNitb(log.Origin):
suite_run = None
@@ -30,12 +30,14 @@ class OsmoNitb(log.Origin):
config_file = None
process = None
bts = None
+ smsc = None
def __init__(self, suite_run, ip_address):
super().__init__(log.C_RUN, 'osmo-nitb_%s' % ip_address.get('addr'))
self.suite_run = suite_run
self.ip_address = ip_address
self.bts = []
+ self.smsc = smsc.Smsc((ip_address.get('addr'), 2775))
def start(self):
self.log('Starting osmo-nitb')
@@ -75,6 +77,7 @@ class OsmoNitb(log.Origin):
for bts in self.bts:
bts_list.append(bts.conf_for_bsc())
config.overlay(values, dict(nitb=dict(net=dict(bts_list=bts_list))))
+ config.overlay(values, self.smsc.get_config())
self.config = values
self.dbg('NITB CONFIG:\n' + pprint.pformat(values))
diff --git a/src/osmo_gsm_tester/sms.py b/src/osmo_gsm_tester/sms.py
index 570ef96..e264b66 100644
--- a/src/osmo_gsm_tester/sms.py
+++ b/src/osmo_gsm_tester/sms.py
@@ -21,14 +21,16 @@ class Sms:
_last_sms_idx = 0
msg = None
- def __init__(self, from_msisdn=None, to_msisdn=None, *tokens):
+ def __init__(self, src_msisdn=None, dst_msisdn=None, *tokens):
Sms._last_sms_idx += 1
+ self._src_msisdn = src_msisdn
+ self._dst_msisdn = dst_msisdn
msgs = ['message nr. %d' % Sms._last_sms_idx]
msgs.extend(tokens)
- if from_msisdn:
- msgs.append('from %s' % from_msisdn)
- if to_msisdn:
- msgs.append('to %s' % to_msisdn)
+ if src_msisdn:
+ msgs.append('from %s' % src_msisdn)
+ if dst_msisdn:
+ msgs.append('to %s' % dst_msisdn)
self.msg = ', '.join(msgs)
def __str__(self):
@@ -42,6 +44,12 @@ class Sms:
return self.msg == other.msg
return self.msg == other
+ def src_msisdn(self):
+ return self._src_msisdn
+
+ def dst_msisdn(self):
+ return self._dst_msisdn
+
def matches(self, msg):
return self.msg == msg
diff --git a/src/osmo_gsm_tester/smsc.py b/src/osmo_gsm_tester/smsc.py
new file mode 100644
index 0000000..4837f37
--- /dev/null
+++ b/src/osmo_gsm_tester/smsc.py
@@ -0,0 +1,50 @@
+# osmo_gsm_tester: smsc interface
+#
+# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
+#
+# Author: Pau Espin Pedrol <pespin@sysmocom.de>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from . import log, config, util, template, process
+
+class Smsc:
+ esmes = None
+
+ SMSC_POLICY_CLOSED = 'closed'
+ SMSC_POLICY_ACCEPT_ALL = 'accept-all'
+
+ def __init__(self, smpp_addr_port):
+ self.addr_port = smpp_addr_port
+ self.policy = self.SMSC_POLICY_CLOSED
+ self.esmes = []
+
+ def get_config(self):
+ values = { 'smsc': { 'policy': self.policy } }
+ esme_list = []
+ for esme in self.esmes:
+ esme_list.append(esme.conf_for_smsc())
+ config.overlay(values, dict(smsc=dict(esme_list=esme_list)))
+ return values
+
+ def esme_add(self, esme):
+ if esme.system_id == '':
+ raise log.Error('esme system_id cannot be empty')
+ self.esmes.append(esme)
+ esme.set_smsc(self)
+
+ def set_smsc_policy(self, smsc_policy):
+ self.policy = smsc_policy
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py
index f4b9260..71b8dc7 100644
--- a/src/osmo_gsm_tester/suite.py
+++ b/src/osmo_gsm_tester/suite.py
@@ -23,7 +23,7 @@ import time
import copy
import traceback
import pprint
-from . import config, log, template, util, resource, schema, ofono_client, event_loop
+from . import config, log, template, util, resource, schema, ofono_client, event_loop, esme, sms
from . import osmo_nitb
from . import osmo_hlr, osmo_mgcpgw, osmo_msc, osmo_bsc, osmo_stp
from . import test
@@ -99,7 +99,7 @@ class Test(log.Origin):
log.large_separator(self.suite_run.trial.name(), self.suite_run.name(), self.name(), sublevel=3)
self.status = Test.UNKNOWN
self.start_timestamp = time.time()
- test.setup(self.suite_run, self, ofono_client, sys.modules[__name__], event_loop)
+ test.setup(self.suite_run, self, ofono_client, sys.modules[__name__], event_loop, sms)
with self.redirect_stdout():
util.run_python_file('%s.%s' % (self.suite_run.definition.name(), self.basename),
self.path)
@@ -363,8 +363,12 @@ class SuiteRun(log.Origin):
l.append(self.modem())
return l
+ def esme(self):
+ esme_obj = esme.Esme(self.msisdn())
+ return esme_obj
+
def msisdn(self):
- msisdn = self.resources_pool.next_msisdn(self.origin)
+ msisdn = self.resources_pool.next_msisdn(self)
self.log('using MSISDN', msisdn)
return msisdn
diff --git a/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl
index 247365e..89982e0 100644
--- a/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl
@@ -23,10 +23,16 @@ ctrl
bind ${msc.ip_address.addr}
smpp
local-tcp-ip ${msc.ip_address.addr} 2775
- system-id test
- policy closed
- esme test
- password test
+ system-id test-msc
+ policy ${smsc.policy}
+%for esme in smsc.esme_list:
+ esme ${esme.system_id}
+% if esme.password == '':
+ no password
+% else:
+ password ${esme.password}
+% endif
default-route
+%endfor
hlr
remote-ip ${hlr.ip_address.addr}
diff --git a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
index a47ac02..23cc225 100644
--- a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
@@ -76,10 +76,16 @@ network
%endfor
smpp
local-tcp-ip ${nitb.ip_address.addr} 2775
- system-id test
- policy closed
- esme test
- password test
+ system-id test-nitb
+ policy ${smsc.policy}
+%for esme in smsc.esme_list:
+ esme ${esme.system_id}
+% if esme.password == '':
+ no password
+% else:
+ password ${esme.password}
+% endif
default-route
+%endfor
ctrl
bind ${nitb.ip_address.addr}
diff --git a/src/osmo_gsm_tester/test.py b/src/osmo_gsm_tester/test.py
index 2958501..49911b3 100644
--- a/src/osmo_gsm_tester/test.py
+++ b/src/osmo_gsm_tester/test.py
@@ -33,9 +33,10 @@ sleep = None
poll = None
prompt = None
Timeout = None
+Sms = None
-def setup(suite_run, _test, ofono_client, suite_module, event_module):
- global trial, suite, test, resources, log, dbg, err, wait, wait_no_raise, sleep, poll, prompt, Timeout
+def setup(suite_run, _test, ofono_client, suite_module, event_module, sms_module):
+ global trial, suite, test, resources, log, dbg, err, wait, wait_no_raise, sleep, poll, prompt, Timeout, Sms
trial = suite_run.trial
suite = suite_run
test = _test
@@ -49,5 +50,6 @@ def setup(suite_run, _test, ofono_client, suite_module, event_module):
poll = event_module.poll
prompt = suite_run.prompt
Timeout = suite_module.Timeout
+ Sms = sms_module.Sms
# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/suites/aoip_smpp/esme_connect_policy_acceptall.py b/suites/aoip_smpp/esme_connect_policy_acceptall.py
new file mode 100755
index 0000000..2a954d5
--- /dev/null
+++ b/suites/aoip_smpp/esme_connect_policy_acceptall.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'accept-all' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) which do not appear on
+# the config file
+
+from osmo_gsm_tester.test import *
+
+hlr = suite.hlr()
+bts = suite.bts() # bts not started, only needed for mgcpgw
+mgcpgw = suite.mgcpgw(bts_ip=bts.remote_addr())
+msc = suite.msc(hlr, mgcpgw)
+smsc = msc.smsc
+esme = suite.esme()
+
+# Here we deliberately omit calling smsc.esme_add() to avoid having it included
+# in the smsc config.
+smsc.set_smsc_policy(smsc.SMSC_POLICY_ACCEPT_ALL)
+esme.set_smsc(smsc)
+
+hlr.start()
+msc.start()
+mgcpgw.start()
+
+# Due to accept-all policy, connect() should work even if we didn't previously
+# configure the esme in the smsc, no matter the system_id / password we use.
+log('Test connect with non-empty values in system_id and password')
+esme.set_system_id('foo')
+esme.set_password('bar')
+esme.connect()
+esme.disconnect()
+
+log('Test connect with empty values in system_id and password')
+esme.set_system_id('')
+esme.set_password('')
+esme.connect()
+esme.disconnect()
diff --git a/suites/aoip_smpp/esme_connect_policy_closed.py b/suites/aoip_smpp/esme_connect_policy_closed.py
new file mode 100755
index 0000000..29b25d1
--- /dev/null
+++ b/suites/aoip_smpp/esme_connect_policy_closed.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'closed' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * SMPP interface of SMSC rejects ESMEs with known system id but wrong password.
+# * SMPP interface of SMSC rejects ESEMs with unknown system id
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVPASWD = 0x0000000E
+SMPP_ESME_RINVSYSID = 0x0000000F
+
+hlr = suite.hlr()
+bts = suite.bts()
+mgcpgw = suite.mgcpgw(bts_ip=bts.remote_addr())
+msc = suite.msc(hlr, mgcpgw)
+smsc = msc.smsc
+
+esme = suite.esme()
+esme_no_pwd = suite.esme()
+esme_no_pwd.set_password('')
+
+smsc.set_smsc_policy(smsc.SMSC_POLICY_CLOSED)
+smsc.esme_add(esme)
+smsc.esme_add(esme_no_pwd)
+
+hlr.start()
+msc.start()
+mgcpgw.start()
+
+log('Test with correct credentials (no password)')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials (no password, non empty)')
+esme_no_pwd.set_password('foobar')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials')
+esme.connect()
+esme.disconnect()
+
+log('Test with bad password, checking for failure')
+correct_password = esme.password
+new_password = 'barfoo' if correct_password == 'foobar' else 'foobar'
+esme.set_password(new_password)
+esme.run_method_expect_failure(SMPP_ESME_RINVPASWD, esme.connect)
+esme.set_password(correct_password)
+
+log('Test with bad system_id, checking for failure')
+correct_system_id = esme.system_id
+new_system_id = 'barfoo' if correct_system_id == 'foobar' else 'foobar'
+esme.set_system_id(new_system_id)
+esme.run_method_expect_failure(SMPP_ESME_RINVSYSID, esme.connect)
+esme.set_system_id(correct_system_id)
diff --git a/suites/aoip_smpp/esme_ms_sms.py b/suites/aoip_smpp/esme_ms_sms.py
new file mode 100755
index 0000000..7f9ef18
--- /dev/null
+++ b/suites/aoip_smpp/esme_ms_sms.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * ESME can send an SMS to an already registered MS when SMSC is in 'forward' mode.
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVDSTADR = 0x0000000B
+
+hlr = suite.hlr()
+bts = suite.bts()
+mgcpgw = suite.mgcpgw(bts_ip=bts.remote_addr())
+msc = suite.msc(hlr, mgcpgw)
+bsc = suite.bsc(msc)
+stp = suite.stp()
+bsc.bts_add(bts)
+
+ms = suite.modem()
+esme = suite.esme()
+msc.smsc.esme_add(esme)
+
+hlr.start()
+stp.start()
+msc.start()
+mgcpgw.start()
+bsc.start()
+bts.start()
+
+esme.connect()
+hlr.subscriber_add(ms)
+ms.connect(msc.mcc_mnc())
+
+ms.log_info()
+print('waiting for modem to attach...')
+wait(ms.is_connected, msc.mcc_mnc())
+wait(msc.subscriber_attached, ms)
+
+print('sending first sms...')
+msg = Sms(esme.msisdn, ms.msisdn, 'smpp send message')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+print('sending second sms (unicode chars not in gsm aplhabet)...')
+msg = Sms(esme.msisdn, ms.msisdn, 'chars:[кизаçйж]')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+# FIXME: This test is not failing with error but succeeds, need to check why: (forward vs store policy?)
+# wrong_msisdn = ms.msisdn + esme.msisdn
+# print('sending third sms (with wrong msisdn %s)' % wrong_msisdn)
+# msg = Sms(esme.msisdn, wrong_msisdn, 'smpp message with wrong dest')
+# esme.run_method_expect_failure(SMPP_ESME_RINVDSTADR, esme.sms_send, msg)
+
+esme.disconnect()
diff --git a/suites/aoip_smpp/suite.conf b/suites/aoip_smpp/suite.conf
new file mode 100644
index 0000000..46f8d09
--- /dev/null
+++ b/suites/aoip_smpp/suite.conf
@@ -0,0 +1,10 @@
+resources:
+ ip_address:
+ - times: 5 # msc, bsc, hlr, stp, mgw
+ bts:
+ - times: 1
+ modem:
+ - times: 1
+
+defaults:
+ timeout: 60s
diff --git a/suites/smpp/esme_connect_policy_acceptall.py b/suites/smpp/esme_connect_policy_acceptall.py
new file mode 100755
index 0000000..d22703d
--- /dev/null
+++ b/suites/smpp/esme_connect_policy_acceptall.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'accept-all' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) which do not appear on
+# the config file
+
+from osmo_gsm_tester.test import *
+
+nitb = suite.nitb()
+smsc = nitb.smsc
+esme = suite.esme()
+
+# Here we deliberately omit calling smsc.esme_add() to avoid having it included
+# in the smsc config.
+smsc.set_smsc_policy(smsc.SMSC_POLICY_ACCEPT_ALL)
+esme.set_smsc(smsc)
+
+nitb.start()
+
+# Due to accept-all policy, connect() should work even if we didn't previously
+# configure the esme in the smsc, no matter the system_id / password we use.
+log('Test connect with non-empty values in system_id and password')
+esme.set_system_id('foo')
+esme.set_password('bar')
+esme.connect()
+esme.disconnect()
+
+log('Test connect with empty values in system_id and password')
+esme.set_system_id('')
+esme.set_password('')
+esme.connect()
+esme.disconnect()
diff --git a/suites/smpp/esme_connect_policy_closed.py b/suites/smpp/esme_connect_policy_closed.py
new file mode 100755
index 0000000..7fac276
--- /dev/null
+++ b/suites/smpp/esme_connect_policy_closed.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'closed' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * SMPP interface of SMSC rejects ESMEs with known system id but wrong password.
+# * SMPP interface of SMSC rejects ESEMs with unknown system id
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVPASWD = 0x0000000E
+SMPP_ESME_RINVSYSID = 0x0000000F
+
+nitb = suite.nitb()
+smsc = nitb.smsc
+esme = suite.esme()
+esme_no_pwd = suite.esme()
+esme_no_pwd.set_password('')
+
+smsc.set_smsc_policy(smsc.SMSC_POLICY_CLOSED)
+smsc.esme_add(esme)
+smsc.esme_add(esme_no_pwd)
+
+nitb.start()
+
+log('Test with correct credentials (no password)')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials (no password, non empty)')
+esme_no_pwd.set_password('foobar')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials')
+esme.connect()
+esme.disconnect()
+
+log('Test with bad password, checking for failure')
+correct_password = esme.password
+new_password = 'barfoo' if correct_password == 'foobar' else 'foobar'
+esme.set_password(new_password)
+esme.run_method_expect_failure(SMPP_ESME_RINVPASWD, esme.connect)
+esme.set_password(correct_password)
+
+log('Test with bad system_id, checking for failure')
+correct_system_id = esme.system_id
+new_system_id = 'barfoo' if correct_system_id == 'foobar' else 'foobar'
+esme.set_system_id(new_system_id)
+esme.run_method_expect_failure(SMPP_ESME_RINVSYSID, esme.connect)
+esme.set_system_id(correct_system_id)
diff --git a/suites/smpp/esme_ms_sms.py b/suites/smpp/esme_ms_sms.py
new file mode 100755
index 0000000..bc9d7d4
--- /dev/null
+++ b/suites/smpp/esme_ms_sms.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * ESME can send an SMS to an already registered MS when SMSC is in 'forward' mode.
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVDSTADR = 0x0000000B
+
+nitb = suite.nitb()
+bts = suite.bts()
+ms = suite.modem()
+esme = suite.esme()
+
+print('start nitb and bts...')
+nitb.bts_add(bts)
+nitb.smsc.esme_add(esme)
+nitb.start()
+bts.start()
+
+esme.connect()
+nitb.subscriber_add(ms)
+ms.connect(nitb.mcc_mnc())
+
+ms.log_info()
+print('waiting for modem to attach...')
+wait(ms.is_connected, nitb.mcc_mnc())
+wait(nitb.subscriber_attached, ms)
+
+print('sending first sms...')
+msg = Sms(esme.msisdn, ms.msisdn, 'smpp send message')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+print('sending second sms (unicode chars not in gsm aplhabet)...')
+msg = Sms(esme.msisdn, ms.msisdn, 'chars:[кизаçйж]')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+
+# FIXME: This test is not failing with error but succeeds, need to check why: (forward vs store policy?)
+# wrong_msisdn = ms.msisdn + esme.msisdn
+# print('sending third sms (with wrong msisdn %s)' % wrong_msisdn)
+# msg = Sms(esme.msisdn, wrong_msisdn, 'smpp message with wrong dest')
+# esme.run_method_expect_failure(SMPP_ESME_RINVDSTADR, esme.sms_send, msg)
+
+esme.disconnect()
diff --git a/suites/smpp/suite.conf b/suites/smpp/suite.conf
new file mode 100644
index 0000000..eb59abb
--- /dev/null
+++ b/suites/smpp/suite.conf
@@ -0,0 +1,10 @@
+resources:
+ ip_address:
+ - times: 1
+ bts:
+ - times: 1
+ modem:
+ - times: 1
+
+defaults:
+ timeout: 60s