Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions lib/gsup/controller/isr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PyHSS GSUP Insert Subscriber Data Request Controller
# Copyright 2025 Lennart Rosam <hello@takuto.de>
# Copyright 2025 Alexander Couzens <lynxis@fe80.eu>
# Copyright 2025-2026 Lennart Rosam <hello@takuto.de>
# Copyright 2025-2026 Alexander Couzens <lynxis@fe80.eu>
# SPDX-License-Identifier: AGPL-3.0-or-later
from enum import IntEnum
from typing import Dict, Callable, Awaitable, Optional
Expand All @@ -12,7 +12,8 @@
from gsup.controller.abstract_controller import GsupController
from gsup.controller.abstract_transaction import AbstractTransaction
from gsup.controller.ulr import ULRTransaction
from gsup.protocol.ipa_peer import IPAPeer
from gsup.protocol.gsup_msg import GsupMessageUtil
from gsup.protocol.ipa_peer import IPAPeer, IPAPeerRole
from logtool import LogTool


Expand All @@ -22,7 +23,7 @@ class __TransactionState(IntEnum):
ISD_REQUEST_SENT = 1
END_STATE_ISR_RECEIVED = 2

def __init__(self, subscriber_info: SubscriberInfo, peer: IPAPeer, cn_domain: str, callback_send_response: Callable[[IPAPeer, GsupMessage], Awaitable[None]]):
def __init__(self, subscriber_info: SubscriberInfo, peer: IPAPeer, cn_domain: str, callback_send_response: Callable[[IPAPeer, GsupMessage], Awaitable[None]], logger: LogTool):
super().__init__()
self.__ipa_peer = peer
self.__subscriber_info = subscriber_info
Expand All @@ -31,6 +32,7 @@ def __init__(self, subscriber_info: SubscriberInfo, peer: IPAPeer, cn_domain: st

self._validate_cn_domain(cn_domain)
self.__cn_domain = cn_domain
self.__logger = logger

async def begin_invoke(self):
if self.__state != self.__TransactionState.BEGIN_STATE_INITIAL:
Expand All @@ -44,8 +46,8 @@ async def continue_invoke(self, message: GsupMessage):
if self.__state != self.__TransactionState.ISD_REQUEST_SENT:
raise ValueError("ISD Transaction not in ISD_REQUEST_SENT state")

if message.msg_type != MsgType.INSERT_DATA_RESULT:
raise ValueError(f"ISD transaction was not successful. Got: {message.msg_type}")
if message.msg_type not in [MsgType.INSERT_DATA_RESULT, MsgType.INSERT_DATA_ERROR]:
self.__logger.log(service='GSUP', level='WARN', message=f"Received unexpected GSUP message type {message.msg_type.name} in ISD Transaction from peer {self.__ipa_peer.name}. Expected INSERT_DATA_RESULT or INSERT_DATA_ERROR.")

self.__state = self.__TransactionState.END_STATE_ISR_RECEIVED

Expand All @@ -56,7 +58,7 @@ def is_finished(self):
return self.__state == self.__TransactionState.END_STATE_ISR_RECEIVED

class ISRController(GsupController):
def __init__(self, logger: LogTool, database: Database, ulr_transactions: Dict[str, ULRTransaction], isd_transactions: Dict[str, ISDTransaction], all_peers: Dict[str, IPAPeer]):
def __init__(self, logger: LogTool, database: Database, ulr_transactions: Dict[tuple[str, str], ULRTransaction], isd_transactions: Dict[tuple[str, str], ISDTransaction], all_peers: Dict[str, IPAPeer]):
super().__init__(logger, database)
self.__ulr_transactions = ulr_transactions
self.__isd_transactions = isd_transactions
Expand All @@ -65,34 +67,38 @@ def __init__(self, logger: LogTool, database: Database, ulr_transactions: Dict[s
async def handle_message(self, peer: IPAPeer, message: GsupMessage):
transaction = self.__find_transaction_for_imsi(message, peer)
if transaction.is_finished():
raise ValueError(f"ULR Transaction for peer {peer.name} is already finished")
raise ValueError(f"ISD Transaction for peer {peer.name} is already finished")

await transaction.continue_invoke(message)

def __find_transaction_for_imsi(self, message: GsupMessage, peer: IPAPeer) -> AbstractTransaction:
if peer.name in self.__ulr_transactions:
return self.__ulr_transactions[peer.name]
if peer.name in self.__isd_transactions:
return self.__isd_transactions[peer.name]
raise ValueError(f"No transaction found for peer {peer.name} during message {message.msg_type}")
imsi = GsupMessageUtil.get_first_ie_by_name('imsi', message.to_dict())
if imsi is None:
raise ValueError(f"Missing IMSI in GSUP message from {peer}. Cannot continue ISR handling.")
if (peer.name, imsi) in self.__ulr_transactions:
return self.__ulr_transactions[(peer.name, imsi)]
if (peer.name, imsi) in self.__isd_transactions:
return self.__isd_transactions[(peer.name, imsi)]
raise ValueError(f"No transaction found for peer {peer.name} + IMSI {imsi} during message {message.msg_type}")

async def handle_subscriber_update(self, subscriber_info: SubscriberInfo):
for location, domain in [
(subscriber_info.location_info_2g.msc, 'cs'),
(subscriber_info.location_info_2g.vlr, 'cs'),
(subscriber_info.location_info_2g.sgsn, 'ps'),
for location, domain, role in [
(subscriber_info.location_info_2g.msc, 'cs', IPAPeerRole.MSC),
(subscriber_info.location_info_2g.vlr, 'cs', IPAPeerRole.MSC),
(subscriber_info.location_info_2g.sgsn, 'ps', IPAPeerRole.SGSN),
]:
peer = self.__find_ipa_peer_by_id(location)
imsi = subscriber_info.imsi
peer = self.__find_ipa_peer_by_id(location, role)
if peer is not None and peer.name not in self.__isd_transactions:
isd_transaction = ISDTransaction(subscriber_info, peer, domain, self._send_gsup_response)
self.__isd_transactions[peer.name] = isd_transaction
isd_transaction = ISDTransaction(subscriber_info, peer, domain, self._send_gsup_response, self._logger)
self.__isd_transactions[(peer.name, imsi)] = isd_transaction
await isd_transaction.begin_invoke()


def __find_ipa_peer_by_id(self, peer_id: Optional[str]) -> Optional[IPAPeer]:
def __find_ipa_peer_by_id(self, peer_id: Optional[str], role: IPAPeerRole) -> Optional[IPAPeer]:
if peer_id is None:
return None
for peer in self.__all_peers.values():
if peer.primary_id == peer_id:
if peer.primary_id == peer_id and peer.role == role:
return peer
return None
8 changes: 4 additions & 4 deletions lib/gsup/controller/ulr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PyHSS GSUP Update Location Request Controller
# Copyright 2025 Lennart Rosam <hello@takuto.de>
# Copyright 2025 Alexander Couzens <lynxis@fe80.eu>
# Copyright 2025-2026 Lennart Rosam <hello@takuto.de>
# Copyright 2025-2026 Alexander Couzens <lynxis@fe80.eu>
# SPDX-License-Identifier: AGPL-3.0-or-later
import traceback
from enum import IntEnum
Expand Down Expand Up @@ -100,7 +100,7 @@ async def __send_cancel_location_request(self):


class ULRController(GsupController):
def __init__(self, logger: LogTool, database: Database, ulr_transactions: Dict[str, ULRTransaction], all_peers: Dict[str, IPAPeer]):
def __init__(self, logger: LogTool, database: Database, ulr_transactions: Dict[tuple[str, str], ULRTransaction], all_peers: Dict[str, IPAPeer]):
super().__init__(logger, database)
self.__ulr_transactions = ulr_transactions
self.__all_ipa_peers = all_peers
Expand Down Expand Up @@ -159,7 +159,7 @@ async def handle_message(self, peer: IPAPeer, message: GsupMessage):
raise ULRError(f"RAT {rat_type_to_check.value} not allowed for subscriber {imsi}", GMMCause.NO_SUIT_CELL_IN_LA)

transaction = ULRTransaction(peer, message, self._send_gsup_response, self.__update_subscriber, subscriber_info)
self.__ulr_transactions[peer.name] = transaction
self.__ulr_transactions[(peer.name, imsi)] = transaction
await transaction.begin_invoke()
except ULRError as e:
await self._logger.logAsync(service='GSUP', level='WARN', message=e.message)
Expand Down
20 changes: 10 additions & 10 deletions lib/gsup/request_dispatcher.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PyHSS GSUP Request dispatcher
# Copyright 2025 Lennart Rosam <hello@takuto.de>
# Copyright 2025 Alexander Couzens <lynxis@fe80.eu>
# Copyright 2025-2026 Lennart Rosam <hello@takuto.de>
# Copyright 2025-2026 Alexander Couzens <lynxis@fe80.eu>
# SPDX-License-Identifier: AGPL-3.0-or-later
from typing import Dict

Expand All @@ -22,8 +22,8 @@

class GsupRequestDispatcher:
def __init__(self, logger: LogTool, database: Database, all_peers: Dict[str, IPAPeer]):
self.__ulr_transactions: Dict[str, ULRTransaction] = dict()
self.__isd_transactions: Dict[str, ISDTransaction] = dict()
self.__ulr_transactions: Dict[tuple[str, str], ULRTransaction] = dict()
self.__isd_transactions: Dict[tuple[str, str], ISDTransaction] = dict()
self.logger = logger
self.database = database
self.__all_peers = all_peers
Expand All @@ -42,13 +42,13 @@ def __init__(self, logger: LogTool, database: Database, all_peers: Dict[str, IPA

async def dispatch(self, peer: IPAPeer, request: GsupMessage):
# clean up old transactions
ulr_to_remove = [peer_name for peer_name, trx in self.__ulr_transactions.items() if trx.is_finished()]
for peer_name in ulr_to_remove:
del self.__ulr_transactions[peer_name]
ulr_to_remove = [(peer_name, imsi) for (peer_name, imsi), trx in self.__ulr_transactions.items() if trx.is_finished()]
for peer_name, imsi in ulr_to_remove:
del self.__ulr_transactions[(peer_name, imsi)]

isd_to_remove = [peer_name for peer_name, trx in self.__isd_transactions.items() if trx.is_finished()]
for peer_name in isd_to_remove:
del self.__isd_transactions[peer_name]
isd_to_remove = [(peer_name, imsi) for (peer_name, imsi), trx in self.__isd_transactions.items() if trx.is_finished()]
for peer_name, imsi in isd_to_remove:
del self.__isd_transactions[(peer_name, imsi)]

if request.msg_type in self.controller_mapping:
await self.controller_mapping[request.msg_type].handle_message(peer, request)
Expand Down
21 changes: 11 additions & 10 deletions services/apiService.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Copyright 2022-2025 Nick <nick@nickvsnetworking.com>
# Copyright 2023-2025 David Kneipp <david@davidkneipp.com>
# Copyright 2025 sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
# Copyright 2025 Lennart Rosam <hello@takuto.de>
# Copyright 2025-2026 Lennart Rosam <hello@takuto.de>
# Copyright 2025-2026 Alexander Couzens <lynxis@fe80.eu>
# SPDX-License-Identifier: AGPL-3.0-or-later
import sys
import json
Expand Down Expand Up @@ -567,27 +568,27 @@ def patch(self, subscriber_id):
data = databaseClient.UpdateObj(SUBSCRIBER, json_data, subscriber_id, False, operation_id)

#If the subscriber is enabled, trigger an ISD in 2G
if 'enabled' in json_data and json_data['enabled'] == True:
update_event = databaseClient.Get_Gsup_SubscriberInfo(json_data['imsi'])
if 'enabled' in data and data['enabled'] == True:
update_event = databaseClient.Get_Gsup_SubscriberInfo(data['imsi'])
redisMessaging.sendMessage('subscriber_update', update_event.model_dump_json())

#If the "enabled" flag on the subscriber is now disabled, trigger a CLR
if 'enabled' in json_data and json_data['enabled'] == False:
if 'enabled' in data and data['enabled'] == False:
print("Subscriber is now disabled, checking to see if we need to trigger a CLR")
#See if we have a serving MME set
try:
assert(json_data['serving_mme'])
assert(data['serving_mme'])
print("Serving MME set - Sending CLR")

diameterClient.sendDiameterRequest(
requestType='CLR',
hostname=json_data['serving_mme'],
imsi=json_data['imsi'],
DestinationHost=json_data['serving_mme'],
DestinationRealm=json_data['serving_mme_realm'],
hostname=data['serving_mme'],
imsi=data['imsi'],
DestinationHost=data['serving_mme'],
DestinationRealm=data['serving_mme_realm'],
CancellationType=1
)
print("Sent CLR via Peer " + str(json_data['serving_mme']))
print("Sent CLR via Peer " + str(data['serving_mme']))
except:
print("No serving MME set - Not sending CLR")
return data, 200
Expand Down