def test_empty_value(self): """ Tests we can initialize an AVP with no value """ avp_val = avp.AVP(0) self.assertEqual(avp_val.value, None) self.assertEqual(avp_val.payload, None) # We can then set its value avp_val.value = b'' self.assertEqual(avp_val.value, b'') self.assertEqual(avp_val.payload, b'') # And unset it again avp_val.value = None self.assertEqual(avp_val.value, None) self.assertEqual(avp_val.payload, None)
def _auth_req(user_name, visited_plmn_id, num_request_vectors, immediate_response_preferred, resync_info): msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = s6a.S6AApplicationCommands.AUTHENTICATION_INFORMATION msg.header.request = True msg.append_avp( avp.AVP('Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a')) msg.append_avp(avp.AVP('Auth-Session-State', 1)) msg.append_avp(avp.AVP('User-Name', user_name)) msg.append_avp(avp.AVP('Visited-PLMN-Id', visited_plmn_id)) msg.append_avp( avp.AVP('Requested-EUTRAN-Authentication-Info', [ avp.AVP('Number-Of-Requested-Vectors', num_request_vectors), avp.AVP('Immediate-Response-Preferred', 1 if immediate_response_preferred else 0), avp.AVP('Re-Synchronization-Info', resync_info), ])) return msg
def _update_location_req(user_name, visited_plmn_id, ulr_flags): msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = s6a.S6AApplicationCommands.UPDATE_LOCATION msg.header.request = True msg.append_avp( avp.AVP('Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a')) msg.append_avp(avp.AVP('Auth-Session-State', 1)) msg.append_avp(avp.AVP('User-Name', user_name)) msg.append_avp(avp.AVP('Visited-PLMN-Id', visited_plmn_id)) msg.append_avp(avp.AVP('ULR-Flags', ulr_flags)) msg.append_avp(avp.AVP('RAT-Type', 1004)) return msg
def test_resync(self): """ Test that we can respond to auth requests with an auth vector """ msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = s6a.S6AApplicationCommands.AUTHENTICATION_INFORMATION msg.header.request = True msg.append_avp( avp.AVP( 'Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a', ), ) msg.append_avp(avp.AVP('Auth-Session-State', 1)) msg.append_avp(avp.AVP('User-Name', '1')) msg.append_avp(avp.AVP('Visited-PLMN-Id', b'(Y')) msg.append_avp( avp.AVP( 'Requested-EUTRAN-Authentication-Info', [ avp.AVP('Number-Of-Requested-Vectors', 1), avp.AVP('Immediate-Response-Preferred', 0), avp.AVP('Re-Synchronization-Info', 30 * b'\x00'), ], ), ) # Encode request message into buffer req_buf = bytearray(msg.length) msg.encode(req_buf, 0) processor = self._server._s6a_manager.lte_processor with unittest.mock.patch.object(processor, 'resync_lte_auth_seq'): self._server.data_received(req_buf) processor.resync_lte_auth_seq.assert_called_once_with( '1', 16 * b'\x00', 14 * b'\x00', )
def test_auth_answer_success(self): """ Tests that we convert gRPC AuthenticationInformation success response into Diameter AIA """ state_id = 1 user_name = '1' visited_plmn_id = b'(Y' num_request_vectors = 2 immediate_response_preferred = True resync_info = b'123456789' req = self._auth_req(user_name, visited_plmn_id, num_request_vectors, immediate_response_preferred, resync_info) # response rand = b'rand' xres = b'xres' autn = b'autn' kasme = b'kasme' auth_info = avp.AVP('Authentication-Info', [ avp.AVP('E-UTRAN-Vector', [ avp.AVP('RAND', rand), avp.AVP('XRES', xres), avp.AVP('AUTN', autn), avp.AVP('KASME', kasme) ]) ] * num_request_vectors) resp = self._server._s6a_manager._gen_response( state_id, req, avp.ResultCode.DIAMETER_SUCCESS, [auth_info]) resp_buf = bytearray(resp.length) resp.encode(resp_buf, 0) result = AuthenticationInformationAnswer( error_code=0, eutran_vectors=[ AuthenticationInformationAnswer.EUTRANVector( rand=rand, xres=xres, autn=autn, kasme=kasme) ] * num_request_vectors) result_future = unittest.mock.Mock() result_future.exception.side_effect = [None] result_future.result.side_effect = [result] self._server._s6a_manager._relay_auth_answer(state_id, req, result_future, 0) self._writes.assert_called_once_with(resp_buf) self._writes.reset_mock()
def test_avp_length(self): """ Tests we validate AVPs lengths are longer than minumum to decode and no longer than the maximum length encodable """ # Avp that has no payload isn't encodable with self.assertRaises(CodecException): avp_val = avp.AVP(0) out_buf = bytearray(avp_val.length) avp_val.encode(out_buf, 0) # Avp shorter than header with self.assertRaises(CodecException): avp.decode(b'\x00' * (avp.HEADER_LEN - 1)) # Too short with vendor bit set with self.assertRaises(CodecException): avp.decode(b'\x00\x00\x00\x00\x80\x00\x00\x00') # Max allowable length of payload avp_val = avp.UTF8StringAVP( 1, 'a' * (0x00FFFFFF - avp.HEADER_LEN), ) out_buf = bytearray(avp_val.length) avp_val.encode(out_buf, 0) self._compare_avp(avp_val, out_buf) # Avp length out of range with self.assertRaises(CodecException): avp_val = avp.UTF8StringAVP( 1, 'a' * (0x00FFFFFF - avp.HEADER_LEN + 1), ) out_buf = bytearray(avp_val.length) avp_val.encode(out_buf, 0)
def setUpClass(cls): cls.msg = message.Message() cls.msg.append_avp(avp.AVP('User-Name', 'hello')) cls.msg.append_avp(avp.AVP('User-Name', 'world')) cls.msg.append_avp(avp.AVP('Host-IP-Address', '127.0.0.1'))
def test_location_update(self): """ Test that we can respond to update location request with subscriber data """ msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = s6a.S6AApplicationCommands.UPDATE_LOCATION msg.header.request = True msg.append_avp( avp.AVP( 'Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a', ), ) msg.append_avp(avp.AVP('Auth-Session-State', 1)) msg.append_avp(avp.AVP('User-Name', '208950000000001')) msg.append_avp(avp.AVP('Visited-PLMN-Id', b'(Y')) msg.append_avp(avp.AVP('RAT-Type', 1004)) msg.append_avp(avp.AVP('ULR-Flags', 34)) # Encode request message into buffer req_buf = bytearray(msg.length) msg.encode(req_buf, 0) msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = s6a.S6AApplicationCommands.UPDATE_LOCATION msg.header.request = False msg.append_avp( avp.AVP( 'Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a', ), ) msg.append_avp(avp.AVP('ULA-Flags', 1)) msg.append_avp( avp.AVP( 'Subscription-Data', [ avp.AVP('MSISDN', b'333608050011'), avp.AVP('Access-Restriction-Data', 47), avp.AVP('Subscriber-Status', 0), avp.AVP('Network-Access-Mode', 2), avp.AVP( 'AMBR', [ avp.AVP('Max-Requested-Bandwidth-UL', 10000), avp.AVP('Max-Requested-Bandwidth-DL', 50000), ], ), avp.AVP( 'APN-Configuration-Profile', [ avp.AVP('Context-Identifier', 0), avp.AVP( 'All-APN-Configurations-Included-Indicator', 0), avp.AVP( 'APN-Configuration', [ avp.AVP('Context-Identifier', 0), avp.AVP('PDN-Type', 0), avp.AVP('Service-Selection', 'oai.ipv4'), avp.AVP( 'EPS-Subscribed-QoS-Profile', [ avp.AVP('QoS-Class-Identifier', 9), avp.AVP( 'Allocation-Retention-Priority', [ avp.AVP( 'Priority-Level', 15), avp.AVP( 'Pre-emption-Capability', 1), avp.AVP( 'Pre-emption-Vulnerability', 0), ], ), ], ), avp.AVP( 'AMBR', [ avp.AVP( 'Max-Requested-Bandwidth-UL', 10000), avp.AVP( 'Max-Requested-Bandwidth-DL', 50000), ], ), ], ), ], ), ], ), ) msg.append_avp(avp.AVP('Auth-Session-State', 1)) # Host identifiers msg.append_avp(avp.AVP('Origin-Host', self._server.host)) msg.append_avp(avp.AVP('Origin-Realm', self._server.realm)) msg.append_avp(avp.AVP('Origin-State-Id', self._server.state_id)) # Response result msg.append_avp(avp.AVP('Result-Code', avp.ResultCode.DIAMETER_SUCCESS)) # Encode response into buffer resp_buf = bytearray(msg.length) msg.encode(resp_buf, 0) self._check_reply(req_buf, resp_buf)
def test_auth_unknown_subscriber(self): """ Test that we reject auth requests if the subscriber is unknown """ msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = s6a.S6AApplicationCommands.AUTHENTICATION_INFORMATION msg.header.request = True msg.append_avp( avp.AVP( 'Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a', ), ) msg.append_avp(avp.AVP('Auth-Session-State', 1)) msg.append_avp(avp.AVP('User-Name', '3')) msg.append_avp(avp.AVP('Visited-PLMN-Id', b'(Y')) msg.append_avp( avp.AVP( 'Requested-EUTRAN-Authentication-Info', [ avp.AVP('Number-Of-Requested-Vectors', 1), avp.AVP('Immediate-Response-Preferred', 0), ], ), ) # Encode request message into buffer req_buf = bytearray(msg.length) msg.encode(req_buf, 0) msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = \ s6a.S6AApplicationCommands.AUTHENTICATION_INFORMATION msg.header.request = False msg.append_avp( avp.AVP( 'Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a', ), ) msg.append_avp(avp.AVP('Auth-Session-State', 1)) # Host identifiers msg.append_avp(avp.AVP('Origin-Host', self._server.host)) msg.append_avp(avp.AVP('Origin-Realm', self._server.realm)) msg.append_avp(avp.AVP('Origin-State-Id', self._server.state_id)) # Response result msg.append_avp( avp.AVP( 'Result-Code', avp.ResultCode.DIAMETER_ERROR_USER_UNKNOWN, ), ) # Encode response into buffer resp_buf = bytearray(msg.length) msg.encode(resp_buf, 0) self._check_reply(req_buf, resp_buf)
def test_auth_success(self): """ Test that we can respond to auth requests with an auth vector """ msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = s6a.S6AApplicationCommands.AUTHENTICATION_INFORMATION msg.header.request = True msg.append_avp( avp.AVP( 'Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a', ), ) msg.append_avp(avp.AVP('Auth-Session-State', 1)) msg.append_avp(avp.AVP('User-Name', '1')) msg.append_avp(avp.AVP('Visited-PLMN-Id', b'(Y')) msg.append_avp( avp.AVP( 'Requested-EUTRAN-Authentication-Info', [ avp.AVP('Number-Of-Requested-Vectors', 1), avp.AVP('Immediate-Response-Preferred', 0), ], ), ) # Encode request message into buffer req_buf = bytearray(msg.length) msg.encode(req_buf, 0) msg = message.Message() msg.header.application_id = s6a.S6AApplication.APP_ID msg.header.command_code = \ s6a.S6AApplicationCommands.AUTHENTICATION_INFORMATION msg.header.request = False msg.append_avp( avp.AVP( 'Session-Id', 'enb-Lenovo-Product.openair4G.eur;1475864727;1;apps6a', ), ) msg.append_avp( avp.AVP( 'Authentication-Info', [ avp.AVP( 'E-UTRAN-Vector', [ avp.AVP( 'RAND', b'\x00\x01\x02\x03\x04\x05' b'\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f', ), avp.AVP('XRES', b'\x2d\xaf\x87\x3d\x73\xf3\x10\xc6'), avp.AVP( 'AUTN', b'\x6f\xbf\xa3\x80\x1f\x57\x80' b'\x00\x7b\xde\x59\x88\x6e\x96\xe4\xfe', ), avp.AVP( 'KASME', b'\x87\x48\xc1\xc0\xa2\x82' b'\x6f\xa4\x05\xb1\xe2\x7e\xa1\x04\x43\x4a' b'\xe5\x56\xc7\x65\xe8\xf0\x61\xeb\xdb\x8a' b'\xe2\x86\xc4\x46\x16\xc2', ), ], ), ], ), ) msg.append_avp(avp.AVP('Auth-Session-State', 1)) # Host identifiers msg.append_avp(avp.AVP('Origin-Host', self._server.host)) msg.append_avp(avp.AVP('Origin-Realm', self._server.realm)) msg.append_avp(avp.AVP('Origin-State-Id', self._server.state_id)) # Response result msg.append_avp(avp.AVP('Result-Code', avp.ResultCode.DIAMETER_SUCCESS)) # Encode response into buffer resp_buf = bytearray(msg.length) msg.encode(resp_buf, 0) self._check_reply(req_buf, resp_buf)
def _relay_auth_answer(self, state_id, msg, answer_future, retries_left): user_name = msg.find_avp(*avp.resolve('User-Name')).value err = answer_future.exception() if err and retries_left > 0: # TODO: retry only on network failure and not application failures logging.info( "Auth %s Error! [%s] %s, retrying...", user_name, err.code(), err.details(), ) self._send_auth_with_retries( state_id, msg, retries_left - 1, ) return elif err: logging.warning( "Auth %s Error! [%s] %s", user_name, err.code(), err.details(), ) resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_UNABLE_TO_COMPLY, ) S6A_AUTH_FAILURE_TOTAL.labels( code=avp.ResultCode.DIAMETER_UNABLE_TO_COMPLY, ).inc() else: answer = answer_future.result() error_code = answer.error_code if answer.error_code: result_info = avp.AVP( 'Experimental-Result', [ avp.AVP('Vendor-Id', 10415), avp.AVP('Experimental-Result-Code', error_code), ], ) resp = self._gen_response( state_id, msg, error_code, [result_info], ) logging.warning( "Auth S6a %s Error! [%s]", user_name, error_code, ) S6A_AUTH_FAILURE_TOTAL.labels(code=error_code, ).inc() else: auth_info = avp.AVP( 'Authentication-Info', [ avp.AVP( 'E-UTRAN-Vector', [ avp.AVP('RAND', vector.rand), avp.AVP('XRES', vector.xres), avp.AVP('AUTN', vector.autn), avp.AVP('KASME', vector.kasme), ], ) for vector in answer.eutran_vectors ], ) resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_SUCCESS, [auth_info], ) S6A_AUTH_SUCCESS_TOTAL.inc() self.writer.send_msg(resp)
def test_location_update_resp(self): """ Test that gRPC UpdateLocation success response triggers Diameter ULA success """ state_id = 1 user_name = '1' visited_plmn_id = b'(Y' ulr_flags = 1 << 2 | 1 << 5 default_context_id = 0 total_ambr = {'ul': 10000, 'dl': 50000} all_apns_included = True req = self._update_location_req(user_name, visited_plmn_id, ulr_flags) # Encode request message into buffer req_buf = bytearray(req.length) req.encode(req_buf, 0) apns = [{ 'context_id': i, 'service_selection': 'apn.%d' % i, 'qos_profile': { 'class_id': i, 'priority_level': i, 'preemption_capability': True if i % 2 else False, 'preemption_vulnerability': False if i % 2 else True, }, 'ambr': { 'ul': 1000 * i, 'dl': 2000 * i, }, } for i in range(2)] resp_avps = [ avp.AVP('ULA-Flags', 1), avp.AVP( 'Subscription-Data', [ avp.AVP('MSISDN', b'333608050011'), avp.AVP('Access-Restriction-Data', 47), avp.AVP('Subscriber-Status', 0), avp.AVP('Network-Access-Mode', 2), avp.AVP( 'AMBR', [ avp.AVP( 'Max-Requested-Bandwidth-UL', total_ambr['ul'], ), avp.AVP( 'Max-Requested-Bandwidth-DL', total_ambr['dl'], ), ], ), avp.AVP( 'APN-Configuration-Profile', [ avp.AVP('Context-Identifier', default_context_id), avp.AVP( 'All-APN-Configurations-Included-Indicator', 1 if all_apns_included else 0, ), *[ avp.AVP( 'APN-Configuration', [ avp.AVP( 'Context-Identifier', apn['context_id'], ), avp.AVP('PDN-Type', 0), avp.AVP( 'Service-Selection', apn['service_selection'], ), avp.AVP( 'EPS-Subscribed-QoS-Profile', [ avp.AVP( 'QoS-Class-Identifier', apn['qos_profile'] ['class_id'], ), avp.AVP( 'Allocation-Retention-Priority', [ avp.AVP( 'Priority-Level', apn['qos_profile'] ['priority_level'], ), avp.AVP( 'Pre-emption-Capability', apn['qos_profile'] ['preemption_capability'], ), avp.AVP( 'Pre-emption-Vulnerability', apn['qos_profile'] ['preemption_vulnerability'], ), ], ), ], ), avp.AVP( 'AMBR', [ avp.AVP( 'Max-Requested-Bandwidth-UL', apn['ambr']['ul'], ), avp.AVP( 'Max-Requested-Bandwidth-DL', apn['ambr']['dl'], ), ], ), ], ) for apn in apns ], ], ), ], ), ] resp = self._server._s6a_manager._gen_response( state_id, req, avp.ResultCode.DIAMETER_SUCCESS, resp_avps, ) resp_buf = bytearray(resp.length) resp.encode(resp_buf, 0) result = UpdateLocationAnswer( error_code=0, default_context_id=default_context_id, total_ambr=UpdateLocationAnswer.AggregatedMaximumBitrate( max_bandwidth_ul=total_ambr['ul'], max_bandwidth_dl=total_ambr['dl'], ), msisdn=b'333608050011', all_apns_included=all_apns_included, apn=[ UpdateLocationAnswer.APNConfiguration( context_id=apn['context_id'], service_selection=apn['service_selection'], qos_profile=UpdateLocationAnswer.APNConfiguration. QoSProfile( class_id=apn['qos_profile']['class_id'], priority_level=apn['qos_profile']['priority_level'], preemption_capability=apn['qos_profile'] ['preemption_capability'], preemption_vulnerability=apn['qos_profile'] ['preemption_vulnerability'], ), ambr=UpdateLocationAnswer.AggregatedMaximumBitrate( max_bandwidth_ul=apn['ambr']['ul'], max_bandwidth_dl=apn['ambr']['dl'], ), pdn=UpdateLocationAnswer.APNConfiguration.IPV4, ) for apn in apns ], ) result_future = unittest.mock.Mock() result_future.exception.side_effect = [None] result_future.result.side_effect = [result] self._server._s6a_manager._relay_update_location_answer( state_id, req, result_future, 0, ) self._writes.assert_called_once_with(resp_buf) self._writes.reset_mock()
class S6AApplication(abc.Application): """ As defined in TS 29.272, the 3GPP S6a/S6d application enables the transfer of subscriber-related data between the Mobile Management Entity (MME) and the Home Subscriber Server (HSS) on the S6a interface and between the Serving GPRS Support Node (SGSN) and the Home Subscriber Server (HSS) on the S6d interface. """ # The ID this application uses for messages APP_ID = 16777251 # The Vendor-Specific-Application-Id and VendorId AVPs that # the S6a application should advertise CAPABILITIES_EXCHANGE_AVPS = [ avp.AVP('Supported-Vendor-Id', avp.VendorId.TGPP), avp.AVP( 'Vendor-Specific-Application-Id', [ avp.AVP('Auth-Application-Id', APP_ID), avp.AVP('Vendor-Id', avp.VendorId.TGPP), ], ), ] # Required fields for requests of each command type REQUIRED_FIELDS = { S6AApplicationCommands.AUTHENTICATION_INFORMATION: [ 'Session-Id', 'Auth-Session-State', 'User-Name', 'Visited-PLMN-Id', 'Requested-EUTRAN-Authentication-Info', ], S6AApplicationCommands.UPDATE_LOCATION: [ 'Session-Id', 'Auth-Session-State', 'User-Name', 'Visited-PLMN-Id', 'RAT-Type', 'ULR-Flags', ], } def __init__(self, lte_processor, realm, host, host_ip, loop=None): """Each application has access to a write stream and a collection of settings, currently limited to realm and host Args: lte_processor: A processor instance realm: the realm the application should serve host: the host name the application should serve host_ip: the IP address of the host """ super(S6AApplication, self).__init__(realm, host, host_ip, loop) self.lte_processor = lte_processor def handle_msg(self, state_id, msg): """ Handle the command of an incoming S6a/S6d request Args: state_id: the server state identifier msg: the message to handle Returns: None """ if not msg.header.request: logging.warning("Received unsolicited answer") return if msg.header.command_code == \ S6AApplicationCommands.AUTHENTICATION_INFORMATION: self._send_auth(state_id, msg) elif msg.header.command_code == S6AApplicationCommands.UPDATE_LOCATION: self._send_location_request(state_id, msg) else: logging.error('Unsupported command: %d', msg.command_code) def validate_message(self, state_id, msg): """ Validate a message and send the appropriate error response if necessary Args: state_id: the server state_id msg: the message to validate Returns: True if the message validated """ # Validate we have all required fields required_fields = self.REQUIRED_FIELDS[msg.header.command_code] if not msg.has_fields(required_fields): logging.error( "Missing AVP for s6a command %d", msg.header.command_code, ) resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_MISSING_AVP, ) self.writer.send_msg(resp) return False return True def _gen_response(self, state_id, msg, result_code, body_avps=None): """ Generates response message headers to an incoming request and appends the response AVPs in the expected order Args: state_id: the server state id msg: the message to respond to result_code: the Result-Code of the response body_avps: (optional) the AVPs to include in the response body Returns: a message instance containing the response """ # Generate response message headers if body_avps is None: body_avps = [] resp_msg = message.Message.create_response_msg(msg) # Session AVPs must come immediately after header RFC3588 8.8 resp_msg.append_avp(msg.find_avp(*avp.resolve('Session-Id'))) for body_avp in body_avps: resp_msg.append_avp(body_avp) # Auth-Session-State is NO_STATE_MAINTAINED (1) resp_msg.append_avp(avp.AVP('Auth-Session-State', 1)) # Host identifiers resp_msg.append_avp(avp.AVP('Origin-Host', self.host)) resp_msg.append_avp(avp.AVP('Origin-Realm', self.realm)) resp_msg.append_avp(avp.AVP('Origin-State-Id', state_id)) # Response result resp_msg.append_avp(avp.AVP('Result-Code', result_code)) return resp_msg def _send_auth(self, state_id, msg): """ Handles an incoming 3GPP-Authentication-Information-Request and writes a 3GPP-Authentication-Information-Answer Args: state_id: the server state id msg: an auth request message Returns: None """ # Validate the message if not self.validate_message(state_id, msg): return imsi = "" try: imsi = msg.find_avp(*avp.resolve('User-Name')).value plmn = msg.find_avp(*avp.resolve('Visited-PLMN-Id')).value request_eutran_info = msg.find_avp( *avp.resolve('Requested-EUTRAN-Authentication-Info'), ) re_sync_info = request_eutran_info.find_avp( *avp.resolve('Re-Synchronization-Info'), ) if re_sync_info: # According to 29.272 7.3.15 this should be concatenation of # RAND and AUTS but OAI only sends AUTS so hardcode till fixed rand = re_sync_info.value[:16] auts = re_sync_info.value[16:] self.lte_processor.resync_lte_auth_seq(imsi, rand, auts) rand, xres, autn, kasme = \ self.lte_processor.generate_lte_auth_vector(imsi, plmn) auth_info = avp.AVP( 'Authentication-Info', [ avp.AVP( 'E-UTRAN-Vector', [ avp.AVP('RAND', rand), avp.AVP('XRES', xres), avp.AVP('AUTN', autn), avp.AVP('KASME', kasme), ], ), ], ) S6A_AUTH_SUCCESS_TOTAL.inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_SUCCESS, [auth_info], ) logging.info("Auth success: %s", imsi) except CryptoError as e: S6A_AUTH_FAILURE_TOTAL.labels( code=avp.ResultCode.DIAMETER_AUTHENTICATION_REJECTED, ).inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_AUTHENTICATION_REJECTED, ) logging.error("Auth error for %s: %s", imsi, e) except SubscriberNotFoundError as e: S6A_AUTH_FAILURE_TOTAL.labels( code=avp.ResultCode.DIAMETER_ERROR_USER_UNKNOWN, ).inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_ERROR_USER_UNKNOWN, ) logging.warning("Subscriber not found: %s", e) except ServiceNotActive as e: S6A_AUTH_FAILURE_TOTAL.labels( code=avp.ResultCode.DIAMETER_ERROR_UNAUTHORIZED_SERVICE, ).inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_ERROR_UNAUTHORIZED_SERVICE, ) logging.error("Service not active for %s: %s", imsi, e) self.writer.send_msg(resp) def _send_location_request(self, state_id, msg): """ Handles an incoming 3GPP-Update-Location-Request request and writes a 3GPP-Update-Location-Answer Args: state_id: the server state id msg: an update location request message Returns: None """ # Validate the message if not self.validate_message(state_id, msg): return ula_flags = avp.AVP('ULA-Flags', 1) try: imsi = msg.find_avp(*avp.resolve('User-Name')).value profile = self.lte_processor.get_sub_profile(imsi) except SubscriberNotFoundError as e: resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_ERROR_USER_UNKNOWN, ) logging.warning('Subscriber not found for ULR: %s', e) return # Stubbed out Subscription Data from OAI subscription_data = avp.AVP( 'Subscription-Data', [ avp.AVP('MSISDN', b'333608050011'), avp.AVP('Access-Restriction-Data', 47), avp.AVP('Subscriber-Status', 0), avp.AVP('Network-Access-Mode', 2), avp.AVP( 'AMBR', [ avp.AVP('Max-Requested-Bandwidth-UL', profile.max_ul_bit_rate), avp.AVP('Max-Requested-Bandwidth-DL', profile.max_dl_bit_rate), ], ), avp.AVP( 'APN-Configuration-Profile', [ avp.AVP('Context-Identifier', 0), avp.AVP('All-APN-Configurations-Included-Indicator', 0), avp.AVP( 'APN-Configuration', [ avp.AVP('Context-Identifier', 0), avp.AVP('PDN-Type', 0), avp.AVP('Service-Selection', 'oai.ipv4'), avp.AVP( 'EPS-Subscribed-QoS-Profile', [ avp.AVP('QoS-Class-Identifier', 9), avp.AVP( 'Allocation-Retention-Priority', [ avp.AVP('Priority-Level', 15), avp.AVP( 'Pre-emption-Capability', 1), avp.AVP( 'Pre-emption-Vulnerability', 0), ], ), ], ), avp.AVP( 'AMBR', [ avp.AVP( 'Max-Requested-Bandwidth-UL', profile.max_ul_bit_rate, ), avp.AVP( 'Max-Requested-Bandwidth-DL', profile.max_dl_bit_rate, ), ], ), ], ), ], ), ], ) S6A_LUR_TOTAL.inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_SUCCESS, [ula_flags, subscription_data], ) self.writer.send_msg(resp)
def _send_location_request(self, state_id, msg): """ Handles an incoming 3GPP-Update-Location-Request request and writes a 3GPP-Update-Location-Answer Args: state_id: the server state id msg: an update location request message Returns: None """ # Validate the message if not self.validate_message(state_id, msg): return ula_flags = avp.AVP('ULA-Flags', 1) try: imsi = msg.find_avp(*avp.resolve('User-Name')).value profile = self.lte_processor.get_sub_profile(imsi) except SubscriberNotFoundError as e: resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_ERROR_USER_UNKNOWN, ) logging.warning('Subscriber not found for ULR: %s', e) return # Stubbed out Subscription Data from OAI subscription_data = avp.AVP( 'Subscription-Data', [ avp.AVP('MSISDN', b'333608050011'), avp.AVP('Access-Restriction-Data', 47), avp.AVP('Subscriber-Status', 0), avp.AVP('Network-Access-Mode', 2), avp.AVP( 'AMBR', [ avp.AVP('Max-Requested-Bandwidth-UL', profile.max_ul_bit_rate), avp.AVP('Max-Requested-Bandwidth-DL', profile.max_dl_bit_rate), ], ), avp.AVP( 'APN-Configuration-Profile', [ avp.AVP('Context-Identifier', 0), avp.AVP('All-APN-Configurations-Included-Indicator', 0), avp.AVP( 'APN-Configuration', [ avp.AVP('Context-Identifier', 0), avp.AVP('PDN-Type', 0), avp.AVP('Service-Selection', 'oai.ipv4'), avp.AVP( 'EPS-Subscribed-QoS-Profile', [ avp.AVP('QoS-Class-Identifier', 9), avp.AVP( 'Allocation-Retention-Priority', [ avp.AVP('Priority-Level', 15), avp.AVP( 'Pre-emption-Capability', 1), avp.AVP( 'Pre-emption-Vulnerability', 0), ], ), ], ), avp.AVP( 'AMBR', [ avp.AVP( 'Max-Requested-Bandwidth-UL', profile.max_ul_bit_rate, ), avp.AVP( 'Max-Requested-Bandwidth-DL', profile.max_dl_bit_rate, ), ], ), ], ), ], ), ], ) S6A_LUR_TOTAL.inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_SUCCESS, [ula_flags, subscription_data], ) self.writer.send_msg(resp)
def _send_auth(self, state_id, msg): """ Handles an incoming 3GPP-Authentication-Information-Request and writes a 3GPP-Authentication-Information-Answer Args: state_id: the server state id msg: an auth request message Returns: None """ # Validate the message if not self.validate_message(state_id, msg): return imsi = "" try: imsi = msg.find_avp(*avp.resolve('User-Name')).value plmn = msg.find_avp(*avp.resolve('Visited-PLMN-Id')).value request_eutran_info = msg.find_avp( *avp.resolve('Requested-EUTRAN-Authentication-Info'), ) re_sync_info = request_eutran_info.find_avp( *avp.resolve('Re-Synchronization-Info'), ) if re_sync_info: # According to 29.272 7.3.15 this should be concatenation of # RAND and AUTS but OAI only sends AUTS so hardcode till fixed rand = re_sync_info.value[:16] auts = re_sync_info.value[16:] self.lte_processor.resync_lte_auth_seq(imsi, rand, auts) rand, xres, autn, kasme = \ self.lte_processor.generate_lte_auth_vector(imsi, plmn) auth_info = avp.AVP( 'Authentication-Info', [ avp.AVP( 'E-UTRAN-Vector', [ avp.AVP('RAND', rand), avp.AVP('XRES', xres), avp.AVP('AUTN', autn), avp.AVP('KASME', kasme), ], ), ], ) S6A_AUTH_SUCCESS_TOTAL.inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_SUCCESS, [auth_info], ) logging.info("Auth success: %s", imsi) except CryptoError as e: S6A_AUTH_FAILURE_TOTAL.labels( code=avp.ResultCode.DIAMETER_AUTHENTICATION_REJECTED, ).inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_AUTHENTICATION_REJECTED, ) logging.error("Auth error for %s: %s", imsi, e) except SubscriberNotFoundError as e: S6A_AUTH_FAILURE_TOTAL.labels( code=avp.ResultCode.DIAMETER_ERROR_USER_UNKNOWN, ).inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_ERROR_USER_UNKNOWN, ) logging.warning("Subscriber not found: %s", e) except ServiceNotActive as e: S6A_AUTH_FAILURE_TOTAL.labels( code=avp.ResultCode.DIAMETER_ERROR_UNAUTHORIZED_SERVICE, ).inc() resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_ERROR_UNAUTHORIZED_SERVICE, ) logging.error("Service not active for %s: %s", imsi, e) self.writer.send_msg(resp)
def test_capability_exchange(self): """Test that we can respond to capability exchange requests""" msg = message.Message() msg.header.command_code = base.BaseApplicationCommands.CAPABILITIES_EXCHANGE msg.header.request = True msg.append_avp(avp.AVP('Host-IP-Address', '127.0.0.1')) msg.append_avp(avp.AVP('Inband-Security-Id', 0)) msg.append_avp(avp.AVP('Supported-Vendor-Id', 0)) msg.append_avp(avp.AVP('Vendor-Id', 0)) msg.append_avp(avp.AVP('Vendor-Specific-Application-Id', [])) # Encode request message into buffer req_buf = bytearray(msg.length) msg.encode(req_buf, 0) msg = message.Message() msg.header.command_code = base.BaseApplicationCommands.CAPABILITIES_EXCHANGE msg.header.request = False msg.append_avp(avp.AVP('Result-Code', avp.ResultCode.DIAMETER_SUCCESS)) msg.append_avp(avp.AVP('Origin-Host', self._server.host)) msg.append_avp(avp.AVP('Origin-Realm', self._server.realm)) msg.append_avp(avp.AVP('Origin-State-Id', self._server.state_id)) msg.append_avp(avp.AVP('Host-IP-Address', self.HOST_ADDR)) msg.append_avp(avp.AVP('Vendor-Id', 0)) msg.append_avp(avp.AVP('Supported-Vendor-Id', avp.VendorId.TGPP)) msg.append_avp( avp.AVP('Vendor-Specific-Application-Id', [ avp.AVP('Auth-Application-Id', s6a.S6AApplication.APP_ID), avp.AVP('Vendor-Id', avp.VendorId.TGPP) ])) msg.append_avp(avp.AVP('Product-Name', 'magma')) # Encode response message into buffer resp_buf = bytearray(msg.length) msg.encode(resp_buf, 0) self._check_reply(req_buf, resp_buf)
class BaseApplication(abc.Application): """ This is where we implement the Diameter Base Protocol Application which is defined in RFC6733. All diameters servers should implement this spec. The base Diameter protocol concerns itself with establishing connections to peers, capabilities negotiation, how messages are sent and routed through peers, and how the connections are eventually torn down. This implements a subset of the diameter Base Protcol that OAI MME requires This includes handling connection initiation and transport failure detection via watchdog requests """ # The ID used for common messages addressed to the base application APP_ID = 0 # The Vendor-Specific-Application-Id and VendorId AVPs that # the Common Application should advertise CAPABILITIES_EXCHANGE_AVPS = [avp.AVP('Vendor-Id', 0)] # Required fields for requests of each command type REQUIRED_FIELDS = { BaseApplicationCommands.CAPABILITIES_EXCHANGE: [ 'Host-IP-Address', 'Inband-Security-Id', 'Vendor-Id', 'Supported-Vendor-Id', 'Vendor-Specific-Application-Id' ], BaseApplicationCommands.DEVICE_WATCHDOG: [], BaseApplicationCommands.DISCONNECT_PEER: [], } def __init__(self, realm, host, host_ip): """Each application has access to a write stream and a collection of settings, currently limited to realm and host Args: realm: the realm the application should serve host: the host name the application should serve host_ip: the IP address of the host """ super(BaseApplication, self).__init__(realm, host, host_ip) self.applications = [] def validate_message(self, state_id, msg): """ Validate a message and send the appropriate error response if necessary Args: msg: the message to validate Returns: True if the message validated """ # Validate we have all required fields required_fields = self.REQUIRED_FIELDS[msg.header.command_code] if not msg.has_fields(required_fields): logging.error("Missing AVP for diameter command %d", msg.header.command_code) resp = self._gen_response(state_id, msg, avp.ResultCode.DIAMETER_MISSING_AVP) self.writer.send_msg(resp) return False return True def register(self, application): """ Registers an application for the Diameter Base application to advertise in the capabilities exchange Args: application: an application instance Returns: None Raises: TypeError: application is not an Application subtype """ if not issubclass(type(application), abc.Application): raise TypeError('Not a valid application') self.applications.append(application) def handle_msg(self, state_id, msg): """ Handle a message bound for the common application Args: state_id: the server state id msg: the message to handle Returns: None """ if not msg.header.request: logging.warning("Received unsolicited answer") return if msg.header.command_code == BaseApplicationCommands.CAPABILITIES_EXCHANGE: self._send_capabilities(state_id, msg) elif msg.header.command_code == BaseApplicationCommands.DEVICE_WATCHDOG: self._send_device_watchdog(state_id, msg) elif msg.header.command_code == BaseApplicationCommands.DISCONNECT_PEER: self._send_disconnect_peer(state_id, msg) else: logging.error('Unsupported command: %d', msg.header.command_code) def _gen_response(self, state_id, msg, result_code, body_avps=None): """ Generate a response message with all the fields that are found in base application responses. Args: state_id: the server state identifer msg: the message to respond to result_code: the result code to send body_avps: the AVPs to include in the response body Returns: Message instance containing the response """ # Generate an empty message with the response headers to the msg if body_avps is None: body_avps = [] resp_msg = message.Message.create_response_msg(msg) resp_msg.append_avp(avp.AVP('Result-Code', result_code)) resp_msg.append_avp(avp.AVP('Origin-Host', self.host)) resp_msg.append_avp(avp.AVP('Origin-Realm', self.realm)) resp_msg.append_avp(avp.AVP('Origin-State-Id', state_id)) # Add body AVPs for body_avp in body_avps: resp_msg.append_avp(body_avp) return resp_msg def _send_capabilities(self, state_id, msg): """ Responds to a Capability-Exchange request by sending Vendor specific AVPs from the base and registered applications Args: state_id: the server state identifier msg: the message to respond to Returns: None """ if self.validate_message(state_id, msg): # Generate the capabilities body body_avps = [avp.AVP('Host-IP-Address', self.host_ip)] body_avps.extend(self.CAPABILITIES_EXCHANGE_AVPS) for application in self.applications: body_avps.extend(application.CAPABILITIES_EXCHANGE_AVPS) body_avps.append(avp.AVP('Product-Name', avp.PRODUCT_NAME)) DIAMETER_CEX_TOTAL.inc() resp = self._gen_response(state_id, msg, avp.ResultCode.DIAMETER_SUCCESS, body_avps) self.writer.send_msg(resp) def _send_device_watchdog(self, state_id, msg): """ Responds to a Device-Watchdog requests which are pings to detection transport failures Args: state_id: the server state identifier msg: the message to respond to Returns: None """ if self.validate_message(state_id, msg): resp = self._gen_response(state_id, msg, avp.ResultCode.DIAMETER_SUCCESS) DIAMETER_WATCHDOG_TOTAL.inc() self.writer.send_msg(resp) def _send_disconnect_peer(self, state_id, msg): """ Responds to a Disconnect-Peer-Request. Upon receipt of this message, the transport connection is shut down. Args: state_id: the server state identifier msg: the message to respond to Returns: None """ logging.info('Received disconnect request, state id: %d', state_id) if self.validate_message(state_id, msg): resp = self._gen_response(state_id, msg, avp.ResultCode.DIAMETER_SUCCESS) DIAMETER_DISCONECT_TOTAL.inc() self.writer.send_msg(resp)
def _relay_update_location_answer( self, state_id, msg, answer_future, retries_left, ): err = answer_future.exception() if err and retries_left > 0: # TODO: retry only on network failure and not application failures user_name = msg.find_avp(*avp.resolve('User-Name')).value logging.info( "Location Update %s Error! [%s] %s, retrying...", user_name, err.code(), err.details(), ) self._send_location_request_with_retries( state_id, msg, retries_left - 1, ) return elif err: user_name = msg.find_avp(*avp.resolve('User-Name')).value logging.warning( "Location Update %s Error! [%s] %s", user_name, err.code(), err.details(), ) resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_UNABLE_TO_COMPLY, ) else: answer = answer_future.result() error_code = answer.error_code if error_code: result_info = avp.AVP( 'Experimental-Result', [ avp.AVP('Vendor-Id', 10415), avp.AVP('Experimental-Result-Code', error_code), ], ) resp = self._gen_response( state_id, msg, error_code, [result_info], ) else: # Stubbed out Subscription Data from OAI subscription_data = avp.AVP( 'Subscription-Data', [ avp.AVP('MSISDN', answer.msisdn), avp.AVP('Access-Restriction-Data', 47), avp.AVP('Subscriber-Status', 0), avp.AVP('Network-Access-Mode', 2), avp.AVP( 'AMBR', [ avp.AVP( 'Max-Requested-Bandwidth-UL', answer.total_ambr.max_bandwidth_ul, ), avp.AVP( 'Max-Requested-Bandwidth-DL', answer.total_ambr.max_bandwidth_dl, ), ], ), avp.AVP( 'APN-Configuration-Profile', [ avp.AVP( 'Context-Identifier', answer.default_context_id, ), avp.AVP( 'All-APN-Configurations-Included-Indicator', 1 if answer.all_apns_included else 0, ), *[ avp.AVP( 'APN-Configuration', [ avp.AVP('Context-Identifier', apn.context_id), avp.AVP('PDN-Type', apn.pdn), avp.AVP( 'Service-Selection', apn.service_selection, ), avp.AVP( 'EPS-Subscribed-QoS-Profile', [ avp.AVP( 'QoS-Class-Identifier', apn.qos_profile. class_id, ), avp.AVP( 'Allocation-Retention-Priority', [ avp.AVP( 'Priority-Level', apn.qos_profile. priority_level, ), avp.AVP( 'Pre-emption-Capability', 1 if apn. qos_profile. preemption_capability else 0, ), avp.AVP( 'Pre-emption-Vulnerability', 1 if apn. qos_profile. preemption_vulnerability else 0, ), ], ), ], ), avp.AVP( 'AMBR', [ avp.AVP( 'Max-Requested-Bandwidth-UL', apn.ambr. max_bandwidth_ul, ), avp.AVP( 'Max-Requested-Bandwidth-DL', apn.ambr. max_bandwidth_dl, ), ], ), ], ) for apn in answer.apn ], ], ), ], ) ula_flags = avp.AVP('ULA-Flags', 1) resp = self._gen_response( state_id, msg, avp.ResultCode.DIAMETER_SUCCESS, [ula_flags, subscription_data], ) self.writer.send_msg(resp) S6A_LUR_TOTAL.inc()