Skip to content

Commit

Permalink
Merge pull request #177 from nickvsnetworking/roaming
Browse files Browse the repository at this point in the history
Roaming
  • Loading branch information
davidkneipp authored Jan 2, 2024
2 parents f780f68 + 40c7104 commit fb24498
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Sh-IMS-Data and IMSPrivateUserIdentity to default_sh_user_data.xml
- Optional config parameter `api.enable_insecure_auc` to allow retrieval of AuC keys through API
- sh_template_path in ims_subscriber
- generateUpgade.sh for generating alembic upgrade scripts
- Control of outbound roaming S6a AIR and ULA responses through roaming_rule and roaming_network objects.
- Roaming management on a per-subscriber basis, through subscriber.roaming_enabled and subscriber.roaming_rule_list.

## [1.0.0] - 2023-09-27

Expand Down
6 changes: 6 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ hss:
scscf_pool:
- 'scscf.ims.mnc001.mcc001.3gppnetwork.org'

roaming:
outbound:
# Whether or not to a subscriber to connect to an undefined network when outbound roaming.
allow_undefined_networks: True


api:
page_size: 200
# Whether or not to return key-based data when querying the AUC. Disable in production systems.
Expand Down
34 changes: 32 additions & 2 deletions lib/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class SUBSCRIBER(Base):
ue_ambr_dl = Column(Integer, default=999999, doc='Downlink Aggregate Maximum Bit Rate')
ue_ambr_ul = Column(Integer, default=999999, doc='Uplink Aggregate Maximum Bit Rate')
nam = Column(Integer, default=0, doc='Network Access Mode [3GPP TS. 123 008 2.1.1.2] - 0 (PACKET_AND_CIRCUIT) or 2 (ONLY_PACKET)')
roaming_enabled = Column(Boolean, default=1, doc='Whether or not to enable roaming on this subscriber')
roaming_rule_list = Column(String(512), doc='Comma separated list of roaming rules applicable to this subscriber')
subscribed_rau_tau_timer = Column(Integer, default=300, doc='Subscribed periodic TAU/RAU timer value in seconds')
serving_mme = Column(String(512), doc='MME serving this subscriber')
serving_mme_timestamp = Column(DateTime, doc='Timestamp of attach to MME')
Expand Down Expand Up @@ -150,6 +152,25 @@ class IMS_SUBSCRIBER(Base):
last_modified = Column(String(100), default=datetime.datetime.now(tz=timezone.utc), doc='Timestamp of last modification')
operation_logs = relationship("IMS_SUBSCRIBER_OPERATION_LOG", back_populates="ims_subscriber")

class ROAMING_RULE(Base):
__tablename__ = 'roaming_rule'
roaming_rule_id = Column(Integer, primary_key = True, doc='Unique ID of ROAMING_RULE entry')
roaming_network_id = Column(Integer, ForeignKey('roaming_network.roaming_network_id', ondelete='CASCADE'), doc='ID of the roaming network to apply the rule for')
allow = Column(Boolean, default=1, doc='Whether to allow outbound roaming on the network')
enabled = Column(Boolean, default=1, doc='Whether the rule is enabled')
last_modified = Column(String(100), default=datetime.datetime.now(tz=timezone.utc), doc='Timestamp of last modification')
operation_logs = relationship("ROAMING_RULE_OPERATION_LOG", back_populates="roaming_rule")

class ROAMING_NETWORK(Base):
__tablename__ = 'roaming_network'
roaming_network_id = Column(Integer, primary_key = True, doc='Unique ID of ROAMING_NETWORK entry')
name = Column(String(512), doc='Name of the roaming network')
preference = Column(Integer, default=1, doc='Preference of the network. Lower numbers are chosen first.')
mcc = Column(String(100), doc='MCC to apply the roaming rule for')
mnc = Column(String(100), doc='3 digit MNC to apply the roaming rule for')
last_modified = Column(String(100), default=datetime.datetime.now(tz=timezone.utc), doc='Timestamp of last modification')
operation_logs = relationship("ROAMING_NETWORK_OPERATION_LOG", back_populates="roaming_network")

class CHARGING_RULE(Base):
__tablename__ = 'charging_rule'
charging_rule_id = Column(Integer, primary_key = True, doc='Unique ID of CHARGING_RULE entry')
Expand Down Expand Up @@ -207,7 +228,6 @@ class SUBSCRIBER_ATTRIBUTES(Base):
value = Column(String(12000), doc='Arbitrary value')
operation_logs = relationship("SUBSCRIBER_ATTRIBUTES_OPERATION_LOG", back_populates="subscriber_attributes")


class OPERATION_LOG_BASE(Base):
__tablename__ = 'operation_log'
id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -250,6 +270,16 @@ class IMS_SUBSCRIBER_OPERATION_LOG(OPERATION_LOG_BASE):
ims_subscriber = relationship("IMS_SUBSCRIBER", back_populates="operation_logs")
ims_subscriber_id = Column(Integer, ForeignKey('ims_subscriber.ims_subscriber_id'))

class ROAMING_RULE_OPERATION_LOG(OPERATION_LOG_BASE):
__mapper_args__ = {'polymorphic_identity': 'roaming_rule'}
roaming_rule = relationship("ROAMING_RULE", back_populates="operation_logs")
roaming_rule_id = Column(Integer, ForeignKey('roaming_rule.roaming_rule_id'))

class ROAMING_NETWORK_OPERATION_LOG(OPERATION_LOG_BASE):
__mapper_args__ = {'polymorphic_identity': 'roaming_network'}
roaming_network = relationship("ROAMING_NETWORK", back_populates="operation_logs")
roaming_network_id = Column(Integer, ForeignKey('roaming_network.roaming_network_id'))

class CHARGING_RULE_OPERATION_LOG(OPERATION_LOG_BASE):
__mapper_args__ = {'polymorphic_identity': 'charging_rule'}
charging_rule = relationship("CHARGING_RULE", back_populates="operation_logs")
Expand Down Expand Up @@ -301,7 +331,7 @@ def __init__(self, logTool, redisMessaging=None):

self.engine = create_engine(
db_string,
echo = self.config['logging'].get('sqlalchemy_sql_echo', True),
echo = self.config['logging'].get('sqlalchemy_sql_echo', False),
pool_recycle=self.config['logging'].get('sqlalchemy_pool_recycle', 5),
pool_size=self.config['logging'].get('sqlalchemy_pool_size', 30),
max_overflow=self.config['logging'].get('sqlalchemy_max_overflow', 0))
Expand Down
131 changes: 130 additions & 1 deletion lib/diameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,66 @@ def deregisterIms(self, imsi=None, msisdn=None) -> bool:
self.logTool.log(service='HSS', level='error', message=f"[diameter.py] [deregisterIms] Error deregistering subscriber from IMS: {traceback.format_exc()}", redisClient=self.redisMessaging)
return False


def validateOutboundRoamingNetwork(self, assignedRoamingRules: str, mcc: str, mnc: str) -> bool:
"""
Ensures that a given PLMN is allowed for outbound roaming.
"""

allowUndefinedNetworks = self.config.get('roaming', {}).get('outbound', {}).get('allow_undefined_networks', True)
roamingRules = self.database.GetAll(self.database.ROAMING_RULE)
subscriberRoamingRules = assignedRoamingRules.split(',')

"""
Iterate over every roaming rule, and it's reference roaming network.
If the network that it refers to matches the supplied mcc and mnc to this function,
then apply the rule action (allow/deny).
"""


for subscriberRoamingRule in subscriberRoamingRules:
for roamingRule in roamingRules:
if roamingRule.get('roaming_rule_id') != subscriberRoamingRule:
continue
roamingNetworkId = roamingRule.get('roaming_network_id')
roamingNetworks = self.database.GetObj(self.database.ROAMING_NETWORK, roamingNetworkId)
allowNetwork = roamingRule.get('allow', True)
for roamingNetwork in roamingNetworks:
if str(roamingNetwork.get('mcc')) == mcc and str(roamingNetwork.get('mnc') == mnc):
if allowNetwork:
return True
else:
return False

"""
By this point we haven't matched on any rules.
If we're allowing undefined networks,
return True, otherwise return False.
"""
if allowUndefinedNetworks:
return True
else:
return False

def validateSubscriberRoaming(self, subscriber: dict, mcc: str, mnc: str) -> bool:
"""
Ensures that a given subscriber is allowed to roam to the provided PLMN.
Checks if a subscriber has roaming_enabled set in the subscriber object, and then evaluates the assigned roaming rules (if any).
"""

roamingEnabled = subscriber.get('roaming_enabled', True)
if not roamingEnabled:
return False

assignedRoamingRules = subscriber.get('roaming_rule_list', "")

outboundNetworkAllowed = self.validateOutboundRoamingNetwork(assignedRoamingRules=assignedRoamingRules, mcc=mcc, mnc=mnc)
if not outboundNetworkAllowed:
return False

return True


def AVP_278_Origin_State_Incriment(self, avps): #Capabilities Exchange Answer incriment AVP body
for avp_dicts in avps:
if avp_dicts['avp_code'] == 278:
Expand Down Expand Up @@ -1217,6 +1277,41 @@ def Answer_16777251_316(self, packet_vars, avps):
message = template.format(type(ex).__name__, ex.args)
raise

try:
plmn = self.get_avp_data(avps, 1407)[0] #Visited-PLMN-ID
decodedPlmn = self.DecodePLMN(plmn=plmn)
mcc = decodedPlmn[0]
mnc = decodedPlmn[1]
if str(mcc) != str(self.MCC) and str(mnc) != str(self.MNC):
subscriberIsRoaming = True

if subscriberIsRoaming:
self.logTool.log(service='HSS', level='debug', message=f"[diameter.py] [Answer_16777251_318] [AIA] Subscriber {imsi} is roaming", redisClient=self.redisMessaging)
subscriberRoamingAllowed = self.validateSubscriberRoaming(subscriber=subscriber_details, mcc=mcc, mnc=mnc)

if not subscriberRoamingAllowed:
avp = ''
session_id = self.get_avp_data(avps, 263)[0] #Get Session-ID
avp += self.generate_avp(263, 40, session_id) #Session-ID AVP set
avp += self.generate_avp(264, 40, self.OriginHost) #Origin Host
avp += self.generate_avp(296, 40, self.OriginRealm) #Origin Realm

#Experimental Result AVP(Parent AVP for Roaming Failure)
avp_experimental_result = ''
avp_experimental_result += self.generate_vendor_avp(266, 40, 10415, '') #AVP Vendor ID
avp_experimental_result += self.generate_avp(298, 40, self.int_to_hex(5004, 4)) #AVP Experimental-Result-Code: DIAMETER_ERROR_ROAMING_NOT_ALLOWED (5004)
avp += self.generate_avp(297, 40, avp_experimental_result) #AVP Experimental-Result(297)

avp += self.generate_avp(277, 40, "00000001") #Auth-Session-State
avp += self.generate_avp(260, 40, "000001024000000c" + format(int(16777251),"x").zfill(8) + "0000010a4000000c000028af") #Vendor-Specific-Application-ID (S6a)
response = self.generate_diameter_packet("01", "40", 318, 16777251, packet_vars['hop-by-hop-identifier'], packet_vars['end-to-end-identifier'], avp) #Generate Diameter packet
return response

self.logTool.log(service='HSS', level='debug', message=f"[diameter.py] [Answer_16777251_318] [AIA] Subscriber {imsi} passed roaming validation for {decodedPlmn}", redisClient=self.redisMessaging)

except Exception as e:
self.logTool.log(service='HSS', level='error', message=f"[diameter.py] [Answer_16777251_318] [AIA] Error when validating subscriber roaming: {traceback.format_exc()}", redisClient=self.redisMessaging)

#Store MME Location into Database
OriginHost = self.get_avp_data(avps, 264)[0] #Get OriginHost from AVP
OriginHost = binascii.unhexlify(OriginHost).decode('utf-8') #Format it
Expand All @@ -1231,7 +1326,7 @@ def Answer_16777251_316(self, packet_vars, avps):
except: #If we don't have a record-route set, we'll send the response to the OriginHost
remote_peer = OriginHost
remote_peer = remote_peer + ";" + str(self.config['hss']['OriginHost'])
self.logTool.log(service='HSS', level='debug', message="[diameter.py] [Answer_16777251_316] [ULR] Remote Peer is " + str(remote_peer), redisClient=self.redisMessaging)
self.logTool.log(service='HSS', level='debug', message="[diameter.py] [Answer_16777251_316] [ULA] Remote Peer is " + str(remote_peer), redisClient=self.redisMessaging)

self.database.Update_Serving_MME(imsi=imsi, serving_mme=OriginHost, serving_mme_peer=remote_peer, serving_mme_realm=OriginRealm)

Expand Down Expand Up @@ -1462,7 +1557,41 @@ def Answer_16777251_318(self, packet_vars, avps):
message = template.format(type(ex).__name__, ex.args)
raise

try:
decodedPlmn = self.DecodePLMN(plmn=plmn)
mcc = decodedPlmn[0]
mnc = decodedPlmn[1]
if str(mcc) != str(self.MCC) and str(mnc) != str(self.MNC):
subscriberIsRoaming = True

if subscriberIsRoaming:
self.logTool.log(service='HSS', level='debug', message=f"[diameter.py] [Answer_16777251_318] [AIA] Subscriber {imsi} is roaming", redisClient=self.redisMessaging)
subscriberRoamingAllowed = self.validateSubscriberRoaming(subscriber=subscriber_details, mcc=mcc, mnc=mnc)

if not subscriberRoamingAllowed:
avp = ''
session_id = self.get_avp_data(avps, 263)[0] #Get Session-ID
avp += self.generate_avp(263, 40, session_id) #Session-ID AVP set
avp += self.generate_avp(264, 40, self.OriginHost) #Origin Host
avp += self.generate_avp(296, 40, self.OriginRealm) #Origin Realm

#Experimental Result AVP(Parent AVP for Roaming Failure)
avp_experimental_result = ''
avp_experimental_result += self.generate_vendor_avp(266, 40, 10415, '') #AVP Vendor ID
avp_experimental_result += self.generate_avp(298, 40, self.int_to_hex(5004, 4)) #AVP Experimental-Result-Code: DIAMETER_ERROR_ROAMING_NOT_ALLOWED (5004)
avp += self.generate_avp(297, 40, avp_experimental_result) #AVP Experimental-Result(297)

avp += self.generate_avp(277, 40, "00000001") #Auth-Session-State
avp += self.generate_avp(260, 40, "000001024000000c" + format(int(16777251),"x").zfill(8) + "0000010a4000000c000028af") #Vendor-Specific-Application-ID (S6a)
response = self.generate_diameter_packet("01", "40", 318, 16777251, packet_vars['hop-by-hop-identifier'], packet_vars['end-to-end-identifier'], avp) #Generate Diameter packet
return response

self.logTool.log(service='HSS', level='debug', message=f"[diameter.py] [Answer_16777251_318] [AIA] Subscriber {imsi} passed roaming validation for {decodedPlmn}", redisClient=self.redisMessaging)

except Exception as e:
self.logTool.log(service='HSS', level='error', message=f"[diameter.py] [Answer_16777251_318] [AIA] Error when validating subscriber roaming: {traceback.format_exc()}", redisClient=self.redisMessaging)


try:
requested_vectors = 1
EUTRAN_Authentication_Info = self.get_avp_data(avps, 1408)
Expand Down
Loading

0 comments on commit fb24498

Please sign in to comment.