1502 lines
58 KiB
Python
1502 lines
58 KiB
Python
# -*- coding: UTF-8 -*-
|
|
#/**
|
|
# * Software Name : pycrate
|
|
# * Version : 0.4
|
|
# *
|
|
# * Copyright 2017. Benoit Michau. ANSSI.
|
|
# * Copyright 2020. Benoit Michau. P1Sec.
|
|
# *
|
|
# * This library is free software; you can redistribute it and/or
|
|
# * modify it under the terms of the GNU Lesser General Public
|
|
# * License as published by the Free Software Foundation; either
|
|
# * version 2.1 of the License, or (at your option) any later version.
|
|
# *
|
|
# * This library 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
|
|
# * Lesser General Public License for more details.
|
|
# *
|
|
# * You should have received a copy of the GNU Lesser General Public
|
|
# * License along with this library; if not, write to the Free Software
|
|
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
# * MA 02110-1301 USA
|
|
# *
|
|
# *--------------------------------------------------------
|
|
# * File Name : pycrate_corenet/Server.py
|
|
# * Created : 2017-06-28
|
|
# * Authors : Benoit Michau
|
|
# *--------------------------------------------------------
|
|
#*/
|
|
|
|
#------------------------------------------------------------------------------#
|
|
# This is the main corenet server
|
|
#
|
|
# It serves connection to:
|
|
# - Home-NodeB over HNBAP and RUA / RANAP
|
|
# - eNodeB and Home-eNodeB over S1AP
|
|
# - gNodeB over NGAP
|
|
#
|
|
# It handles signalling trafic for UE
|
|
# and connects them to specific service handler (SMS, GTPU, ...)
|
|
#------------------------------------------------------------------------------#
|
|
|
|
from .utils import *
|
|
from .HdlrHNB import HNBd
|
|
from .HdlrENB import ENBd
|
|
from .HdlrGNB import GNBd
|
|
from .HdlrUE import UEd
|
|
from .ServerAuC import AuC
|
|
from .ServerGTPU import ARPd, GTPUd, BLACKHOLE_LAN, BLACKHOLE_WAN
|
|
#
|
|
from .ProcCNHnbap import HNBAPErrorIndGW
|
|
from .ProcCNRua import RUAErrorInd
|
|
from .ProcCNS1ap import S1APErrorIndNonUECN
|
|
from .ProcCNNgap import NGAPErrorIndNonUECN
|
|
|
|
|
|
# to log all the SCTP socket send() / recv() calls
|
|
DEBUG_SK = False
|
|
|
|
|
|
class CorenetServer(object):
|
|
"""Complete control-plane and user-plane server to handle:
|
|
- Home-NodeB, over HNBAP and RUA / RANAP
|
|
- eNodeB, over S1AP
|
|
- gNodeB, over NGAP
|
|
And UE connecting through them for data connection (over GTP-U) and SMS
|
|
"""
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# debug and tracing level
|
|
#--------------------------------------------------------------------------#
|
|
#
|
|
# verbosity level: list of log types to display when calling self._log(logtype, msg)
|
|
DEBUG = ('ERR', 'WNG', 'INF', 'DBG')
|
|
# to log SCTP socket send() / recv() content
|
|
TRACE_SK = False
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# network server settings
|
|
#--------------------------------------------------------------------------#
|
|
#
|
|
# SCTP sockets recv() buffer length
|
|
SERVER_BUFLEN = 16384
|
|
# for extended socket buffering,
|
|
# configure /proc/sys/net/core/rmem_max and /proc/sys/net/core/wmem_max accordingly
|
|
#SERVER_BUFLEN = 1048576
|
|
#
|
|
SERVER_MAXCLI = 16
|
|
#
|
|
# HNBAP server
|
|
SERVER_HNB = {'INET' : socket.AF_INET,
|
|
'IP' : '10.1.1.1',
|
|
'port' : 29169,
|
|
'MAXCLI': SERVER_MAXCLI,
|
|
'errclo': True,
|
|
'GTPU' : '10.1.1.1'}
|
|
#SERVER_HNB = {} # disabling HNB server
|
|
#
|
|
# S1AP server
|
|
SERVER_ENB = {'INET' : socket.AF_INET,
|
|
'IP' : '10.2.1.1',
|
|
'port' : 36412,
|
|
'MAXCLI': SERVER_MAXCLI,
|
|
'errclo': True,
|
|
'GTPU' : '10.2.1.1'}
|
|
#SERVER_ENB = {} # disabling S1AP server
|
|
#
|
|
# NGAP Server
|
|
SERVER_GNB = {'INET' : socket.AF_INET,
|
|
'IP' : '10.3.1.1',
|
|
'port' : 38412,
|
|
'MAXCLI': SERVER_MAXCLI,
|
|
'errclo': True,
|
|
'GTPU' : '10.3.1.1'}
|
|
#SERVER_GNB = {} # disable NGAP Server
|
|
#
|
|
# Server scheduler resolution:
|
|
# This is the timeout on the main select() loop.
|
|
SCHED_RES = 0.1
|
|
# This is the resolution (in sec) for the Server to start a thread that
|
|
# checks the list of registered UE, and checks for ongoing NAS procedures
|
|
# potentially in timeout.
|
|
# If set to 0, no check is made (so, NAS procedures can stall)
|
|
# It is useless to make it lower than the SCHED_RES.
|
|
SCHED_UE_TO = 0.5
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# corenet service handlers
|
|
#--------------------------------------------------------------------------#
|
|
# These are references to services handlers
|
|
#
|
|
# Authentication Centre
|
|
AUCd = AuC
|
|
# GTPU trafic forwarder
|
|
GTPUd = GTPUd
|
|
# SMS center
|
|
SMSd = None
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# corenet global config parameters
|
|
#--------------------------------------------------------------------------#
|
|
#
|
|
# main PLMN served
|
|
PLMN = '00101'
|
|
|
|
|
|
### AMF-related config
|
|
#
|
|
# AMF ID for each served PLMN
|
|
# PLMN (str): AMF ID 3-tuple (RegionID uint8, SetID uint10, Pointer uint6)
|
|
AMF_GUAMI = {
|
|
PLMN: (0x01, 0x001, 0x00),
|
|
}
|
|
#
|
|
# arbitrary dict of indexed slices identifiers
|
|
# S-NSSAI is at least an SST (uint8) and eventually an SD (uint24)
|
|
AMF_SNSSAI = {
|
|
0 : (0x00, ), # default S-NSSAI
|
|
1 : (0x01, ),
|
|
2 : (0x02, ),
|
|
21 : (0x02, 0x000001),
|
|
}
|
|
# list of slice supported for each served PLMN
|
|
AMF_PLMNSupp = {
|
|
PLMN: [AMF_SNSSAI[0]],
|
|
#$plmn1: [AMF_SNSSAI[1]],
|
|
#$plmn2: [AMF_SNSSAI[2], AMF_SNSSAI[21]],
|
|
}
|
|
#
|
|
# NG connection AMF parameters
|
|
ConfigNG = {
|
|
'AMFName' : 'CorenetAMF',
|
|
'RelativeAMFCapacity': 10,
|
|
#'UERetentionInformation': 'ues-retained',
|
|
}
|
|
|
|
|
|
### IuCS, IuPS and S1 common parameters
|
|
#
|
|
# equivalent PLMNs served, used for Iu and S1 interface
|
|
# None or list of PLMNs ['30124', '763326', ...]
|
|
EQUIV_PLMN = None
|
|
#
|
|
# emergency number lists
|
|
# None or list of 2-tuple [(number_category, number), ...]
|
|
# number_category is a set of strings: 'Police', 'Ambulance', 'Fire', 'Marine', 'Mountain'
|
|
# number is a digits string
|
|
# e.g.
|
|
#CorenetServer.EMERG_NUMS = [
|
|
# ({'Police', 'Ambulance', 'Fire'}, '112112'),
|
|
# ({'Marine', 'Mountain'}, '112113')]
|
|
EMERG_NUMS = None
|
|
|
|
|
|
### MME-related config
|
|
#
|
|
# MME GroupID and Code
|
|
MME_GID = 1
|
|
MME_CODE = 1
|
|
#
|
|
# S1 connection MME parameters
|
|
ConfigS1 = {
|
|
'MMEname': 'CorenetMME',
|
|
'ServedGUMMEIs' : [
|
|
{'servedPLMNs' : [plmn_str_to_buf(PLMN)],
|
|
'servedGroupIDs': [uint_to_bytes(MME_GID, 16)],
|
|
'servedMMECs' : [uint_to_bytes(MME_CODE, 8)]}
|
|
],
|
|
'RelativeMMECapacity': 10,
|
|
'EquivPLMNList' : EQUIV_PLMN,
|
|
'EmergNumList' : EMERG_NUMS,
|
|
}
|
|
|
|
|
|
### MSC/VLR/SGSN-related config
|
|
#
|
|
# HNBAP connection GW parameters (keep it empty)
|
|
ConfigHNBAP = {}
|
|
# RUA connection GW parameters (keep it empty)
|
|
ConfigRUA = {}
|
|
# RANAP connection IuCS core parameters
|
|
ConfigIuCS = {
|
|
'EquivPLMNList': EQUIV_PLMN,
|
|
'EmergNumList' : EMERG_NUMS,
|
|
}
|
|
# RANAP connection IuPS core parameters
|
|
ConfigIuPS = {
|
|
'EquivPLMNList': EQUIV_PLMN,
|
|
'EmergNumList' : EMERG_NUMS,
|
|
}
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# HNB, ENB and GNB parameters
|
|
#--------------------------------------------------------------------------#
|
|
#
|
|
# Connection from RAN equipments:
|
|
# Home-NodeB, eNodeB and gNodeB indexed by their global ID
|
|
# (PLMN, CellId) for home-NodeB and eNodeB
|
|
# (PLMN, CellType, CellId) for gNodeB
|
|
# the RAN dict can be initialized with {(PLMN, *Cell*): None} here
|
|
# this provides a whitelist of allowed basestations.
|
|
RAN = {}
|
|
#
|
|
# Otherwise, this is a flag to allow any RAN equipment to connect the server
|
|
# in case its PLMN is in the RAN_ALLOWED_PLMN list.
|
|
# If enabled, RAN dict will be populated at runtime
|
|
# If disabled, RAN keys (PLMN, *Cell*) needs to be setup by configuration (see above)
|
|
RAN_CONNECT_ANY = True
|
|
#
|
|
# This is the list of accepted PLMN for RAN equipment connecting,
|
|
# when RAN_CONNECT_ANY is enabled
|
|
RAN_ALLOWED_PLMN = [PLMN]
|
|
#
|
|
# lookup dict to get the set of RAN ids (PLMN, CellId) that serves a given
|
|
# LAI, RAI and TAI
|
|
LAI = {}
|
|
RAI = {}
|
|
TAI = {}
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# UE parameters
|
|
#--------------------------------------------------------------------------#
|
|
#
|
|
# UE configuration parameters
|
|
ConfigUE = {
|
|
# $IMSI: {'PDN' : [($APN -str-, $PDNType -1..3-, $IPAddr -str-, ...), ...],
|
|
# 'MSISDN': $phone_num -str-,
|
|
# 'USIM' : $milenage_supported -bool-}
|
|
# PDP type: 0:PPP, 1:IPv4, 2: IPv6 /64 local if, 3: IPv4v6 (-> 1 IPv4 + 1 IPv6 local if)
|
|
# PDN type: 1:IPv4, 2:IPv6 /64 local if, 3:IPv4v6 (-> 1 IPv4 + 1 IPv6 local if)
|
|
# PDU type: TODO
|
|
'*': {'PDP' : [('*', 1, '192.168.1.199')],
|
|
'PDN' : [('*', 1, '192.168.1.199')],
|
|
'PDU' : [], # TODO
|
|
'MSISDN': '0123456789',
|
|
'USIM' : True
|
|
},
|
|
'001011000000001': {'PDP' : [('*', 3, '192.168.1.201', '0:1:0:c9'),
|
|
('corenet', 1, '192.168.1.201')],
|
|
'PDN' : [('*', 3, '192.168.1.201', '0:1:0:c9'),
|
|
('corenet', 1, '192.168.1.201')],
|
|
'PDU' : [], # TODO
|
|
'MSISDN': '100001',
|
|
'USIM' : True
|
|
},
|
|
'001011000000002': {'PDP' : [('*', 3, '192.168.1.202', '0:1:0:ca'),
|
|
('corenet', 1, '192.168.1.202')],
|
|
'PDN' : [('*', 3, '192.168.1.202', '0:1:0:ca'),
|
|
('corenet', 1, '192.168.1.202')],
|
|
'PDU' : [], # TODO
|
|
'MSISDN': '100002',
|
|
'USIM' : True
|
|
}
|
|
}
|
|
#
|
|
# Packet Data Protocol config for 2G-3G PS domain, per APN
|
|
ConfigPDP = {
|
|
'*': {
|
|
'DNS': ((1, '8.8.8.8'), # Google DNS servers
|
|
(1, '8.8.4.4'),
|
|
(2, '2001:4860:4860::8888'),
|
|
(2, '2001:4860:4860::8844')),
|
|
'MTU': (None, None),
|
|
},
|
|
'corenet': {
|
|
'DNS': ((1, '8.8.8.8'),
|
|
(1, '8.8.4.4')),
|
|
'MTU': (None, None),
|
|
},
|
|
}
|
|
#
|
|
# Packet Data Network config for EPC, per APN
|
|
ConfigPDN = {
|
|
'*': {
|
|
'QCI': 9,
|
|
'DNS': ((1, '8.8.8.8'), # Google DNS servers
|
|
(1, '8.8.4.4'),
|
|
(2, '2001:4860:4860::8888'),
|
|
(2, '2001:4860:4860::8844')),
|
|
'MTU': (None, None),
|
|
},
|
|
'corenet': {
|
|
'QCI': 9,
|
|
'DNS': ((1, '8.8.8.8'),
|
|
(1, '8.8.4.4')),
|
|
'MTU': (None, None),
|
|
},
|
|
}
|
|
#
|
|
# PDU Sessions config for 5GS, per DNN
|
|
ConfigPDU = {
|
|
'*': {
|
|
# TODO
|
|
},
|
|
'corenet': {
|
|
# TODO
|
|
},
|
|
}
|
|
#
|
|
# UE, indexed by IMSI, and their UEd handler instance
|
|
UE = {}
|
|
# UE, indexed by TMSI when the IMSI is unknown (at attachment),
|
|
# and their UEd handler instance are set in ._UEpre, created at init
|
|
#
|
|
# TMSI / P-TMSI / M-TMSI / 5G-TMSI to IMSI conversion
|
|
TMSI = {}
|
|
PTMSI = {}
|
|
MTMSI = {}
|
|
FGTMSI = {}
|
|
#
|
|
# This is a filter which enables the potential attachment of non-preconfigured
|
|
# UE to the CorenetServer
|
|
# WNG: for IMSI that are not preconfigured (no Ki in the AuC database),
|
|
# further UE-related procedure will fail because of missing crypto material.
|
|
# When an non-preconfigured UE attaches the CorenetServer, ConfigUE['*'] is
|
|
# used to provide a default config and need to be defined.
|
|
# use UE_ATTACH_FILTER = None to disable this permissive filter.
|
|
UE_ATTACH_FILTER = '^00101'
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# logging and init methods
|
|
#--------------------------------------------------------------------------#
|
|
|
|
def _log(self, logtype, msg):
|
|
"""Server logging facility
|
|
|
|
DEBUG logtype: 'ERR', 'WNG', 'INF', 'DBG'
|
|
TRACE logtype: 'TRACE_SK_[UL|DL]',
|
|
'TRACE_ASN_[HNBAP|RUA|S1AP|NGAP]_[UL|DL]',
|
|
"""
|
|
if logtype[:3] == 'TRA':
|
|
if logtype[6:8] == 'SK':
|
|
log('[TRA] [%s]\n%s%s%s'\
|
|
% (logtype[6:], TRACE_COLOR_START, hexlify(msg).decode('ascii'), TRACE_COLOR_END))
|
|
else:
|
|
log('[TRA] [%s]\n%s%s%s'\
|
|
% (logtype[6:], TRACE_COLOR_START, msg, TRACE_COLOR_END))
|
|
elif logtype in self.DEBUG:
|
|
log('[%s] %s' % (logtype, msg))
|
|
|
|
def __init__(self, serving=True, threaded=True):
|
|
# initialize the Python built-in Mersennes Twister LFSR for producing TMSI
|
|
random.seed(random.SystemRandom().randint(0, 1<<64))
|
|
# starting the server in background
|
|
self._running = False
|
|
if threaded:
|
|
self._server = threadit(self.start, serving=serving)
|
|
else:
|
|
self.start(serving=serving)
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# SCTP socket server
|
|
#--------------------------------------------------------------------------#
|
|
|
|
def start(self, serving=True):
|
|
#
|
|
if DEBUG_SK:
|
|
self._skc = []
|
|
# LUT for connected SCTP client and ENBId / HNBId
|
|
self.SCTPCli = {}
|
|
#
|
|
# start SCTP servers, bind() and listen()
|
|
self._start_server()
|
|
#
|
|
# init the dict for storing UE with unknown IMSI at attachment
|
|
self._UEpre = {}
|
|
# set the LUT for MSISDN to IMSI translation
|
|
self.MSISDN = {}
|
|
for imsi, cfgue in self.ConfigUE.items():
|
|
self.MSISDN[cfgue['MSISDN']] = imsi
|
|
# init the UE procedure cleaner holder
|
|
# (with a dummy thread, which will be overridden at runtime)
|
|
self._clean_ue_proc = threadit( lambda: 1 )
|
|
#
|
|
# clear LAI, RAI, TAI dict
|
|
self.LAI.clear()
|
|
self.RAI.clear()
|
|
self.TAI.clear()
|
|
#
|
|
# initialize GTP TEID UL counter
|
|
self._GTP_TEID_UL = randint(1, 200000)
|
|
#
|
|
# start sub-servers
|
|
if self.AUCd:
|
|
self.AUCd = self.__class__.AUCd()
|
|
if self.GTPUd:
|
|
self.GTPUd = self.__class__.GTPUd()
|
|
if self.SMSd:
|
|
self.SMSd = self.__class__.SMSd()
|
|
self.SMSd.Server = self
|
|
#
|
|
if serving:
|
|
# serve connections
|
|
self._serve()
|
|
# self._running has been set to False, main loop exited
|
|
self._log('INF', 'SCTP server stopped')
|
|
|
|
def is_running(self):
|
|
return self._running
|
|
|
|
def _start_server(self):
|
|
self.SCTPServ = []
|
|
for (cfg, attr) in ((self.SERVER_HNB, '_sk_hnb'),
|
|
(self.SERVER_ENB, '_sk_enb'),
|
|
(self.SERVER_GNB, '_sk_gnb')):
|
|
if 'INET' not in cfg or 'IP' not in cfg \
|
|
or 'port' not in cfg or 'MAXCLI' not in cfg:
|
|
setattr(self, attr, None)
|
|
continue
|
|
#
|
|
try:
|
|
sk = sctp.sctpsocket_tcp(cfg['INET'])
|
|
addr = (cfg['IP'], cfg['port'])
|
|
srv = attr[-3:].upper()
|
|
self.sctp_set_events(sk)
|
|
except Exception as err:
|
|
raise(CorenetErr('cannot create SCTP socket: %s' % err))
|
|
try:
|
|
sk.bind(addr)
|
|
except Exception as err:
|
|
raise(CorenetErr('cannot bind SCTP socket on addr %r: %s' % (addr, err)))
|
|
try:
|
|
sk.listen(cfg['MAXCLI'])
|
|
except Exception as err:
|
|
raise(CorenetErr('cannot listen to SCTP connection: {1}'.format(err)))
|
|
#
|
|
self._log('INF', 'SCTP %s server started on address %r' % (srv, addr))
|
|
setattr(self, attr, sk)
|
|
self.SCTPServ.append(sk)
|
|
#
|
|
self.SCTPServ = tuple(self.SCTPServ)
|
|
|
|
def _serve(self):
|
|
# Main server loop, using select() to read sockets, the loop:
|
|
# gets new SCTP clients,
|
|
# gets new SCTP streams for connected SCTP clients,
|
|
# and eventually timeouts running UE NAS procedures
|
|
self._running, T0 = True, time()
|
|
while self._running:
|
|
skr = []
|
|
try:
|
|
skr = select(self.SCTPServ + tuple(self.SCTPCli), (), (), self.SCHED_RES)[0]
|
|
except Exception as err:
|
|
self._log('ERR', 'select() error: %s' % err)
|
|
self._running = False
|
|
#
|
|
for sk in skr:
|
|
if sk == self._sk_gnb:
|
|
# new gNodeB SCTP client (NGSetupRequest)
|
|
self.handle_new_gnb()
|
|
elif sk == self._sk_enb:
|
|
# new eNodeB STCP client (S1SetupRequest)
|
|
self.handle_new_enb()
|
|
elif sk == self._sk_hnb:
|
|
# new Home-NodeB SCTP client (HNBRegisterRequest)
|
|
self.handle_new_hnb()
|
|
else:
|
|
# read from connected SCTP client for a new stream
|
|
# (whatever PDU)
|
|
self.handle_stream_msg(sk)
|
|
#
|
|
# clean-up potential signalling procedures in timeout
|
|
if self.SCHED_UE_TO and time() - T0 > self.SCHED_UE_TO and \
|
|
not self._clean_ue_proc.isAlive():
|
|
# select() timeout or more than `SCHED_RES' seconds since
|
|
# last timeout
|
|
self._clean_ue_proc = threadit(self.clean_ue_proc)
|
|
T0 = time()
|
|
|
|
def stop(self):
|
|
self._running = False
|
|
asn_ngap_release()
|
|
asn_s1ap_release()
|
|
asn_hnbap_release()
|
|
asn_rua_release()
|
|
asn_ranap_release()
|
|
sleep(self.SCHED_RES + 0.01)
|
|
if self._sk_hnb is not None:
|
|
self._sk_hnb.close()
|
|
if self._sk_enb is not None:
|
|
self._sk_enb.close()
|
|
if self._sk_gnb is not None:
|
|
self._sk_gnb.close()
|
|
self._clean_ue_proc.join()
|
|
#
|
|
# disconnect all RAN clients
|
|
for cli in self.SCTPCli:
|
|
cli.close()
|
|
self.RAN[self.SCTPCli[cli]].disconnect()
|
|
self.SCTPCli.clear()
|
|
#
|
|
# stop sub-servers
|
|
try:
|
|
self.AUCd.stop()
|
|
self.GTPUd.stop()
|
|
self.SMSd.stop()
|
|
except Exception:
|
|
pass
|
|
|
|
def sctp_handle_notif(self, sk, notif):
|
|
self._log('DBG', 'SCTP notification: type %i, flags %i' % (notif.type, notif.flags))
|
|
# TODO
|
|
|
|
def sctp_set_events(self, sk):
|
|
# configure the SCTP socket to receive adaptation layer and stream id
|
|
# indications in sctp_recv() notification
|
|
sk.events.data_io = True
|
|
sk.events.adaptation_layer = True
|
|
#sk.events.association = True
|
|
sk.events.flush()
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# SCTP stream handler
|
|
#--------------------------------------------------------------------------#
|
|
|
|
def _read_sk(self, sk):
|
|
# we always arrive there after a select() call,
|
|
# hence, recv() should always return straight without blocking
|
|
# TODO: loop on recv() to get the complete stream (in case of very long PDU...),
|
|
# then defragment those PDUs properly
|
|
# TODO: in case notif has only 0, specific events need to be subscribed
|
|
# to get at least ppid and stream
|
|
try:
|
|
addr, flags, buf, notif = sk.sctp_recv(self.SERVER_BUFLEN)
|
|
except TimeoutError as err:
|
|
# the client disconnected
|
|
if sk in self.SCTPCli:
|
|
self._rem_sk(sk)
|
|
return None, None
|
|
except ConnectionError as err:
|
|
# something went bad with the endpoint
|
|
self._log('ERR', 'sctp_recv() failed, err: {0}'.format(err))
|
|
if sk in self.SCTPCli:
|
|
self._rem_sk(sk)
|
|
return None, None
|
|
if DEBUG_SK:
|
|
self._skc.append( ('recv', time(), addr, flags, buf, notif) )
|
|
if not buf:
|
|
if flags & sctp.FLAG_NOTIFICATION:
|
|
# SCTP notification
|
|
self.sctp_handle_notif(sk, notif)
|
|
elif sk in self.SCTPCli:
|
|
# the client just disconnected
|
|
self._rem_sk(sk)
|
|
else:
|
|
if self.TRACE_SK:
|
|
self._log('TRACE_SK_UL', buf)
|
|
if not flags & sctp.FLAG_EOR:
|
|
self._log('WNG', 'SCTP message truncated')
|
|
# TODO: store all fragments from the peer until the next msg with FLAG_EOR
|
|
return None, None
|
|
return buf, notif
|
|
|
|
def _rem_sk(self, sk):
|
|
# close socket
|
|
sk.close()
|
|
# select RAN client
|
|
cli = self.RAN[self.SCTPCli[sk]]
|
|
if isinstance(cli, HNBd):
|
|
self._log('DBG', 'HNB %r closed connection' % (cli.ID,))
|
|
# remove from the Server location tables
|
|
if cli.Config:
|
|
self._unset_hnb_loc(cli)
|
|
elif isinstance(cli, ENBd):
|
|
self._log('DBG', 'eNB %r closed connection' % (cli.ID,))
|
|
# remove from the Server location tables
|
|
if cli.Config:
|
|
self._unset_enb_loc(cli)
|
|
elif isinstance(cli, GNBd):
|
|
self._log('DBG', 'gNB %s closed connection' % (cli.ID,))
|
|
# remove from the Server location tables
|
|
if cli.Config:
|
|
self._unset_gnb_loc(cli)
|
|
else:
|
|
assert()
|
|
# update HNB / ENB state
|
|
cli.disconnect()
|
|
# update list of clients socket, and dict of RAN clients
|
|
del self.SCTPCli[sk]
|
|
|
|
def _write_sk(self, sk, buf, ppid=0, stream=0):
|
|
if self.TRACE_SK:
|
|
self._log('TRACE_SK_DL', buf)
|
|
if ppid:
|
|
ppid = htonl(ppid)
|
|
#if stream:
|
|
# stream = htonl(stream)
|
|
ret = 0
|
|
try:
|
|
ret = sk.sctp_send(buf, ppid=ppid, stream=stream)
|
|
except Exception as err:
|
|
self._log('ERR', 'cannot send buf to SCTP client at address %r' % (sk.getpeername(), ))
|
|
if DEBUG_SK:
|
|
self._skc.append( ('send', time(), buf, ppid, stream, err) )
|
|
else:
|
|
if DEBUG_SK:
|
|
self._skc.append( ('send', time(), buf, ppid, stream) )
|
|
return ret
|
|
|
|
def handle_stream_msg(self, sk):
|
|
buf, notif = self._read_sk(sk)
|
|
if not buf:
|
|
# WNG: it may be required to handle SCTP notifications, at some point...
|
|
return
|
|
# getting SCTP ppid, stream id and eNB/HNB handler
|
|
ppid, sid, ranid = ntohl(notif.ppid), notif.stream, self.SCTPCli[sk]
|
|
ran = self.RAN[ranid]
|
|
#
|
|
if ppid == SCTP_PPID_HNBAP:
|
|
assert( isinstance(ran, HNBd) )
|
|
hnb = ran
|
|
if not asn_hnbap_acquire():
|
|
hnb._log('ERR', 'unable to acquire the HNBAP module')
|
|
return
|
|
try:
|
|
PDU_HNBAP.from_aper(buf)
|
|
except Exception:
|
|
asn_hnbap_release()
|
|
hnb._log('WNG', 'invalid HNBAP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = hnb.init_hnbap_proc(HNBAPErrorIndGW,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(buf)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
pdu_rx = PDU_HNBAP()
|
|
if hnb.TRACE_ASN_HNBAP:
|
|
hnb._log('TRACE_ASN_HNBAP_UL', PDU_HNBAP.to_asn1())
|
|
asn_hnbap_release()
|
|
if not isinstance(pdu_rx[1], dict):
|
|
# invalid PDU, undefined extension
|
|
hnb._log('WNG', 'invalid HNBAP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = hnb.init_hnbap_proc(HNBAPErrorIndGW,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(pdu_rx)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
pdu_tx = hnb.process_hnbap_pdu(pdu_rx)
|
|
for pdu in pdu_tx:
|
|
self.send_hnbap_pdu(hnb, pdu)
|
|
#
|
|
elif ppid == SCTP_PPID_RUA:
|
|
assert( isinstance(ran, HNBd) )
|
|
hnb = ran
|
|
if not asn_rua_acquire():
|
|
hnb._log('ERR', 'unable to acquire the RUA module')
|
|
return
|
|
try:
|
|
PDU_RUA.from_aper(buf)
|
|
except Exception:
|
|
asn_rua_release()
|
|
self._log('WNG', 'invalid RUA PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = hnb.init_rua_proc(RUAErrorInd,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(buf)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
pdu_rx = PDU_RUA()
|
|
if hnb.TRACE_ASN_RUA:
|
|
hnb._log('TRACE_ASN_RUA_UL', PDU_HNBAP.to_asn1())
|
|
asn_rua_release()
|
|
if not isinstance(pdu_rx[1], dict):
|
|
# invalid PDU, undefined extension
|
|
self._log('WNG', 'invalid RUA PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = hnb.init_rua_proc(RUAErrorInd,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(pdu_rx)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
pdu_tx = hnb.process_rua_pdu(pdu_rx)
|
|
for pdu in pdu_tx:
|
|
self.send_rua_pdu(hnb, pdu)
|
|
#
|
|
elif ppid == SCTP_PPID_S1AP:
|
|
assert( isinstance(ran, ENBd) )
|
|
enb = ran
|
|
if not asn_s1ap_acquire():
|
|
enb._log('ERR', 'unable to acquire the S1AP module')
|
|
return
|
|
try:
|
|
PDU_S1AP.from_aper(buf)
|
|
except Exception:
|
|
asn_s1ap_release()
|
|
enb._log('WNG', 'invalid S1AP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = enb.init_s1ap_proc(S1APErrorIndNonUECN,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(buf)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
pdu_rx = PDU_S1AP()
|
|
if enb.TRACE_ASN_S1AP:
|
|
enb._log('TRACE_ASN_S1AP_UL', PDU_S1AP.to_asn1())
|
|
asn_s1ap_release()
|
|
if not isinstance(pdu_rx[1], dict):
|
|
# invalid PDU, undefined extension
|
|
enb._log('WNG', 'invalid S1AP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = enb.init_s1ap_proc(S1APErrorIndNonUECN,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(pdu_rx)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
if sid == enb.SKSid:
|
|
# non-UE-associated signalling
|
|
pdu_tx = enb.process_s1ap_pdu(pdu_rx)
|
|
else:
|
|
# UE-associated signalling
|
|
pdu_tx = enb.process_s1ap_ue_pdu(pdu_rx, sid)
|
|
for pdu in pdu_tx:
|
|
self.send_s1ap_pdu(enb, pdu, sid)
|
|
#
|
|
elif ppid == SCTP_PPID_NGAP:
|
|
assert( isinstance(ran, GNBd) )
|
|
gnb = ran
|
|
if not asn_ngap_acquire():
|
|
gnb._log('ERR', 'unable to acquire the NGAP module')
|
|
return
|
|
try:
|
|
PDU_NGAP.from_aper(buf)
|
|
except Exception:
|
|
asn_ngap_release()
|
|
gnb._log('WNG', 'invalid NGAP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = gnb.init_ngap_proc(NGAPErrorIndNonUECN,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(buf)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
pdu_rx = PDU_NGAP()
|
|
if gnb.TRACE_ASN_NGAP:
|
|
gnb._log('TRACE_ASN_NGAP_UL', PDU_NGAP.to_asn1())
|
|
asn_ngap_release()
|
|
if not isinstance(pdu_rx[1], dict):
|
|
# invalid PDU, undefined extension
|
|
gnb._log('WNG', 'invalid NGAP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
Err = gnb.init_ngap_proc(NGAPErrorIndNonUECN,
|
|
Cause=('protocol', 'transfer-syntax-error'))
|
|
Err.recv(pdu_rx)
|
|
pdu_tx = Err.send()
|
|
else:
|
|
if sid == gnb.SKSid:
|
|
# non-UE-associated signalling
|
|
pdu_tx = gnb.process_ngap_pdu(pdu_rx)
|
|
else:
|
|
# UE-associated signalling
|
|
pdu_tx = gnb.process_ngap_ue_pdu(pdu_rx, sid)
|
|
for pdu in pdu_tx:
|
|
self.send_ngap_pdu(gnb, pdu, sid)
|
|
#
|
|
else:
|
|
self._log('ERR', 'invalid SCTP PPID, %i' % ppid)
|
|
if self.SERVER_HNB['errclo']:
|
|
self._rem_sk(sk)
|
|
return
|
|
|
|
def send_hnbap_pdu(self, hnb, pdu):
|
|
if not asn_hnbap_acquire():
|
|
hnb._log('ERR', 'unable to acquire the HNBAP module')
|
|
return
|
|
PDU_HNBAP.set_val(pdu)
|
|
if hnb.TRACE_ASN_HNBAP:
|
|
hnb._log('TRACE_ASN_HNBAP_DL', PDU_HNBAP.to_asn1())
|
|
buf = PDU_HNBAP.to_aper()
|
|
asn_hnbap_release()
|
|
return self._write_sk(hnb.SK, buf, ppid=SCTP_PPID_HNBAP)
|
|
|
|
def send_rua_pdu(self, hnb, pdu):
|
|
if not asn_rua_acquire():
|
|
hnb._log('ERR', 'unable to acquire the RUA module')
|
|
return
|
|
PDU_RUA.set_val(pdu)
|
|
if hnb.TRACE_ASN_RUA:
|
|
hnb._log('TRACE_ASN_RUA_DL', PDU_RUA.to_asn1())
|
|
buf = PDU_RUA.to_aper()
|
|
asn_rua_release()
|
|
return self._write_sk(hnb.SK, buf, ppid=SCTP_PPID_RUA)
|
|
|
|
def send_s1ap_pdu(self, enb, pdu, sid):
|
|
if not asn_s1ap_acquire():
|
|
enb._log('ERR', 'unable to acquire the S1AP module')
|
|
return
|
|
PDU_S1AP.set_val(pdu)
|
|
if enb.TRACE_ASN_S1AP:
|
|
enb._log('TRACE_ASN_S1AP_DL', PDU_S1AP.to_asn1())
|
|
buf = PDU_S1AP.to_aper()
|
|
asn_s1ap_release()
|
|
return self._write_sk(enb.SK, buf, ppid=SCTP_PPID_S1AP, stream=sid)
|
|
|
|
def send_ngap_pdu(self, gnb, pdu, sid):
|
|
if not asn_ngap_acquire():
|
|
gnb._log('ERR', 'unable to acquire the NGAP module')
|
|
return
|
|
PDU_NGAP.set_val(pdu)
|
|
if gnb.TRACE_ASN_NGAP:
|
|
gnb._log('TRACE_ASN_NGAP_DL', PDU_NGAP.to_asn1())
|
|
buf = PDU_NGAP.to_aper()
|
|
asn_ngap_release()
|
|
return self._write_sk(gnb.SK, buf, ppid=SCTP_PPID_NGAP, stream=sid)
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# eNodeB connection (4G)
|
|
#--------------------------------------------------------------------------#
|
|
|
|
def _parse_s1setup(self, pdu):
|
|
if pdu[0] != 'initiatingMessage' or pdu[1]['procedureCode'] != 17:
|
|
# not initiating / S1Setup
|
|
self._log('WNG', 'invalid S1AP PDU for setting up the eNB S1AP link')
|
|
return
|
|
#
|
|
pIEs, plmn, cellid = pdu[1]['value'][1], None, None
|
|
IEs = pIEs['protocolIEs']
|
|
if 'protocolExtensions' in pIEs:
|
|
Exts = pIEs['protocolExtensions']
|
|
else:
|
|
Exts = []
|
|
for ie in IEs:
|
|
if ie['id'] == 59:
|
|
# Global-ENB-ID
|
|
globenbid = ie['value'][1]
|
|
plmn = globenbid['pLMNidentity']
|
|
cellid = globenbid['eNB-ID'][1] # both macro / home eNB-ID are BIT STRING
|
|
break
|
|
if plmn is None or cellid is None:
|
|
self._log('WNG', 'invalid S1AP PDU for setting up the eNB S1AP link: '\
|
|
'missing PLMN and CellID')
|
|
return
|
|
# decode PLMN and CellID
|
|
try:
|
|
PLMN = plmn_buf_to_str(plmn)
|
|
CellID = cellid_bstr_to_str(cellid)
|
|
return PLMN, CellID
|
|
except Exception:
|
|
return None
|
|
|
|
def _send_s1setuprej(self, sk, cause):
|
|
IEs = [{'criticality': 'ignore',
|
|
'id': 2, # id-Cause
|
|
'value': (('S1AP-IEs', 'Cause'), cause)}]
|
|
pdu = ('unsuccessfulOutcome',
|
|
{'criticality': 'ignore',
|
|
'procedureCode': 17,
|
|
'value': (('S1AP-PDU-Contents', 'S1SetupFailure'),
|
|
{'protocolIEs' : IEs})})
|
|
if not asn_s1ap_acquire():
|
|
self._log('ERR', 'unable to acquire the S1AP module')
|
|
else:
|
|
PDU_S1AP.set_val(pdu)
|
|
if ENBd.TRACE_ASN_S1AP:
|
|
self._log('TRACE_ASN_S1AP_DL', PDU_S1AP.to_asn1())
|
|
self._write_sk(sk, PDU_S1AP.to_aper(), ppid=SCTP_PPID_S1AP, stream=0)
|
|
asn_s1ap_release()
|
|
if self.SERVER_ENB['errclo']:
|
|
sk.close()
|
|
|
|
def handle_new_enb(self):
|
|
sk, addr = self._sk_enb.accept()
|
|
self._log('DBG', 'New eNB client from address %r' % (addr, ))
|
|
#
|
|
buf, notif = self._read_sk(sk)
|
|
if not buf:
|
|
# WNG: maybe required to handle SCTP notification, at some point
|
|
return
|
|
# verifying SCTP Payload Protocol ID and setting stream ID for
|
|
# non-UE-associated trafic
|
|
ppid, sid = ntohl(notif.ppid), notif.stream
|
|
if ppid != SCTP_PPID_S1AP:
|
|
self._log('ERR', 'invalid S1AP PPID, %i' % ppid)
|
|
if self.SERVER_ENB['errclo']:
|
|
sk.close()
|
|
return
|
|
#
|
|
if not asn_s1ap_acquire():
|
|
self._log('ERR', 'unable to acquire the S1AP module')
|
|
return
|
|
try:
|
|
PDU_S1AP.from_aper(buf)
|
|
except Exception:
|
|
self._log('WNG', 'invalid S1AP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
asn_s1ap_release()
|
|
# return nothing, no need to bother
|
|
return
|
|
if ENBd.TRACE_ASN_S1AP:
|
|
self._log('TRACE_ASN_S1AP_UL', PDU_S1AP.to_asn1())
|
|
pdu_rx = PDU_S1AP()
|
|
asn_s1ap_release()
|
|
#
|
|
ENBId = self._parse_s1setup(pdu_rx)
|
|
if ENBId is None:
|
|
# send S1SetupReject
|
|
self._send_s1setuprej(sk, cause=('protocol', 'abstract-syntax-error-reject'))
|
|
return
|
|
elif ENBId not in self.RAN:
|
|
if not self.RAN_CONNECT_ANY:
|
|
self._log('ERR', 'eNB %r not allowed to connect' % (ENBId, ))
|
|
# send S1SetupReject
|
|
self._send_s1setuprej(sk, cause=('radioNetwork', 'unspecified'))
|
|
return
|
|
elif ENBId[0] not in self.RAN_ALLOWED_PLMN:
|
|
self._log('ERR', 'eNB %r not allowed to connect, bad PLMN' % (ENBId, ))
|
|
self._send_s1setuprej(sk, cause=('radioNetwork', 'unspecified'))
|
|
return
|
|
else:
|
|
# creating an entry for this eNB
|
|
enb = ENBd(self, sk, sid)
|
|
self.RAN[ENBId] = enb
|
|
else:
|
|
if self.RAN[ENBId] is None:
|
|
# eNB allowed, but not yet connected
|
|
enb = ENBd(self, sk, sid)
|
|
self.RAN[ENBId] = enb
|
|
elif not self.RAN[ENBId].is_connected():
|
|
# eNB already connected and disconnected in the past
|
|
enb = self.RAN[ENBId]
|
|
enb.__init__(self, sk, sid)
|
|
else:
|
|
# eNB already connected
|
|
self._log('ERR', 'eNB %r already connected from address %r'\
|
|
% (ENBId, self.RAN[ENBId].SK.getpeername()))
|
|
if self.SERVER_ENB['errclo']:
|
|
sk.close()
|
|
return
|
|
#
|
|
# process the initial PDU
|
|
pdu_tx = enb.process_s1ap_pdu(pdu_rx)
|
|
# keep track of the client
|
|
self.SCTPCli[sk] = ENBId
|
|
# add the enb TAI to the Server location tables
|
|
if enb.Config:
|
|
self._set_enb_loc(enb)
|
|
#
|
|
# send available PDU(s) back
|
|
if not asn_s1ap_acquire():
|
|
enb._log('ERR', 'unable to acquire the S1AP module')
|
|
return
|
|
for pdu in pdu_tx:
|
|
PDU_S1AP.set_val(pdu)
|
|
if ENBd.TRACE_ASN_S1AP:
|
|
enb._log('TRACE_ASN_S1AP_DL', PDU_S1AP.to_asn1())
|
|
self._write_sk(sk, PDU_S1AP.to_aper(), ppid=SCTP_PPID_S1AP, stream=sid)
|
|
asn_s1ap_release()
|
|
|
|
def _set_enb_loc(self, enb):
|
|
for tai in enb.Config['TAIs']:
|
|
if tai in self.TAI:
|
|
assert( enb.ID not in self.TAI[tai] )
|
|
self.TAI[tai].add( enb.ID )
|
|
else:
|
|
self.TAI[tai] = set( (enb.ID, ) )
|
|
|
|
def _unset_enb_loc(self, enb):
|
|
for tai in enb.Config['TAIs']:
|
|
try:
|
|
self.TAI[tai].remove(enb.ID)
|
|
except Exception:
|
|
self._log('ERR', 'RAN node %r not referenced into the TAI table' % (enb.ID,))
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# gNodeB connection (5G)
|
|
#--------------------------------------------------------------------------#
|
|
|
|
def _parse_ngsetup(self, pdu):
|
|
if pdu[0] != 'initiatingMessage' or pdu[1]['procedureCode'] != 21:
|
|
# not initiating / NGSetup
|
|
self._log('WNG', 'invalid NGAP PDU for setting up the gNB NGAP link')
|
|
return
|
|
#
|
|
pIEs, plmn, ranid = pdu[1]['value'][1], None, None
|
|
IEs = pIEs['protocolIEs']
|
|
if 'protocolExtensions' in pIEs:
|
|
Exts = pIEs['protocolExtensions']
|
|
else:
|
|
Exts = []
|
|
for ie in IEs:
|
|
if ie['id'] == 27:
|
|
# GlobalRANNodeID:
|
|
# PLMN,
|
|
# ID type (gNB-ID, macroNgENB-ID, shortMacroNgENB-ID, longMacroNgENB-ID or n3IWF-ID),
|
|
# ID bit-string value
|
|
ranid = globranid_to_hum(ie['value'][1])
|
|
break
|
|
if ranid is None:
|
|
self._log('WNG', 'invalid NGAP PDU for setting up the gNB NGAP link: '\
|
|
'missing PLMN and RAN-ID')
|
|
return
|
|
return ranid
|
|
|
|
def _send_ngsetuprej(self, sk, cause):
|
|
IEs = [{'criticality': 'ignore',
|
|
'id': 15, # id-Cause
|
|
'value': (('NGAP-IEs', 'Cause'), cause)}]
|
|
pdu = ('unsuccessfulOutcome',
|
|
{'criticality': 'ignore',
|
|
'procedureCode': 21,
|
|
'value': (('NGAP-PDU-Contents', 'NGSetupFailure'),
|
|
{'protocolIEs' : IEs})})
|
|
if not asn_ngap_acquire():
|
|
self._log('ERR', 'unable to acquire the NGAP module')
|
|
else:
|
|
PDU_NGAP.set_val(pdu)
|
|
if GNBd.TRACE_ASN_NGAP:
|
|
self._log('TRACE_ASN_NGAP_DL', PDU_NGAP.to_asn1())
|
|
self._write_sk(sk, PDU_NGAP.to_aper(), ppid=SCTP_PPID_NGAP, stream=0)
|
|
asn_ngap_release()
|
|
if self.SERVER_GNB['errclo']:
|
|
sk.close()
|
|
|
|
def handle_new_gnb(self):
|
|
sk, addr = self._sk_gnb.accept()
|
|
self._log('DBG', 'New gNB client from address %r' % (addr, ))
|
|
#
|
|
buf, notif = self._read_sk(sk)
|
|
if not buf:
|
|
# WNG: maybe required to handle SCTP notification, at some point
|
|
return
|
|
# verifying SCTP Payload Protocol ID and setting stream ID for
|
|
# non-UE-associated trafic
|
|
ppid, sid = ntohl(notif.ppid), notif.stream
|
|
if ppid != SCTP_PPID_NGAP:
|
|
self._log('ERR', 'invalid NGAP PPID, %i' % ppid)
|
|
if self.SERVER_GNB['errclo']:
|
|
sk.close()
|
|
return
|
|
#
|
|
if not asn_ngap_acquire():
|
|
self._log('ERR', 'unable to acquire the NGAP module')
|
|
return
|
|
try:
|
|
PDU_NGAP.from_aper(buf)
|
|
except Exception:
|
|
self._log('WNG', 'invalid NGAP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
asn_ngap_release()
|
|
# return nothing, no need to bother
|
|
return
|
|
if GNBd.TRACE_ASN_NGAP:
|
|
self._log('TRACE_ASN_NGAP_UL', PDU_NGAP.to_asn1())
|
|
pdu_rx = PDU_NGAP()
|
|
asn_ngap_release()
|
|
#
|
|
GNBId = self._parse_ngsetup(pdu_rx)
|
|
if GNBId is None:
|
|
# send NGSetupReject
|
|
self._send_ngsetuprej(sk, cause=('protocol', 'abstract-syntax-error-reject'))
|
|
return
|
|
elif GNBId not in self.RAN:
|
|
if not self.RAN_CONNECT_ANY:
|
|
self._log('ERR', 'gNB %r not allowed to connect' % (GNBId,))
|
|
# send NGSetupReject
|
|
self._send_ngsetuprej(sk, cause=('radioNetwork', 'unspecified'))
|
|
return
|
|
elif GNBId[0] not in self.RAN_ALLOWED_PLMN:
|
|
self._log('ERR', 'gNB %r not allowed to connect, bad PLMN' % (GNBId,))
|
|
self._send_ngsetuprej(sk, cause=('radioNetwork', 'unspecified'))
|
|
return
|
|
else:
|
|
# creating an entry for this gNB
|
|
gnb = GNBd(self, sk, sid)
|
|
self.RAN[GNBId] = gnb
|
|
else:
|
|
if self.RAN[GNBId] is None:
|
|
# gNB allowed, but not yet connected
|
|
gnb = GNBd(self, sk, sid)
|
|
self.RAN[GNBId] = gnb
|
|
elif not self.RAN[GNBId].is_connected():
|
|
# gNB already connected and disconnected in the past
|
|
gnb = self.RAN[GNBId]
|
|
gnb.__init__(self, sk, sid)
|
|
else:
|
|
# gNB already connected
|
|
self._log('ERR', 'gNB %r already connected from address %r'\
|
|
% (GNBId, self.RAN[GNBId].SK.getpeername()))
|
|
if self.SERVER_GNB['errclo']:
|
|
sk.close()
|
|
return
|
|
#
|
|
# process the initial PDU
|
|
pdu_tx = gnb.process_ngap_pdu(pdu_rx)
|
|
# keep track of the client
|
|
self.SCTPCli[sk] = GNBId
|
|
# add the gnb TAI to the Server location tables
|
|
if gnb.Config:
|
|
self._set_gnb_loc(gnb)
|
|
#
|
|
# send available PDU(s) back
|
|
if not asn_ngap_acquire():
|
|
gnb._log('ERR', 'unable to acquire the NGAP module')
|
|
return
|
|
for pdu in pdu_tx:
|
|
PDU_NGAP.set_val(pdu)
|
|
if GNBd.TRACE_ASN_NGAP:
|
|
gnb._log('TRACE_ASN_NGAP_DL', PDU_NGAP.to_asn1())
|
|
self._write_sk(sk, PDU_NGAP.to_aper(), ppid=SCTP_PPID_NGAP, stream=sid)
|
|
asn_ngap_release()
|
|
|
|
# in 5G, gNB are dealing with TA more or less in the same way as in 4G
|
|
_set_gnb_loc = _set_enb_loc
|
|
_unset_gnb_loc = _unset_enb_loc
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# Home-NodeB connection (3G)
|
|
#--------------------------------------------------------------------------#
|
|
|
|
def _parse_hnbregreq(self, pdu):
|
|
if pdu[0] != 'initiatingMessage' or pdu[1]['procedureCode'] != 1:
|
|
# not initiating / HNBRegisterRequest
|
|
self._log('WNG', 'invalid HNBAP PDU for registering the HNB')
|
|
return
|
|
pIEs, plmn, cellid = pdu[1]['value'][1], None, None
|
|
IEs = pIEs['protocolIEs']
|
|
if 'protocolExtensions' in pIEs:
|
|
Exts = pIEs['protocolExtensions']
|
|
else:
|
|
Exts = []
|
|
for ie in IEs:
|
|
if ie['id'] == 9:
|
|
plmn = ie['value'][1]
|
|
elif ie['id'] == 11:
|
|
cellid = ie['value'][1]
|
|
if plmn is not None and cellid is not None:
|
|
break
|
|
if plmn is None or cellid is None:
|
|
self._log('WNG', 'invalid HNBAP PDU for registering the HNB: missing PLMN and CellID')
|
|
return
|
|
# decode PLMN and CellID
|
|
try:
|
|
PLMN = plmn_buf_to_str(plmn)
|
|
CellID = cellid_bstr_to_str(cellid)
|
|
return PLMN, CellID
|
|
except Exception:
|
|
return None
|
|
|
|
def _send_hnbregrej(self, sk, cause):
|
|
IEs = [{'criticality': 'ignore',
|
|
'id': 1, # id-Cause
|
|
'value': (('HNBAP-IEs', 'Cause'), cause)}]
|
|
pdu = ('unsuccessfulOutcome',
|
|
{'criticality': 'ignore',
|
|
'procedureCode': 1,
|
|
'value': (('HNBAP-PDU-Contents', 'HNBRegisterReject'),
|
|
{'protocolIEs' : IEs})})
|
|
if not asn_hnbap_acquire():
|
|
self._log('ERR', 'unable to acquire the HNBAP module')
|
|
else:
|
|
PDU_HNBAP.set_val(pdu)
|
|
if HNBd.TRACE_ASN_HNBAP:
|
|
self._log('TRACE_ASN_HNBAP_DL', PDU_HNBAP.to_asn1())
|
|
self._write_sk(sk, PDU_HNBAP.to_aper(), ppid=SCTP_PPID_HNBAP)
|
|
asn_hnbap_release()
|
|
if self.SERVER_HNB['errclo']:
|
|
sk.close()
|
|
|
|
def handle_new_hnb(self):
|
|
sk, addr = self._sk_hnb.accept()
|
|
self._log('DBG', 'New HNB client from address %r' % (addr, ))
|
|
#
|
|
buf, notif = self._read_sk(sk)
|
|
if not buf:
|
|
# WNG: maybe required to handle SCTP notification, at some point
|
|
return
|
|
# verifying SCTP Payload Protocol ID
|
|
ppid = ntohl(notif.ppid)
|
|
if ppid != SCTP_PPID_HNBAP:
|
|
self._log('ERR', 'invalid HNBAP PPID, %i' % ppid)
|
|
if self.SERVER_HNB['errclo']:
|
|
sk.close()
|
|
return
|
|
#
|
|
if not asn_hnbap_acquire():
|
|
self._log('ERR', 'unable to acquire the HNBAP module')
|
|
return
|
|
try:
|
|
PDU_HNBAP.from_aper(buf)
|
|
except Exception:
|
|
self._log('WNG', 'invalid HNBAP PDU transfer-syntax: %s'\
|
|
% hexlify(buf).decode('ascii'))
|
|
asn_hnbap_release()
|
|
# return nothing, no need to bother
|
|
return
|
|
if HNBd.TRACE_ASN_HNBAP:
|
|
self._log('TRACE_ASN_HNBAP_UL', PDU_HNBAP.to_asn1())
|
|
pdu = PDU_HNBAP()
|
|
asn_hnbap_release()
|
|
#
|
|
# ensure we have a HNBRegisterRequest with PLMN and CellID provided
|
|
HNBId = self._parse_hnbregreq(pdu)
|
|
if HNBId is None:
|
|
# send HNBRegisterReject
|
|
self._send_hnbregrej(sk, cause=('protocol', 'abstract-syntax-error-reject'))
|
|
return
|
|
elif HNBId not in self.RAN:
|
|
if not self.RAN_CONNECT_ANY:
|
|
self._log('ERR', 'HNB %r not allowed to connect' % (HNBId,))
|
|
# send HNBRegisterReject
|
|
self._send_hnbregrej(sk, cause=('radioNetwork', 'unauthorised-HNB'))
|
|
return
|
|
elif HNBId[0] not in self.RAN_ALLOWED_PLMN:
|
|
self._log('ERR', 'HNB %r not allowed to connect, bad PLMN' % (HNBId,))
|
|
self._send_hnbregrej(sk, cause=('radioNetwork', 'unauthorised-HNB'))
|
|
return
|
|
else:
|
|
# creating an entry for this HNB
|
|
hnb = HNBd(self, sk)
|
|
self.RAN[HNBId] = hnb
|
|
else:
|
|
if self.RAN[HNBId] is None:
|
|
# HNB allowed, but not yet connected
|
|
hnb = HNBd(self, sk)
|
|
self.RAN[HNBId] = hnb
|
|
elif not self.RAN[HNBId].is_connected():
|
|
# HNB already connected and disconnected in the past
|
|
hnb = self.RAN[HNBId]
|
|
hnb.__init__(self, sk)
|
|
else:
|
|
# HNB already connected
|
|
self._log('ERR', 'HNB %r already connected from address %r'\
|
|
% (HNBId, self.RAN[HNBId].SK.getpeername()))
|
|
if self.SERVER_HNB['errclo']:
|
|
sk.close()
|
|
return
|
|
#
|
|
# process the initial PDU
|
|
ret = hnb.process_hnbap_pdu(pdu)
|
|
# keep track of the client
|
|
self.SCTPCli[sk] = HNBId
|
|
# add the hnb LAI / RAI to the Server location tables
|
|
if hnb.Config:
|
|
self._set_hnb_loc(hnb)
|
|
#
|
|
# send available PDU(s) back
|
|
if not asn_hnbap_acquire():
|
|
hnb._log('ERR', 'unable to acquire the HNBAP module')
|
|
return
|
|
for retpdu in ret:
|
|
PDU_HNBAP.set_val(retpdu)
|
|
if HNBd.TRACE_ASN_HNBAP:
|
|
hnb._log('TRACE_ASN_HNBAP_DL', PDU_HNBAP.to_asn1())
|
|
self._write_sk(sk, PDU_HNBAP.to_aper(), ppid=SCTP_PPID_HNBAP)
|
|
asn_hnbap_release()
|
|
|
|
def _set_hnb_loc(self, hnb):
|
|
lai = (hnb.Config['PLMNidentity'], hnb.Config['LAC'])
|
|
rai = lai + (hnb.Config['RAC'], )
|
|
if lai in self.LAI:
|
|
assert( hnb.ID not in self.LAI[lai] )
|
|
self.LAI[lai].add( hnb.ID )
|
|
else:
|
|
self.LAI[lai] = set( (hnb.ID, ) )
|
|
if rai in self.RAI:
|
|
assert( hnb.ID not in self.RAI[rai] )
|
|
self.RAI[rai].add( hnb.ID )
|
|
else:
|
|
self.RAI[rai] = set( (hnb.ID, ) )
|
|
|
|
def _unset_hnb_loc(self, hnb):
|
|
lai = (hnb.Config['PLMNidentity'], hnb.Config['LAC'])
|
|
rai = lai + (hnb.Config['RAC'], )
|
|
try:
|
|
self.LAI[lai].remove(hnb.ID)
|
|
except Exception:
|
|
self._log('ERR', 'HNB not referenced into the LAI table')
|
|
try:
|
|
self.RAI[rai].remove(hnb.ID)
|
|
except Exception:
|
|
self._log('ERR', 'HNB not referenced into the RAI table')
|
|
|
|
#--------------------------------------------------------------------------#
|
|
# UE handler
|
|
#--------------------------------------------------------------------------#
|
|
|
|
def get_ued(self, **kw):
|
|
"""return a UEd instance or None, according to the UE identity provided
|
|
|
|
kw: imsi (digit-str), tmsi (uint32), ptmsi (uint32), mtmsi (uint32) or fgtmsi (uint32)
|
|
|
|
If an imsi is provided, returns the UEd instance in case the IMSI is allowed
|
|
If a tmsi or ptmsi is provided, returns
|
|
the UEd instance corresponding to this TMSI if already available
|
|
a new UEd instance which will take care of requesting the IMSI
|
|
"""
|
|
if 'imsi' in kw:
|
|
imsi = kw['imsi']
|
|
if imsi in self.UE:
|
|
# UEd already available
|
|
return self.UE[imsi]
|
|
elif imsi in self.ConfigUE:
|
|
# UEd has to be instantiated
|
|
self.UE[imsi] = UEd(self, imsi, config=self.ConfigUE[imsi])
|
|
return self.UE[imsi]
|
|
elif self.UE_ATTACH_FILTER and re.match(self.UE_ATTACH_FILTER, imsi) and \
|
|
'*' in self.ConfigUE:
|
|
self._log('WNG', 'attaching a UE without dedicated configuration, IMSI %s' % imsi)
|
|
self.UE[imsi] = UEd(self, imsi, config=self.ConfigUE['*'])
|
|
return self.UE[imsi]
|
|
else:
|
|
self._log('INF', 'IMSI not allowed, %s' % imsi)
|
|
elif 'tmsi' in kw:
|
|
tmsi = kw['tmsi']
|
|
if tmsi in self.TMSI:
|
|
return self.UE[self.TMSI[tmsi]]
|
|
else:
|
|
# creating a UEd instance which will request IMSI
|
|
return self.create_dummy_ue(tmsi=tmsi)
|
|
elif 'ptmsi' in kw:
|
|
ptmsi = kw['ptmsi']
|
|
if ptmsi in self.PTMSI:
|
|
return self.UE[self.PTMSI[ptmsi]]
|
|
else:
|
|
# creating a UEd instance which will request IMSI
|
|
return self.create_dummy_ue(ptmsi=ptmsi)
|
|
elif 'mtmsi' in kw:
|
|
mtmsi = kw['mtmsi']
|
|
if mtmsi in self.MTMSI:
|
|
return self.UE[self.MTMSI[mtmsi]]
|
|
else:
|
|
# creating a UEd instance which will request IMSI
|
|
return self.create_dummy_ue(mtmsi=mtmsi)
|
|
elif 'fgtmsi' in kw:
|
|
fgtmsi = kw['fgtmsi']
|
|
if fgtmsi in self.FGTMSI:
|
|
return self.UE[self.FGTMSI[fgtmsi]]
|
|
else:
|
|
# creating a UEd instance which will request SUPI
|
|
return self.create_dummy_ue(fgtmsi=fgtmsi)
|
|
return None
|
|
|
|
def create_dummy_ue(self, **kw):
|
|
assert( len(kw) == 1 )
|
|
ued = UEd(self, '', **kw)
|
|
self._UEpre[tuple(kw.values())[0]] = ued
|
|
return ued
|
|
|
|
def is_imsi_allowed(self, imsi):
|
|
if imsi in self.ConfigUE:
|
|
# preconfigured UE
|
|
return True
|
|
elif re.match(self.UE_ATTACH_FILTER, imsi) and '*' in self.ConfigUE:
|
|
# non-preconfigured UE
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def is_imei_allowed(self, imei):
|
|
# to be implemented
|
|
return True
|
|
|
|
def is_imeisv_allowed(self, imeisv):
|
|
# to be implemented
|
|
return True
|
|
|
|
def clean_ue_proc(self):
|
|
#self._log('DBG', 'clean_ue_proc()')
|
|
# go over all UE and abort() NAS signalling procedures in timeout
|
|
T = time()
|
|
for ue in self.UE.values():
|
|
|
|
if ue.IuCS is not None:
|
|
|
|
if ue.IuCS.MM.Proc:
|
|
for P in ue.IuCS.MM.Proc:
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.IuCS.CC.Proc:
|
|
for P in ue.IuCS.CC.Proc.values():
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.IuCS.SMS.Proc:
|
|
for P in tuple(ue.IuCS.SMS.Proc.values()):
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.IuCS.SS.Proc:
|
|
for P in ue.IuCS.SS.Proc.values():
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
|
|
if ue.IuPS is not None:
|
|
|
|
if ue.IuPS.GMM.Proc:
|
|
for P in ue.IuPS.GMM.Proc:
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.IuPS.SM.Proc:
|
|
for P in tuple(ue.IuPS.SM.Proc.values()):
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.S1 is not None:
|
|
|
|
if ue.S1.EMM.Proc:
|
|
for P in ue.S1.EMM.Proc:
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.S1.ESM.Proc:
|
|
for P in tuple(ue.S1.ESM.Proc.values()):
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.S1.SMS.Proc:
|
|
for P in tuple(ue.S1.SMS.Proc.values()):
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.NG is not None:
|
|
|
|
if ue.NG.FGMM.Proc:
|
|
for P in ue.NG.FGMM.Proc:
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.NG.FGSM.Proc:
|
|
for P in tuple(ue.NG.FGSM.Proc.values()):
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
if ue.NG.SMS.Proc:
|
|
for P in tuple(ue.NG.SMS.Proc.values()):
|
|
if hasattr(P, 'TimerStop') and T > P.TimerStop:
|
|
P._log('WNG', 'timeout: aborting')
|
|
P.abort()
|
|
|
|
def get_gtp_teid(self):
|
|
if self._GTP_TEID_UL > 4294967294:
|
|
self._GTP_TEID_UL = randint(1, 200000)
|
|
self._GTP_TEID_UL += 1
|
|
return self._GTP_TEID_UL
|
|
|
|
def send_smsrp(self, msisdn, rp_msg):
|
|
if msisdn not in self.MSISDN:
|
|
# unknown msisdn
|
|
self.SMSd.discard_rp(rp_msg, msisdn)
|
|
return
|
|
imsi = self.MSISDN[msisdn]
|
|
if imsi not in self.UE:
|
|
# UE not attached
|
|
self.SMSd.discard_rp(rp_msg, msisdn)
|
|
return
|
|
ue = self.UE[imsi]
|
|
return ue.smsrp_downlink(rp_msg)
|
|
|