diff --git a/go.mod b/go.mod index d8075050..bbc9908b 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/free5gc/util v1.2.0 github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 + github.com/h2non/gock v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/pkg/errors v0.9.1 @@ -45,7 +46,6 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect - github.com/h2non/gock v1.2.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect diff --git a/internal/context/context.go b/internal/context/context.go index cdd342bf..1e3ada98 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -79,10 +79,11 @@ type AMFContext struct { NetworkName factory.NetworkName NgapIpList []string // NGAP Server IP NgapPort int - T3502Value int // unit is second - T3512Value int // unit is second - Non3gppDeregTimerValue int // unit is second - TimeZone string // "[+-]HH:MM[+][1-2]", Refer to TS 29.571 - 5.2.2 Simple Data Types + T3502Value int // unit is second + T3512Value int // unit is second + Non3gppDeregTimerValue int // unit is second + TimeZone string // "[+-]HH:MM[+][1-2]", Refer to TS 29.571 - 5.2.2 Simple Data Types + PendingHandovers sync.Map // map[supi]*chan PendingHandoverResponse // read-only fields T3513Cfg factory.TimerValue T3522Cfg factory.TimerValue @@ -96,6 +97,11 @@ type AMFContext struct { OAuth2Required bool } +type PendingHandoverResponse struct { + Response201 *models.CreateUeContextResponse201 + Response403 *models.CreateUeContextResponse403 +} + type AMFContextEventSubscription struct { IsAnyUe bool IsGroupUe bool @@ -393,14 +399,18 @@ func (context *AMFContext) AmfRanFindByConn(conn net.Conn) (*AmfRan, bool) { func (context *AMFContext) AmfRanFindByRanID(ranNodeID models.GlobalRanNodeId) (*AmfRan, bool) { var ran *AmfRan var ok bool + isEmpty := true context.AmfRanPool.Range(func(key, value interface{}) bool { + isEmpty = false amfRan := value.(*AmfRan) if amfRan.RanId == nil { + logger.CommLog.Warnf("RAN Node ID is nil") return true } switch amfRan.RanPresent { case RanPresentGNbId: + logger.CommLog.Debugf("%+v", amfRan.RanId.GNbId) if amfRan.RanId.GNbId != nil && ranNodeID.GNbId != nil && amfRan.RanId.GNbId.GNBValue == ranNodeID.GNbId.GNBValue { ran = amfRan @@ -422,6 +432,9 @@ func (context *AMFContext) AmfRanFindByRanID(ranNodeID models.GlobalRanNodeId) ( } return true }) + if isEmpty { + logger.CommLog.Warnln("AmfRanPool is empty") + } return ran, ok } diff --git a/internal/ngap/handler.go b/internal/ngap/handler.go index a4521fc7..94608ff6 100644 --- a/internal/ngap/handler.go +++ b/internal/ngap/handler.go @@ -1490,7 +1490,38 @@ func handleHandoverRequestAcknowledgeMain(ran *context.AmfRan, if sourceUe == nil { // TODO: Send Namf_Communication_CreateUEContext Response to S-AMF ran.Log.Error("handover between different Ue has not been implement yet") + + ueContextCreatedData := buildUeContextCreatedData(amfUe, pduSessionResourceHandoverList) + + resp201 := models.CreateUeContextResponse201{ + JsonData: ueContextCreatedData, + BinaryDataN2Information: targetToSourceTransparentContainer.Value, + } + + amfSelf := amfUe.ServingAMF() + + // Create channel if not exist + // pendingHOResponseChan := make(chan context.PendingHandoverResponse) + var pendingHOResponseChan chan context.PendingHandoverResponse + value, ok := amfSelf.PendingHandovers.Load(amfUe.Supi) + if ok { + ran.Log.Info("PendingHandoverResponse channel created by CreateUEContextProcedure") + pendingHOResponseChan = value.(chan context.PendingHandoverResponse) + } else { + // error handling for nil channel value + // send UE Context Release Command to the target RAN + ran.Log.Error("PendingHandoverResponse channel not found") + ngap_message.SendUEContextReleaseCommand(targetUe, + context.UeContextReleaseHandover, ngapType.CausePresentRadioNetwork, + ngapType.CauseRadioNetworkPresentHoFailureInTarget5GCNgranNodeOrTargetSystem) + } + + // Send the Response to CreateUEContextProcedure() + pendingHOResponseChan <- context.PendingHandoverResponse{ + Response201: &resp201, + } } else { + // Handover in the same AMF ran.Log.Tracef("Source: RanUeNgapID[%d] AmfUeNgapID[%d]", sourceUe.RanUeNgapId, sourceUe.AmfUeNgapId) ran.Log.Tracef("Target: RanUeNgapID[%d] AmfUeNgapID[%d]", targetUe.RanUeNgapId, targetUe.AmfUeNgapId) if len(pduSessionResourceHandoverList.List) == 0 { @@ -1510,6 +1541,68 @@ func handleHandoverRequestAcknowledgeMain(ran *context.AmfRan, } } +func buildUeContextCreatedData(amfUe *context.AmfUe, + pduSessionResourceHandoverList ngapType.PDUSessionResourceHandoverList, +) (ueContextCreatedData *models.UeContextCreatedData) { + ueContextCreatedData = new(models.UeContextCreatedData) + + ueContextCreatedData.UeContext = new(models.UeContext) + ueContextCreatedData.UeContext.Supi = amfUe.Supi + ueContextCreatedData.UeContext.SupiUnauthInd = amfUe.UnauthenticatedSupi + if amfUe.Gpsi != "" { + ueContextCreatedData.UeContext.GpsiList = append(ueContextCreatedData.UeContext.GpsiList, amfUe.Gpsi) + } + if amfUe.Pei != "" { + ueContextCreatedData.UeContext.Pei = amfUe.Pei + } + if amfUe.UdmGroupId != "" { + ueContextCreatedData.UeContext.UdmGroupId = amfUe.UdmGroupId + } + if amfUe.AusfGroupId != "" { + ueContextCreatedData.UeContext.AusfGroupId = amfUe.AusfGroupId + } + if amfUe.AccessAndMobilitySubscriptionData != nil { + if amfUe.AccessAndMobilitySubscriptionData.SubscribedUeAmbr != nil { + ueContextCreatedData.UeContext.SubUeAmbr = &models.Ambr{ + Uplink: amfUe.AccessAndMobilitySubscriptionData.SubscribedUeAmbr.Uplink, + Downlink: amfUe.AccessAndMobilitySubscriptionData.SubscribedUeAmbr.Downlink, + } + } + if amfUe.AccessAndMobilitySubscriptionData.RfspIndex != 0 { + ueContextCreatedData.UeContext.SubRfsp = amfUe.AccessAndMobilitySubscriptionData.RfspIndex + } + } + if amfUe.PcfId != "" { + ueContextCreatedData.UeContext.PcfId = amfUe.PcfId + } + if amfUe.AmPolicyUri != "" { + ueContextCreatedData.UeContext.PcfAmPolicyUri = amfUe.AmPolicyUri + } + for _, eventSub := range amfUe.EventSubscriptionsInfo { + if eventSub.EventSubscription != nil { + ueContextCreatedData.UeContext.EventSubscriptionList = append( + ueContextCreatedData.UeContext.EventSubscriptionList, *eventSub.EventSubscription) + } + } + if amfUe.TraceData != nil { + ueContextCreatedData.UeContext.TraceData = amfUe.TraceData + } + + ueContextCreatedData.TargetToSourceData = new(models.N2InfoContent) + ueContextCreatedData.TargetToSourceData.NgapIeType = models.AmfCommunicationNgapIeType_TAR_TO_SRC_CONTAINER + + ueContextCreatedData.TargetToSourceData.NgapData = new(models.RefToBinaryData) + ueContextCreatedData.TargetToSourceData.NgapData.ContentId = "N2InfoContent" + + for _, pduSessionResourceHandoverItem := range pduSessionResourceHandoverList.List { + ueContextCreatedData.PduSessionList = append(ueContextCreatedData.PduSessionList, models.N2SmInformation{ + PduSessionId: int32(pduSessionResourceHandoverItem.PDUSessionID.Value), + }) + } + + return ueContextCreatedData +} + func handleHandoverFailureMain(ran *context.AmfRan, targetUe *context.RanUe, cause *ngapType.Cause, @@ -1633,10 +1726,161 @@ func handleHandoverRequiredMain(ran *context.AmfRan, // handover between different AMF sourceUe.Log.Warnf("Handover required : cannot find target Ran Node Id[%+v] in this AMF", targetRanNodeId) sourceUe.Log.Error("Handover between different AMF has not been implemented yet") + hoFailCause = business_metrics.HANDOVER_BETWEEN_DIFFERENT_AMF_NOT_SUPPORTED return // TODO: Send to T-AMF // Described in (23.502 4.9.1.3.2) step 3.Namf_Communication_CreateUEContext Request + /* + var ueContextCreateData models.UeContextCreateData + ueContextCreateData.UeContext.Supi = amfUe.Supi + ueContextCreateData.UeContext.SupiUnauthInd = amfUe.UnauthenticatedSupi + ueContextCreateData.UeContext.UdmGroupId = amfUe.UdmGroupId + ueContextCreateData.UeContext.AusfGroupId = amfUe.AusfGroupId + ueContextCreateData.UeContext.RestrictedPrimaryRatList[0] = amfUe.RatType + + ueContextCreateData.TargetId.RanNodeId = &targetRanNodeId + ueContextCreateData.TargetId.Tai = &amfUe.Tai + + ueContextCreateData.PduSessionList = make([]models.N2SmInformation, 0) + for _, pDUSessionResourceHoItem := range pDUSessionResourceListHORqd.List { + pduSessionID := int32(pDUSessionResourceHoItem.PDUSessionID.Value) + smContext, okSmContextFindByPDUSessionID := amfUe.SmContextFindByPDUSessionID(pduSessionID) + if !okSmContextFindByPDUSessionID { + sourceUe.Log.Warnf("SmContext[PDU Session ID:%d] not found", pduSessionID) + // TODO: Check if doing error handling here + continue + } + snssai := smContext.Snssai() + ueContextCreateData.PduSessionList = append(ueContextCreateData.PduSessionList, models.N2SmInformation{ + PduSessionId: pduSessionID, + SNssai: &snssai, + }) + } + + ueContextCreateData.SourceToTargetData.NgapIeType = models.AmfCommunicationNgapIeType_HANDOVER_REQUIRED + ueContextCreateData.SourceToTargetData.NgapData.ContentId = "N2SmInfo" + + ueContextCreateData.N2NotifyUri = "" + ueContextCreatedData, targetToSourceTransparentContainer, + problemDetails, err := consumer.GetConsumer().CreateUEContextRequest(amfUe, ueContextCreateData) + + if problemDetails != nil { + // get UeContextCreateError (HANDOVER FAILURE) from target AMF. + // Send Handover Preparation Failure to source RAN (described in TS 38.413 8.4.1.3). + sourceUe.Log.Info("Handle Handover Preparation Failure [HoFailure In Target5GC NgranNode Or TargetSystem]") + cause = &ngapType.Cause{ + Present: ngapType.CausePresentRadioNetwork, + RadioNetwork: &ngapType.CauseRadioNetwork{ + Value: ngapType.CauseRadioNetworkPresentHoFailureInTarget5GCNgranNodeOrTargetSystem, + }, + } + ngap_message.SendHandoverPreparationFailure(sourceUe, *cause, nil) + return + } else if err != nil { + // error occurred in S-AMF. + sourceUe.Log.Errorf("CreateUEContextRequest Error in source AMF: %s", err.Error()) + cause = &ngapType.Cause{ + Present: ngapType.CausePresentRadioNetwork, + RadioNetwork: &ngapType.CauseRadioNetwork{ + Value: ngapType.CauseRadioNetworkPresentUnspecified, + }, + } + ngap_message.SendHandoverPreparationFailure(sourceUe, *cause, nil) + } else { + // Get UeContextCreatedData from T-AMF. + // Send HandoverCommand to S-RAN. + var pduSessionResourceHandoverList ngapType.PDUSessionResourceHandoverList + var pduSessionResourceToReleaseList ngapType.PDUSessionResourceToReleaseListHOCmd + + for _, N2SmInfo := range ueContextCreatedData.PduSessionList { + var item ngapType.PDUSessionResourceHandoverItem + item.PDUSessionID.Value = int64(N2SmInfo.PduSessionId) + } + + ngap_message.SendHandoverCommand(sourceUe, pduSessionResourceHandoverList, pduSessionResourceToReleaseList, + *targetToSourceTransparentContainer, nil) + /* + // describe in 23.502 4.9.1.3.2 step11 + if pDUSessionResourceAdmittedList != nil { + targetUe.Log.Infof("Send HandoverRequestAcknowledgeTransfer to SMF") + for _, item := range pDUSessionResourceAdmittedList.List { /* + pduSessionID := int32(item.PDUSessionID.Value) + transfer := item.HandoverRequestAcknowledgeTransfer + smContext, ok := amfUe.SmContextFindByPDUSessionID(pduSessionID) + if !ok { + targetUe.Log.Warnf("SmContext[PDU Session ID:%d] not found", pduSessionID) + // TODO: Check if doing error handling here + continue + } + resp, errResponse, problemDetails, err := consumer.GetConsumer().SendUpdateSmContextN2HandoverPrepared(amfUe, + smContext, models.N2SmInfoType_HANDOVER_REQ_ACK, transfer) + if err != nil { + targetUe.Log.Errorf("Send HandoverRequestAcknowledgeTransfer error: %v", err) + } + if problemDetails != nil { + targetUe.Log.Warnf("ProblemDetails[status: %d, Cause: %s]", problemDetails.Status, problemDetails.Cause) + } + if resp != nil && resp.BinaryDataN2SmInformation != nil { + handoverItem := ngapType.PDUSessionResourceHandoverItem{} + handoverItem.PDUSessionID = item.PDUSessionID + handoverItem.HandoverCommandTransfer = resp.BinaryDataN2SmInformation + pduSessionResourceHandoverList.List = append(pduSessionResourceHandoverList.List, handoverItem) + targetUe.SuccessPduSessionId = append(targetUe.SuccessPduSessionId, pduSessionID) + } + if errResponse != nil && errResponse.BinaryDataN2SmInformation != nil { + releaseItem := ngapType.PDUSessionResourceToReleaseItemHOCmd{} + releaseItem.PDUSessionID = item.PDUSessionID + releaseItem.HandoverPreparationUnsuccessfulTransfer = errResponse.BinaryDataN2SmInformation + pduSessionResourceToReleaseList.List = append(pduSessionResourceToReleaseList.List, releaseItem) + } + } + } + + if pDUSessionResourceFailedToSetupListHOAck != nil { + targetUe.Log.Infof("Send HandoverResourceAllocationUnsuccessfulTransfer to SMF") + for _, item := range pDUSessionResourceFailedToSetupListHOAck.List { + pduSessionID := int32(item.PDUSessionID.Value) + transfer := item.HandoverResourceAllocationUnsuccessfulTransfer + smContext, ok := amfUe.SmContextFindByPDUSessionID(pduSessionID) + if !ok { + targetUe.Log.Warnf("SmContext[PDU Session ID:%d] not found", pduSessionID) + // TODO: Check if doing error handling here + continue + } + _, _, problemDetails, err := consumer.GetConsumer().SendUpdateSmContextN2HandoverPrepared(amfUe, smContext, + models.N2SmInfoType_HANDOVER_RES_ALLOC_FAIL, transfer) + if err != nil { + targetUe.Log.Errorf("Send HandoverResourceAllocationUnsuccessfulTransfer error: %v", err) + } + if problemDetails != nil { + targetUe.Log.Warnf("ProblemDetails[status: %d, Cause: %s]", problemDetails.Status, problemDetails.Cause) + } + } + } + + sourceUe := targetUe.SourceUe + if sourceUe == nil { + // TODO: Send Namf_Communication_CreateUEContext Response to S-AMF + ran.Log.Error("handover between different Ue has not been implement yet") + } else { + ran.Log.Tracef("Source: RanUeNgapID[%d] AmfUeNgapID[%d]", sourceUe.RanUeNgapId, sourceUe.AmfUeNgapId) + ran.Log.Tracef("Target: RanUeNgapID[%d] AmfUeNgapID[%d]", targetUe.RanUeNgapId, targetUe.AmfUeNgapId) + if len(pduSessionResourceHandoverList.List) == 0 { + targetUe.Log.Info("Handle Handover Preparation Failure [HoFailure In Target5GC NgranNode Or TargetSystem]") + cause := &ngapType.Cause{ + Present: ngapType.CausePresentRadioNetwork, + RadioNetwork: &ngapType.CauseRadioNetwork{ + Value: ngapType.CauseRadioNetworkPresentHoFailureInTarget5GCNgranNodeOrTargetSystem, + }, + } + ngap_message.SendHandoverPreparationFailure(sourceUe, *cause, nil) + return + } + ngap_message.SendHandoverCommand(sourceUe, pduSessionResourceHandoverList, pduSessionResourceToReleaseList, + *targetToSourceTransparentContainer, nil) + } + } */ } else { // Handover in same AMF sourceUe.HandOverType.Value = handoverType.Value diff --git a/internal/ngap/message/send.go b/internal/ngap/message/send.go index 67dfd80a..059e07b2 100644 --- a/internal/ngap/message/send.go +++ b/internal/ngap/message/send.go @@ -701,6 +701,68 @@ func SendHandoverRequest(sourceUe *context.RanUe, targetRan *context.AmfRan, cau isHoReqSent, additionalCause = SendToRanUe(targetUe, pkt) } +func SendHandoverRequestWithAMFChange(targetUe *context.RanUe, targetRan *context.AmfRan, cause ngapType.Cause, + pduSessionResourceSetupListHOReq ngapType.PDUSessionResourceSetupListHOReq, + sourceToTargetTransparentContainer ngapType.SourceToTargetTransparentContainer, nsci bool, +) { + isHoReqSent := false + additionalCause := "" + defer ngap_metrics.IncrMetricsSentMsg(ngap_metrics.HANDOVER_REQUEST, &isHoReqSent, cause, &additionalCause) + + defer func(msgSent *bool, cause ngapType.Cause) { + hoCause := ngap_metrics.GetCauseErrorStr(&cause) + if hoCause == "unknown ngapType.Cause" { + hoCause = additionalCause + } + + if msgSent != nil && !*msgSent { + business_metrics.IncrHoEventCounter(business_metrics.HANDOVER_TYPE_NGAP_VALUE, + utils.FailureMetric, hoCause, targetUe.HandOverStartTime) + } + }(&isHoReqSent, cause) + + if targetUe == nil { + additionalCause = ngap_metrics.SOURCE_UE_NIL_ERR + logger.NgapLog.Error("targetUe is nil") + return + } + + targetUe.Log.Info("Send Handover Request") + + amfUe := targetUe.AmfUe + if amfUe == nil { + additionalCause = ngap_metrics.AMF_UE_NIL_ERR + targetUe.Log.Error("amfUe is nil") + return + } + if targetRan == nil { + additionalCause = ngap_metrics.TARGET_RAN_NIL_ERR + targetUe.Log.Error("targetRan is nil") + return + } + + if len(pduSessionResourceSetupListHOReq.List) > context.MaxNumOfPDUSessions { + additionalCause = ngap_metrics.PDU_LIST_OOR_ERR + targetUe.Log.Error("Pdu List out of range") + return + } + + if len(sourceToTargetTransparentContainer.Value) == 0 { + additionalCause = ngap_metrics.SRC_TO_TARGET_TRANSPARENT_CONTAINER_NIL_ERR + targetUe.Log.Error("Source To Target TransparentContainer is nil") + return + } + + pkt, err := BuildHandoverRequest(targetUe, cause, pduSessionResourceSetupListHOReq, + sourceToTargetTransparentContainer, nsci) + if err != nil { + additionalCause = ngap_metrics.NGAP_MSG_BUILD_ERR + targetUe.Log.Errorf("Build HandoverRequest failed : %s", err.Error()) + return + } + isHoReqSent, additionalCause = SendToRanUe(targetUe, pkt) +} + // pduSessionResourceSwitchedList: provided by AMF, and the transfer data is from SMF // pduSessionResourceReleasedList: provided by AMF, and the transfer data is from SMF // newSecurityContextIndicator: if AMF has activated a new 5G NAS security context, set it to true, diff --git a/internal/sbi/consumer/amf_service.go b/internal/sbi/consumer/amf_service.go index aee49aea..80eebefc 100644 --- a/internal/sbi/consumer/amf_service.go +++ b/internal/sbi/consumer/amf_service.go @@ -7,6 +7,8 @@ import ( amf_context "github.com/free5gc/amf/internal/context" "github.com/free5gc/amf/internal/logger" "github.com/free5gc/nas/nasMessage" + "github.com/free5gc/ngap" + "github.com/free5gc/ngap/ngapType" "github.com/free5gc/openapi" Namf_Communication "github.com/free5gc/openapi/amf/Communication" "github.com/free5gc/openapi/models" @@ -153,11 +155,14 @@ func (s *namfService) buildAmPolicyReqTriggers( } func (s *namfService) CreateUEContextRequest(ue *amf_context.AmfUe, ueContextCreateData models.UeContextCreateData) ( - ueContextCreatedData *models.UeContextCreatedData, problemDetails *models.ProblemDetails, err error, + ueContextCreatedData *models.UeContextCreatedData, + targetToSourceTransparentContainer *ngapType.TargetToSourceTransparentContainer, + problemDetails *models.ProblemDetails, + err error, ) { client := s.getComClient(ue.TargetAmfUri) if client == nil { - return nil, nil, openapi.ReportError("amf not found") + return nil, nil, nil, openapi.ReportError("amf not found") } req := models.CreateUeContextRequest{ @@ -165,7 +170,7 @@ func (s *namfService) CreateUEContextRequest(ue *amf_context.AmfUe, ueContextCre } ctx, _, err := amf_context.GetSelf().GetTokenCtx(models.ServiceName_NAMF_COMM, models.NrfNfManagementNfType_AMF) if err != nil { - return nil, nil, err + return nil, nil, nil, err } creatuectxreq := Namf_Communication.CreateUEContextRequest{ @@ -177,14 +182,30 @@ func (s *namfService) CreateUEContextRequest(ue *amf_context.AmfUe, ueContextCre if localErr == nil { ueContextCreatedData = res.CreateUeContextResponse201.JsonData logger.ConsumerLog.Debugf("UeContextCreatedData: %+v", *ueContextCreatedData) + + if res.CreateUeContextResponse201.BinaryDataN2Information != nil { + pdu, errNgapDecoder := ngap.Decoder(res.CreateUeContextResponse201.BinaryDataN2Information) + if errNgapDecoder != nil && + pdu.Present == ngapType.NGAPPDUPresentSuccessfulOutcome && + pdu.SuccessfulOutcome.ProcedureCode.Value == ngapType.ProcedureCodeHandoverResourceAllocation { + handoverRequestAcknowledge := pdu.SuccessfulOutcome.Value.HandoverRequestAcknowledge + for _, ie := range handoverRequestAcknowledge.ProtocolIEs.List { + if ie.Id.Value == ngapType.ProtocolIEIDTargetToSourceTransparentContainer { + targetToSourceTransparentContainer = ie.Value.TargetToSourceTransparentContainer + } + } + } else if errNgapDecoder != nil { + return nil, nil, nil, errNgapDecoder + } + } } else { if apiErr, ok := localErr.(openapi.GenericOpenAPIError); ok { creatErr := apiErr.Model().(*Namf_Communication.CreateUEContextError) - return nil, &creatErr.ProblemDetails, nil + return nil, nil, &creatErr.ProblemDetails, nil } - return nil, nil, localErr + return nil, nil, nil, localErr } - return ueContextCreatedData, problemDetails, err + return ueContextCreatedData, targetToSourceTransparentContainer, problemDetails, err } func (s *namfService) ReleaseUEContextRequest(ue *amf_context.AmfUe, ngapCause models.NgApCause) ( diff --git a/internal/sbi/consumer/smf_service.go b/internal/sbi/consumer/smf_service.go index 79c3764f..c7a4fbaf 100644 --- a/internal/sbi/consumer/smf_service.go +++ b/internal/sbi/consumer/smf_service.go @@ -443,9 +443,13 @@ func (s *nsmfService) SendUpdateSmContextHandoverBetweenAccessType( } func (s *nsmfService) SendUpdateSmContextHandoverBetweenAMF( - ue *amf_context.AmfUe, smContext *amf_context.SmContext, amfid string, guami *models.Guami, activate bool) ( - *models.UpdateSmContextResponse200, *models.UpdateSmContextResponse400, *models.ProblemDetails, error, -) { + ue *amf_context.AmfUe, + targetRanId *models.NgRanTargetId, + smContext *amf_context.SmContext, + amfid string, + guami *models.Guami, + activate bool, +) (*models.UpdateSmContextResponse200, *models.UpdateSmContextResponse400, *models.ProblemDetails, error) { updateData := models.SmfPduSessionSmContextUpdateData{} updateData.ServingNfId = amfid updateData.ServingNetwork = guami.PlmnId @@ -461,6 +465,12 @@ func (s *nsmfService) SendUpdateSmContextHandoverBetweenAMF( } } } + // Based on TS 29.502 V17.1.0 section 5.2.2.3.4.2 "N2 Handover Preparation" + // step 1, include hoState=PREPARING, targetId(T-RAN Node ID and TAI), + // targetServingNfId(T-AMF), and N2 SM information (direct path availability) + // in the updateData. + updateData.HoState = models.HoState_PREPARING + updateData.TargetId = targetRanId return s.consumer.SendUpdateSmContextRequest(smContext, &updateData, nil, nil) } diff --git a/internal/sbi/processor/createUeContext_test.go b/internal/sbi/processor/createUeContext_test.go new file mode 100644 index 00000000..8aecf74e --- /dev/null +++ b/internal/sbi/processor/createUeContext_test.go @@ -0,0 +1,1096 @@ +package processor_test + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + "net/http/httptest" + "net/textproto" + "sync" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/h2non/gock" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + amf_context "github.com/free5gc/amf/internal/context" + amf_ngap "github.com/free5gc/amf/internal/ngap" + "github.com/free5gc/amf/internal/sbi/consumer" + "github.com/free5gc/amf/internal/sbi/processor" + "github.com/free5gc/amf/pkg/factory" + "github.com/free5gc/amf/pkg/service" + "github.com/free5gc/aper" + "github.com/free5gc/ngap" + "github.com/free5gc/ngap/ngapConvert" + "github.com/free5gc/ngap/ngapType" + "github.com/free5gc/openapi" + "github.com/free5gc/openapi/models" + "github.com/free5gc/sctp" + "github.com/free5gc/util/httpwrapper" +) + +var plmnIdModel = models.PlmnId{ + Mcc: "208", + Mnc: "93", +} +var TestPlmn = ngapConvert.PlmnIdToNgap(plmnIdModel) + +var ( + mockRAN *amf_context.AmfRan + RanId = models.GlobalRanNodeId{ + PlmnId: &plmnIdModel, + N3IwfId: "123", + GNbId: &models.GNbId{ + BitLength: 22, + GNBValue: "2a3f44", + }, + NgeNbId: "2a3f44", + } +) +var RanListenerIP = "127.0.0.2:38412" + +var pduSessionID int32 = 10 + +var wg sync.WaitGroup + +// Mock AMF Config, copied from free5gc/config/amfcfg.yaml +var testConfig = factory.Config{ + Info: &factory.Info{ + Version: "v1.0.0", + Description: "AMF test Configuration", + }, + Configuration: &factory.Configuration{ + AmfName: "TestAMF", + NgapIpList: []string{ + "127.0.0.18", + }, + NgapPort: 38412, + Sbi: &factory.Sbi{ + Scheme: "http", + RegisterIPv4: "127.0.0.18", + BindingIPv4: "127.0.0.18", + Port: 8000, + }, + ServiceNameList: []string{ + "namf-comm", + "namf-evts", + "namf-mt", + "namf-loc", + "namf-oam", + }, + ServedGumaiList: []models.Guami{ + { + PlmnId: &models.PlmnIdNid{ + Mcc: "208", + Mnc: "93", + }, + AmfId: "cafe56", + }, + }, + SupportTAIList: []models.Tai{ + { + PlmnId: &plmnIdModel, + Tac: "000001", + }, + }, + PlmnSupportList: []factory.PlmnSupportItem{ + { + PlmnId: &plmnIdModel, + SNssaiList: []models.Snssai{ + { + Sst: 1, + Sd: "010203", + }, + { + Sst: 1, + Sd: "112233", + }, + }, + }, + }, + SupportDnnList: []string{ + "internet", + }, + NrfUri: "http://127.0.0.10:8000", + Security: &factory.Security{ + IntegrityOrder: []string{ + "NIA2", + }, + CipheringOrder: []string{ + "NEA0", + "NEA2", + }, + }, + NetworkName: factory.NetworkName{ + Full: "free5GC", + Short: "free", + }, + NgapIE: &factory.NgapIE{ + MobilityRestrictionList: &factory.MobilityRestrictionList{ + Enable: true, + }, + MaskedIMEISV: &factory.MaskedIMEISV{ + Enable: true, + }, + RedirectionVoiceFallback: &factory.RedirectionVoiceFallback{ + Enable: false, + }, + }, + NasIE: &factory.NasIE{ + NetworkFeatureSupport5GS: &factory.NetworkFeatureSupport5GS{ + Enable: true, + Length: 1, + ImsVoPS: 0, + Emc: 0, + Emf: 0, + IwkN26: 0, + Mpsi: 0, + EmcN3: 0, + Mcsi: 0, + }, + }, + T3502Value: 720, + T3512Value: 3600, + Non3gppDeregTimerValue: 3240, + T3513: factory.TimerValue{ + Enable: true, + ExpireTime: 6 * time.Second, + MaxRetryTimes: 4, + }, + T3522: factory.TimerValue{ + Enable: true, + ExpireTime: 6 * time.Second, + MaxRetryTimes: 4, + }, + T3550: factory.TimerValue{ + Enable: true, + ExpireTime: 6 * time.Second, + MaxRetryTimes: 4, + }, + T3555: factory.TimerValue{ + Enable: true, + ExpireTime: 6 * time.Second, + MaxRetryTimes: 4, + }, + T3560: factory.TimerValue{ + Enable: true, + ExpireTime: 6 * time.Second, + MaxRetryTimes: 4, + }, + T3565: factory.TimerValue{ + Enable: true, + ExpireTime: 6 * time.Second, + MaxRetryTimes: 4, + }, + T3570: factory.TimerValue{ + Enable: true, + ExpireTime: 6 * time.Second, + MaxRetryTimes: 4, + }, + }, + Logger: &factory.Logger{ + Enable: true, + Level: "trace", + ReportCaller: false, + }, +} + +// startSctpMockRAN starts an SCTP listener at listenAddr (e.g. "127.0.0.2:38412"). +// It returns a channel that will receive the accepted *sctp.SCTPConn and a stop func. +func startSctpMockRAN(t *testing.T, listenAddr string) (acceptCh chan *sctp.SCTPConn, stop func()) { + addr, err := sctp.ResolveSCTPAddr("sctp", listenAddr) + if err != nil { + t.Fatalf("ResolveSCTPAddr failed: %+v", err) + } + ln, err := sctp.ListenSCTP("sctp", addr) + if err != nil { + t.Fatalf("ListenSCTP failed: %+v", err) + } + acceptCh = make(chan *sctp.SCTPConn, 1) + + wg.Add(1) + go func() { + defer wg.Done() + + conn, errAccept := ln.AcceptSCTP(-1) // no timeout. + if errAccept != nil { + t.Logf("AcceptSCTP error: %+v", err) + return + } + acceptCh <- conn + // keep listener open (or close here if only one accept is needed) + }() + + stop = func() { + if errClose := ln.Close(); errClose != nil { + t.Errorf("SCTP Listener Close() error: %s", err.Error()) + } + } + + // give listener a moment to start + time.Sleep(10 * time.Millisecond) + return acceptCh, stop +} + +func initAMFConfig(t *testing.T) { + factory.AmfConfig = &testConfig + amfContext := amf_context.GetSelf() + amf_context.InitAmfContext(amfContext) + + // Create remote SCTP address + remoteAddr, err := sctp.ResolveSCTPAddr("sctp", RanListenerIP) + if err != nil { + t.Errorf("initAMFConfig: resolve remote addr error: %+v", err) + return + } + + // Create local address + localAddr, err := sctp.ResolveSCTPAddr("sctp", "127.0.0.18:0") + if err != nil { + panic(err) + } + + // Establish SCTP connection with specified remote address + conn, err := sctp.DialSCTP("sctp", localAddr, remoteAddr) + if err != nil { + t.Errorf("initAMFConfig: dial SCTP error: %+v", err) + return + } + + wg.Add(1) + go func(conn *sctp.SCTPConn) { + defer wg.Done() + + buf := make([]byte, 65536) + + if n, errRead := conn.Read(buf); errRead == nil { + amf_ngap.Dispatch(conn, buf[:n]) + } + }(conn) + + mockRAN = amfContext.NewAmfRan(conn) // Add mock RAN context + + mockRanId := ngapConvert.RanIDToNgap(RanId) + mockRAN.SetRanId(&mockRanId) +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +func buildHandoverRequiredTransfer() (data ngapType.HandoverRequiredTransfer) { + data.DirectForwardingPathAvailability = new(ngapType.DirectForwardingPathAvailability) + data.DirectForwardingPathAvailability.Value = ngapType.DirectForwardingPathAvailabilityPresentDirectPathAvailable + return data +} + +func getHandoverRequiredTransfer(t *testing.T) []byte { + data := buildHandoverRequiredTransfer() + encodeData, err := aper.MarshalWithParams(data, "valueExt") + if err != nil { + t.Fatalf("aper MarshalWithParams error in GetHandoverRequiredTransfer: %+v", err) + } + return encodeData +} + +func buildSourceToTargetTransparentTransfer( + targetGNBID []byte, targetCellID []byte, +) (data ngapType.SourceNGRANNodeToTargetNGRANNodeTransparentContainer) { + // RRC Container + data.RRCContainer.Value = aper.OctetString("\x00\x00\x11") + + // PDU Session Resource Information List + data.PDUSessionResourceInformationList = new(ngapType.PDUSessionResourceInformationList) + infoItem := ngapType.PDUSessionResourceInformationItem{} + infoItem.PDUSessionID.Value = int64(pduSessionID) + qosItem := ngapType.QosFlowInformationItem{} + qosItem.QosFlowIdentifier.Value = 1 + infoItem.QosFlowInformationList.List = append(infoItem.QosFlowInformationList.List, qosItem) + data.PDUSessionResourceInformationList.List = append(data.PDUSessionResourceInformationList.List, infoItem) + + // Target Cell ID + data.TargetCellID.Present = ngapType.TargetIDPresentTargetRANNodeID + data.TargetCellID.NRCGI = new(ngapType.NRCGI) + data.TargetCellID.NRCGI.PLMNIdentity = TestPlmn + data.TargetCellID.NRCGI.NRCellIdentity.Value = aper.BitString{ + Bytes: append(targetGNBID, targetCellID...), + BitLength: 36, + } + + // UE History Information + lastVisitedCellItem := ngapType.LastVisitedCellItem{} + lastVisitedCellInfo := &lastVisitedCellItem.LastVisitedCellInformation + lastVisitedCellInfo.Present = ngapType.LastVisitedCellInformationPresentNGRANCell + lastVisitedCellInfo.NGRANCell = new(ngapType.LastVisitedNGRANCellInformation) + ngRanCell := lastVisitedCellInfo.NGRANCell + ngRanCell.GlobalCellID.Present = ngapType.NGRANCGIPresentNRCGI + ngRanCell.GlobalCellID.NRCGI = new(ngapType.NRCGI) + ngRanCell.GlobalCellID.NRCGI.PLMNIdentity = TestPlmn + ngRanCell.GlobalCellID.NRCGI.NRCellIdentity.Value = aper.BitString{ + Bytes: []byte{0x00, 0x00, 0x00, 0x00, 0x10}, + BitLength: 36, + } + ngRanCell.CellType.CellSize.Value = ngapType.CellSizePresentVerysmall + ngRanCell.TimeUEStayedInCell.Value = 10 + + data.UEHistoryInformation.List = append(data.UEHistoryInformation.List, lastVisitedCellItem) + return data +} + +func getSourceToTargetTransparentTransfer(targetGNBID []byte, targetCellID []byte, t *testing.T) []byte { + data := buildSourceToTargetTransparentTransfer(targetGNBID, targetCellID) + encodeData, err := aper.MarshalWithParams(data, "valueExt") + if err != nil { + t.Fatalf("MarshalWithParams error in GetSourceToTargetTransparentTransfer: %+v\ndata: %+v", err, data) + } + return encodeData +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +func buildHandoverRequiredNGAPBinaryData(t *testing.T) []byte { + targetGNBID := []byte{0x00, 0x01, 0x02} + targetCellID := []byte{0x01, 0x20} + handoverRequiredTransfer := getHandoverRequiredTransfer(t) + sourceToTargetTransparentContainer := getSourceToTargetTransparentTransfer(targetGNBID, targetCellID, t) + + pdu := ngapType.NGAPPDU{ + Present: ngapType.NGAPPDUPresentInitiatingMessage, + InitiatingMessage: &ngapType.InitiatingMessage{ + ProcedureCode: ngapType.ProcedureCode{ + Value: ngapType.ProcedureCodeHandoverPreparation, + }, + Criticality: ngapType.Criticality{ + Value: ngapType.CriticalityPresentReject, + }, + Value: ngapType.InitiatingMessageValue{ + Present: ngapType.InitiatingMessagePresentHandoverRequired, + HandoverRequired: &ngapType.HandoverRequired{}, + }, + }, + } + + hoRequired := pdu.InitiatingMessage.Value.HandoverRequired + + // AMF UE NGAP ID + ie := ngapType.HandoverRequiredIEs{} + + ie.Id.Value = ngapType.ProtocolIEIDAMFUENGAPID + ie.Criticality.Value = ngapType.CriticalityPresentIgnore + ie.Value = ngapType.HandoverRequiredIEsValue{ + Present: ngapType.HandoverRequiredIEsPresentAMFUENGAPID, + AMFUENGAPID: &ngapType.AMFUENGAPID{ + Value: 2, + }, + } + + hoRequired.ProtocolIEs.List = append(hoRequired.ProtocolIEs.List, ie) + + // RAN UE NGAP ID + ie = ngapType.HandoverRequiredIEs{} + + ie.Id.Value = ngapType.ProtocolIEIDRANUENGAPID + ie.Criticality.Value = ngapType.CriticalityPresentIgnore + ie.Value = ngapType.HandoverRequiredIEsValue{ + Present: ngapType.HandoverRequiredIEsPresentRANUENGAPID, + RANUENGAPID: &ngapType.RANUENGAPID{ + Value: 1, + }, + } + + hoRequired.ProtocolIEs.List = append(hoRequired.ProtocolIEs.List, ie) + + // Handover Type + ie = ngapType.HandoverRequiredIEs{} + + ie.Id.Value = ngapType.ProtocolIEIDHandoverType + ie.Criticality.Value = ngapType.CriticalityPresentReject + ie.Value = ngapType.HandoverRequiredIEsValue{ + Present: ngapType.HandoverRequiredIEsPresentHandoverType, + HandoverType: &ngapType.HandoverType{ + Value: ngapType.HandoverTypePresentIntra5gs, + }, + } + + hoRequired.ProtocolIEs.List = append(hoRequired.ProtocolIEs.List, ie) + + // Cause + ie = ngapType.HandoverRequiredIEs{} + + ie.Id.Value = ngapType.ProtocolIEIDCause + ie.Criticality.Value = ngapType.CriticalityPresentIgnore + ie.Value = ngapType.HandoverRequiredIEsValue{ + Present: ngapType.HandoverRequiredIEsPresentCause, + Cause: &ngapType.Cause{ + Present: ngapType.CausePresentRadioNetwork, + RadioNetwork: &ngapType.CauseRadioNetwork{ + Value: ngapType.CauseRadioNetworkPresentHandoverDesirableForRadioReason, + }, + }, + } + + hoRequired.ProtocolIEs.List = append(hoRequired.ProtocolIEs.List, ie) + + // Target ID + ie = ngapType.HandoverRequiredIEs{} + + ie.Id.Value = ngapType.ProtocolIEIDTargetID + ie.Criticality.Value = ngapType.CriticalityPresentReject + ie.Value = ngapType.HandoverRequiredIEsValue{ + Present: ngapType.HandoverRequiredIEsPresentTargetID, + TargetID: &ngapType.TargetID{ + Present: ngapType.TargetIDPresentTargetRANNodeID, + TargetRANNodeID: &ngapType.TargetRANNodeID{ + GlobalRANNodeID: ngapConvert.RanIDToNgap(RanId), + SelectedTAI: ngapType.TAI{ + PLMNIdentity: TestPlmn, + TAC: ngapType.TAC{ + Value: aper.OctetString("\x30\x33\x99"), + }, + }, + }, + }, + } + + hoRequired.ProtocolIEs.List = append(hoRequired.ProtocolIEs.List, ie) + + // PDU Session Resource List HO Rqd + ie = ngapType.HandoverRequiredIEs{} + + ie.Id.Value = ngapType.ProtocolIEIDPDUSessionResourceListHORqd + ie.Criticality.Value = ngapType.CriticalityPresentReject + ie.Value = ngapType.HandoverRequiredIEsValue{ + Present: ngapType.HandoverRequiredIEsPresentPDUSessionResourceListHORqd, + PDUSessionResourceListHORqd: &ngapType.PDUSessionResourceListHORqd{ + List: []ngapType.PDUSessionResourceItemHORqd{ + { + PDUSessionID: ngapType.PDUSessionID{ + Value: int64(pduSessionID), + }, + HandoverRequiredTransfer: handoverRequiredTransfer, + }, + }, + }, + } + + hoRequired.ProtocolIEs.List = append(hoRequired.ProtocolIEs.List, ie) + + // Source To Target Transparent Container + ie = ngapType.HandoverRequiredIEs{} + + ie.Id.Value = ngapType.ProtocolIEIDSourceToTargetTransparentContainer + ie.Criticality.Value = ngapType.CriticalityPresentReject + ie.Value = ngapType.HandoverRequiredIEsValue{ + Present: ngapType.HandoverRequiredIEsPresentSourceToTargetTransparentContainer, + SourceToTargetTransparentContainer: &ngapType.SourceToTargetTransparentContainer{ + Value: sourceToTargetTransparentContainer, + }, + } + + hoRequired.ProtocolIEs.List = append(hoRequired.ProtocolIEs.List, ie) + + binaryData, err := ngap.Encoder(pdu) + if err != nil { + t.Fatalf("NGAP Binary data encoding failure. (%+v)\n", err) + panic(err) + } + + return binaryData +} + +func buildHandoverRequestAcknowledgeTransfer(t *testing.T) (binaryData []byte) { + hoReqAckTransfer := ngapType.HandoverRequestAcknowledgeTransfer{} + + // DL NG-U UP TNL Information + hoReqAckTransfer.DLNGUUPTNLInformation.Present = ngapType.UPTransportLayerInformationPresentGTPTunnel + hoReqAckTransfer.DLNGUUPTNLInformation.GTPTunnel = &ngapType.GTPTunnel{} + + teidOct := make([]byte, 4) // ref: TS 29.502 V17.1.0 6.1.6.3.2 Simple data types + binary.BigEndian.PutUint32(teidOct, uint32(0x5bd60076)) + + gtpTunnel := hoReqAckTransfer.DLNGUUPTNLInformation.GTPTunnel + gtpTunnel.TransportLayerAddress.Value.Bytes = net.IP{127, 0, 0, 8} + gtpTunnel.TransportLayerAddress.Value.BitLength = uint64(len(net.IP{127, 0, 0, 8}) * 8) + gtpTunnel.GTPTEID.Value = teidOct + + // QoS Flow Setup Response List + item := ngapType.QosFlowItemWithDataForwarding{} + item.QosFlowIdentifier.Value = int64(1) + hoReqAckTransfer.QosFlowSetupResponseList.List = append(hoReqAckTransfer.QosFlowSetupResponseList.List, item) + + binaryData, err := aper.MarshalWithParams(hoReqAckTransfer, "valueExt") + if err != nil { + t.Fatalf("aper MarshalWithParams error in GetHandoverRequiredTransfer: %+v", err) + } + + return binaryData +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +func TestHandleCreateUEContextRequest(t *testing.T) { + logrus.SetLevel(logrus.TraceLevel) + + openapi.InterceptH2CClient() + defer openapi.RestoreH2CClient() + + acceptCh, stopListener := startSctpMockRAN(t, RanListenerIP) + + initAMFConfig(t) + + // get accepted conn + conn := <-acceptCh + if conn == nil { + t.Fatalf("mock RAN did not accept connection") + } + + defer func() { + wg.Wait() + + time.Sleep(50 * time.Millisecond) + + if err := conn.Close(); err == nil { + t.Log("[T-RAN] SCTP conn closed\n", + "Don't bother the above \"SCTPConn: SCTPWrite failed bad file descriptor\" message") + } + + time.Sleep(100 * time.Millisecond) + + stopListener() + t.Log("[T-RAN] SCTP listener closed\n", + "Don't bother the above \"SCTP: Failed to shutdown fd operation not supported\" message") + }() + + // Mock T-RAN: Capture Handover Request NGAP message, and respond with Handover Request Acknowledge. + // read loop (in goroutine) to capture NGAP messages sent by AMF + wg.Add(1) + go func() { + defer wg.Done() + + buf := make([]byte, 65536) + + n, err := conn.Read(buf) + if err != nil { + t.Logf("conn.Read error: %+v", err) + return + } + pdu, err := ngap.Decoder(buf[:n]) + if err != nil { + t.Logf("ngap decode error: %+v", err) + return + } + + // extract HANDOVER REQUEST IEs + var amfUeNgapId *ngapType.AMFUENGAPID + // var handoverType *ngapType.HandoverType + // var ueAggregateMaximumBitRate *ngapType.UEAggregateMaximumBitRate + // var cause *ngapType.Cause + // var ueSecurityCapabilities *ngapType.UESecurityCapabilities + // var securityContext *ngapType.SecurityContext + var pduSessionResourceSetupListHOReq *ngapType.PDUSessionResourceSetupListHOReq + // var allowedNSSAI *ngapType.AllowedNSSAI + // var sourceToTargetTransparentContainer *ngapType.SourceToTargetTransparentContainer + // var guami *ngapType.GUAMI + + for _, ie := range pdu.InitiatingMessage.Value.HandoverRequest.ProtocolIEs.List { + switch ie.Id.Value { + case ngapType.ProtocolIEIDAMFUENGAPID: // mandatory, reject + amfUeNgapId = ie.Value.AMFUENGAPID + case ngapType.ProtocolIEIDHandoverType: // mandatory, reject + // handoverType = ie.Value.HandoverType + case ngapType.ProtocolIEIDCause: // mandatory, ignore + // cause = ie.Value.Cause + case ngapType.ProtocolIEIDUEAggregateMaximumBitRate: // mandatory, reject + // ueAggregateMaximumBitRate = ie.Value.UEAggregateMaximumBitRate + case ngapType.ProtocolIEIDUESecurityCapabilities: // mandatory, reject + // ueSecurityCapabilities = ie.Value.UESecurityCapabilities + case ngapType.ProtocolIEIDSecurityContext: // mandatory, reject + // securityContext = ie.Value.SecurityContext + case ngapType.ProtocolIEIDPDUSessionResourceSetupListHOReq: // mandatory, reject + pduSessionResourceSetupListHOReq = ie.Value.PDUSessionResourceSetupListHOReq + case ngapType.ProtocolIEIDAllowedNSSAI: // mandatory, reject + // allowedNSSAI = ie.Value.AllowedNSSAI + case ngapType.ProtocolIEIDSourceToTargetTransparentContainer: // mandatory, reject + // sourceToTargetTransparentContainer = ie.Value.SourceToTargetTransparentContainer + case ngapType.ProtocolIEIDGUAMI: // mandatory, reject + // guami = ie.Value.GUAMI + default: + switch ie.Criticality.Value { + case ngapType.CriticalityPresentReject: + t.Errorf("Not comprehended IE ID 0x%04x (criticality: reject)", ie.Id.Value) + case ngapType.CriticalityPresentIgnore: + t.Logf("Not comprehended IE ID 0x%04x (criticality: ignore)", ie.Id.Value) + case ngapType.CriticalityPresentNotify: + t.Logf("Not comprehended IE ID 0x%04x (criticality: notify)", ie.Id.Value) + } + } + } + + // Create HANDOVER REQUEST ACKNOWLEDGE message in response to HANDOVER REQUEST + hoReqAckPdu := ngapType.NGAPPDU{} + hoReqAckPdu.Present = ngapType.NGAPPDUPresentSuccessfulOutcome + hoReqAckPdu.SuccessfulOutcome = &ngapType.SuccessfulOutcome{} + + hoReqAckPdu.SuccessfulOutcome.ProcedureCode.Value = ngapType.ProcedureCodeHandoverResourceAllocation + hoReqAckPdu.SuccessfulOutcome.Criticality.Value = ngapType.CriticalityPresentReject + hoReqAckPdu.SuccessfulOutcome.Value.Present = ngapType.SuccessfulOutcomePresentHandoverRequestAcknowledge + hoReqAckPdu.SuccessfulOutcome.Value.HandoverRequestAcknowledge = &ngapType.HandoverRequestAcknowledge{} + + hoReqAckIe := hoReqAckPdu.SuccessfulOutcome.Value.HandoverRequestAcknowledge + + // AMF UE NGAP ID + ie := ngapType.HandoverRequestAcknowledgeIEs{} + ie.Id.Value = ngapType.ProtocolIEIDAMFUENGAPID + ie.Criticality.Value = ngapType.CriticalityPresentIgnore + ie.Value.Present = ngapType.HandoverRequestAcknowledgeIEsPresentAMFUENGAPID + + ie.Value.AMFUENGAPID = amfUeNgapId + + hoReqAckIe.ProtocolIEs.List = append(hoReqAckIe.ProtocolIEs.List, ie) + + // RAN UE NGAP ID + ie = ngapType.HandoverRequestAcknowledgeIEs{} + ie.Id.Value = ngapType.ProtocolIEIDRANUENGAPID + ie.Criticality.Value = ngapType.CriticalityPresentIgnore + ie.Value.Present = ngapType.HandoverRequestAcknowledgeIEsPresentRANUENGAPID + ie.Value.RANUENGAPID = &ngapType.RANUENGAPID{ + Value: 1, + } + + hoReqAckIe.ProtocolIEs.List = append(hoReqAckIe.ProtocolIEs.List, ie) + + // PDU Session Resource Admitted List + ie = ngapType.HandoverRequestAcknowledgeIEs{} + ie.Id.Value = ngapType.ProtocolIEIDPDUSessionResourceAdmittedList + ie.Criticality.Value = ngapType.CriticalityPresentIgnore + ie.Value.Present = ngapType.HandoverRequestAcknowledgeIEsPresentPDUSessionResourceAdmittedList + + ie.Value.PDUSessionResourceAdmittedList = &ngapType.PDUSessionResourceAdmittedList{} + if pduSessionResourceSetupListHOReq != nil { + for _, setupItem := range pduSessionResourceSetupListHOReq.List { + admittedItem := ngapType.PDUSessionResourceAdmittedItem{ + PDUSessionID: setupItem.PDUSessionID, + HandoverRequestAcknowledgeTransfer: buildHandoverRequestAcknowledgeTransfer(t), + } + ie.Value.PDUSessionResourceAdmittedList.List = append(ie.Value.PDUSessionResourceAdmittedList.List, admittedItem) + } + } + + hoReqAckIe.ProtocolIEs.List = append(hoReqAckIe.ProtocolIEs.List, ie) + + // PDU Session Resource Failed to Setup List (optional) + + // Target to Source Transparent Container + ie = ngapType.HandoverRequestAcknowledgeIEs{} + ie.Id.Value = ngapType.ProtocolIEIDTargetToSourceTransparentContainer + ie.Criticality.Value = ngapType.CriticalityPresentReject + ie.Value.Present = ngapType.HandoverRequestAcknowledgeIEsPresentTargetToSourceTransparentContainer + + // TODO + ie.Value.TargetToSourceTransparentContainer = &ngapType.TargetToSourceTransparentContainer{} + + hoReqAckIe.ProtocolIEs.List = append(hoReqAckIe.ProtocolIEs.List, ie) + + // set SCTP PPID = ngap.PPID (so AMF's SCTP reader treats payload as NGAP) + if info, errGetParam := conn.GetDefaultSentParam(); errGetParam == nil { + info.PPID = ngap.PPID + if err2 := conn.SetDefaultSentParam(info); err2 != nil { + t.Logf("SetDefaultSentParam error: %+v", err2) + } + } else { + // GetDefaultSentParam may fail for some libs; still try SetDefaultSentParam with new info + if errGetParam = conn.SetDefaultSentParam(&sctp.SndRcvInfo{PPID: ngap.PPID}); errGetParam != nil { + t.Errorf("SCTP Conn SetDefaultSentParam error: %s", errGetParam.Error()) + } + } + + buff, err := ngap.Encoder(hoReqAckPdu) + if err != nil { + t.Errorf("ngap encode error: %+v", err) + } + + _, err = conn.Write(buff) + if err != nil { + t.Errorf("conn.Write error: %+v", err) + } + t.Log("sent HANDOVER REQUEST ACKNOWLEDGE") + }() + + // Setup mock AMF + mockAMF := service.NewMockAmfAppInterface(gomock.NewController(t)) + consumer, err := consumer.NewConsumer(mockAMF) + if err != nil { + t.Fatalf("Failed to create consumer: %+v", err) + } + + processor, err := processor.NewProcessor(mockAMF) + if err != nil { + t.Fatalf("Failed to create processor: %+v", err) + } + + service.AMF = mockAMF + + mockAMF.EXPECT().Context().Return(amf_context.GetSelf()).AnyTimes() + mockAMF.EXPECT().Consumer().Return(consumer).AnyTimes() + + // create SmContext for UE + smfApiRoot := "http://127.0.0.1" + smCtxReference := uuid.New().URN() + smfInstanceId := uuid.New().String() + + // Setup mock SMF response for UpdateSmContextHandoverBetweenAMF + defer gock.Off() // Flush pending mocks after test execution + + // build binary data for N2 Information + resourceSetupRequestTransfer := ngapType.PDUSessionResourceSetupRequestTransfer{} + // PDU Session Aggregate Maximum Bit Rate (Conditional) + // UL NG-U UP TNL Information + ie := ngapType.PDUSessionResourceSetupRequestTransferIEs{} + ie.Id.Value = ngapType.ProtocolIEIDULNGUUPTNLInformation + ie.Criticality.Value = ngapType.CriticalityPresentReject + n3IP := net.IP{127, 0, 0, 8} // ref: config/upfcfg.yaml + teidOct := make([]byte, 4) // ref: TS 29.502 V17.1.0 6.1.6.3.2 Simple data types + binary.BigEndian.PutUint32(teidOct, uint32(0x5bd60076)) + ie.Value = ngapType.PDUSessionResourceSetupRequestTransferIEsValue{ + Present: ngapType.PDUSessionResourceSetupRequestTransferIEsPresentULNGUUPTNLInformation, + ULNGUUPTNLInformation: &ngapType.UPTransportLayerInformation{ + Present: ngapType.UPTransportLayerInformationPresentGTPTunnel, + GTPTunnel: &ngapType.GTPTunnel{ + TransportLayerAddress: ngapType.TransportLayerAddress{ + Value: aper.BitString{ + Bytes: n3IP, + BitLength: uint64(len(n3IP) * 8), + }, + }, + GTPTEID: ngapType.GTPTEID{Value: teidOct}, + }, + }, + } + resourceSetupRequestTransfer.ProtocolIEs.List = append(resourceSetupRequestTransfer.ProtocolIEs.List, ie) + + // PDU Session Type + ie = ngapType.PDUSessionResourceSetupRequestTransferIEs{} + ie.Id.Value = ngapType.ProtocolIEIDPDUSessionType + ie.Criticality.Value = ngapType.CriticalityPresentReject + ie.Value = ngapType.PDUSessionResourceSetupRequestTransferIEsValue{ + Present: ngapType.PDUSessionResourceSetupRequestTransferIEsPresentPDUSessionType, + PDUSessionType: &ngapType.PDUSessionType{ + Value: ngapType.PDUSessionTypePresentIpv4, + }, + } + resourceSetupRequestTransfer.ProtocolIEs.List = append(resourceSetupRequestTransfer.ProtocolIEs.List, ie) + + // QoS Flow Setup Request List + // use Default 5qi, arp + + authDefQos := models.AuthorizedDefaultQos{ + Var5qi: 9, + Arp: &models.Arp{ + PriorityLevel: 8, + }, + PriorityLevel: 8, + } // ref: smf/internal/sbi/processor/pdu_session_test.go; sessRule.AuthDefQos + ie = ngapType.PDUSessionResourceSetupRequestTransferIEs{} + ie.Id.Value = ngapType.ProtocolIEIDQosFlowSetupRequestList + ie.Criticality.Value = ngapType.CriticalityPresentReject + ie.Value = ngapType.PDUSessionResourceSetupRequestTransferIEsValue{ + Present: ngapType.PDUSessionResourceSetupRequestTransferIEsPresentQosFlowSetupRequestList, + QosFlowSetupRequestList: &ngapType.QosFlowSetupRequestList{ + List: []ngapType.QosFlowSetupRequestItem{ + { + QosFlowIdentifier: ngapType.QosFlowIdentifier{ + // ref: smf/internal/context/session_rules.go; int64(sessRule.DefQosQFI), + Value: int64(1), + }, + QosFlowLevelQosParameters: ngapType.QosFlowLevelQosParameters{ + QosCharacteristics: ngapType.QosCharacteristics{ + Present: ngapType.QosCharacteristicsPresentNonDynamic5QI, + NonDynamic5QI: &ngapType.NonDynamic5QIDescriptor{ + FiveQI: ngapType.FiveQI{ + Value: int64(authDefQos.Var5qi), + }, + }, + }, + AllocationAndRetentionPriority: ngapType.AllocationAndRetentionPriority{ + PriorityLevelARP: ngapType.PriorityLevelARP{ + Value: int64(authDefQos.Arp.PriorityLevel), + }, + PreEmptionCapability: ngapType.PreEmptionCapability{ + Value: ngapType.PreEmptionCapabilityPresentShallNotTriggerPreEmption, + }, + PreEmptionVulnerability: ngapType.PreEmptionVulnerability{ + Value: ngapType.PreEmptionVulnerabilityPresentNotPreEmptable, + }, + }, + }, + }, + }, + }, + } + resourceSetupRequestTransfer.ProtocolIEs.List = append(resourceSetupRequestTransfer.ProtocolIEs.List, ie) + + n2Buf, err := aper.MarshalWithParams(resourceSetupRequestTransfer, "valueExt") + if err != nil { + fmt.Printf("encode resourceSetupRequestTransfer failed: %s", err) + } + + // Build multipart/related message + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + + // part 1: JSON metadata + h1 := textproto.MIMEHeader{} + h1.Set("Content-Type", "application/json") + p1, err := mw.CreatePart(h1) + if err != nil { + t.Errorf("UpdateSmContextHandoverBetweenAMF response Create Part 1 error: %s", err.Error()) + } + jsonBuf, err := json.Marshal(models.SmContextUpdatedData{ + HoState: models.HoState_PREPARING, + N2SmInfoType: models.N2SmInfoType_PDU_RES_SETUP_REQ, + N2SmInfo: &models.RefToBinaryData{ + ContentId: "PDU_RES_SETUP_REQ", + }, + }) + if err == nil { + if _, errWrite := p1.Write(jsonBuf); errWrite != nil { + t.Errorf("UpdateSmContextHandoverBetweenAMF response write Part 1 error: %s", err.Error()) + } + } + + // part 2: N2 SM info (PDU Session Resource Setup Request Transfer) + h2 := textproto.MIMEHeader{} + h2.Set("Content-Type", "application/vnd.3gpp.ngap") + h2.Set("Content-ID", "PDU_RES_SETUP_REQ") + p2, err := mw.CreatePart(h2) + if err != nil { + t.Errorf("UpdateSmContextHandoverBetweenAMF response Create Part 2 error: %s", err.Error()) + } + if _, err = p2.Write(n2Buf); err != nil { + t.Errorf("UpdateSmContextHandoverBetweenAMF response write Part 2 error: %s", err.Error()) + } + + // Write the closing boundary + if err = mw.Close(); err != nil { + t.Errorf("multipart writer Close() error: %s", err.Error()) + } + + gock.New(smfApiRoot). + Post("/nsmf-pdusession/v1/sm-contexts/"+smCtxReference). + Times(1). + Reply(200). + SetHeader("Content-Type", fmt.Sprintf("multipart/related; boundary=%s", mw.Boundary())). + Body(bytes.NewReader(buf.Bytes())) + + // Create SMF response for UpdateSmContextN2HandoverPrepared + var buff bytes.Buffer + mw2 := multipart.NewWriter(&buff) + + // part 1: JSON metadata + hd1 := textproto.MIMEHeader{} + hd1.Set("Content-Type", "application/json") + pt1, err := mw2.CreatePart(hd1) + if err != nil { + t.Errorf("UpdateSmContextN2HandoverPrepared response Create Part 1 error: %s", err.Error()) + } + jsonBuff, err := json.Marshal(models.SmContextUpdatedData{ + HoState: models.HoState_PREPARED, + N2SmInfoType: models.N2SmInfoType_HANDOVER_CMD, + N2SmInfo: &models.RefToBinaryData{ + ContentId: "HANDOVER_CMD", + }, + }) + if err == nil { + if _, err = pt1.Write(jsonBuff); err != nil { + t.Errorf("UpdateSmContextN2HandoverPrepared response write Part 1 error: %s", err.Error()) + } + } + + // part 2: N2 SM info (PDU Session Resource Setup Request Transfer) + hd2 := textproto.MIMEHeader{} + hd2.Set("Content-Type", "application/vnd.3gpp.ngap") + hd2.Set("Content-ID", "HANDOVER_CMD") + pt2, err := mw2.CreatePart(hd2) + if err != nil { + t.Errorf("UpdateSmContextN2HandoverPrepared response Create Part 2 error: %s", err.Error()) + } + if _, err = pt2.Write(n2Buf); err != nil { + t.Errorf("UpdateSmContextN2HandoverPrepared response write Part 2 error: %s", err.Error()) + } + + // Write the closing boundary + if err = mw2.Close(); err != nil { + t.Errorf("multipart writer2 Close() error: %s", err.Error()) + } + + gock.New(smfApiRoot). + Post("/nsmf-pdusession/v1/sm-contexts/"+smCtxReference). + Times(1). + Reply(200). + SetHeader("Content-Type", fmt.Sprintf("multipart/related; boundary=%s", mw2.Boundary())). + Body(bytes.NewReader(buff.Bytes())) + + CreateUeContextRequest := models.CreateUeContextRequest{ + JsonData: &models.UeContextCreateData{ + UeContext: &models.UeContext{ + Supi: "imsi-2089300007487", + RestrictedRatList: []models.RatType{ + models.RatType_NR, + }, + Pei: "imeisv-1234567890123412", + MmContextList: []models.MmContext{ + { + AccessType: models.AccessType__3_GPP_ACCESS, + NasSecurityMode: &models.NasSecurityMode{ + IntegrityAlgorithm: models.IntegrityAlgorithm_NIA2, + CipheringAlgorithm: models.CipheringAlgorithm_NEA0, + }, // genNasSecurityMode(), + UeSecurityCapability: base64.StdEncoding.EncodeToString([]uint8{0x00, 0x00}), + }, + }, + SessionContextList: []models.PduSessionContext{ + { + PduSessionId: pduSessionID, + SmContextRef: smfApiRoot + "/nsmf-pdusession/v1/sm-contexts/" + smCtxReference, + SNssai: &testConfig.Configuration.PlmnSupportList[0].SNssaiList[0], + Dnn: testConfig.Configuration.SupportDnnList[0], + AccessType: models.AccessType__3_GPP_ACCESS, + // NsInstance: , + PlmnId: &plmnIdModel, + SmfServiceInstanceId: smfInstanceId, + // HsmfId: , + // VsmfId: , + }, + }, + SubUeAmbr: &models.Ambr{ + Uplink: "1000 Mbps", + Downlink: "1000 Mbps", + }, + ServiceAreaRestriction: &models.ServiceAreaRestriction{ + RestrictionType: models.RestrictionType_ALLOWED_AREAS, + Areas: []models.Area{ + { + Tacs: []string{"000001"}, + AreaCode: "+886", // free5gc/test/consumerTestdata/PCF/TestAMPolicy/AMPolicy.go + }, + }, + }, + }, + TargetId: &models.NgRanTargetId{ + RanNodeId: &RanId, + Tai: &models.Tai{ + PlmnId: &models.PlmnId{ + Mcc: "208", + Mnc: "93", + }, + Tac: "000001", + }, + }, + SourceToTargetData: &models.N2InfoContent{ + NgapMessageType: 0, + NgapIeType: "HANDOVER_REQUIRED", + NgapData: &models.RefToBinaryData{ + ContentId: "N2SmInfo", + }, + }, + PduSessionList: []models.N2SmInformation{ + { + PduSessionId: pduSessionID, + }, + }, + N2NotifyUri: "127.0.0.1", + UeRadioCapability: nil, + NgapCause: &models.NgApCause{ + Group: 0, // radioNetwork, based on TS 29.571 Type: NgApCause + Value: int32(ngapType.CauseRadioNetworkPresentNgIntraSystemHandoverTriggered), + }, + SupportedFeatures: "", + }, + BinaryDataN2Information: buildHandoverRequiredNGAPBinaryData(t), + } + testCases := []struct { + testDescription string + resultDescription string + request models.CreateUeContextRequest + responseBody any + expectedHTTPResp *httpwrapper.Response + }{ + { + testDescription: "Valid Request", + resultDescription: "", + request: CreateUeContextRequest, + responseBody: &models.CreateUeContextResponse201{}, + expectedHTTPResp: &httpwrapper.Response{ + Header: nil, + Status: http.StatusCreated, + Body: models.CreateUeContextResponse201{ + JsonData: &models.UeContextCreatedData{ + UeContext: &models.UeContext{ + Supi: CreateUeContextRequest.JsonData.UeContext.Supi, + Pei: CreateUeContextRequest.JsonData.UeContext.Pei, + SubUeAmbr: CreateUeContextRequest.JsonData.UeContext.SubUeAmbr, + }, + TargetToSourceData: &models.N2InfoContent{ + NgapIeType: models.AmfCommunicationNgapIeType_TAR_TO_SRC_CONTAINER, + NgapData: &models.RefToBinaryData{ + ContentId: "N2InfoContent", + }, + }, + PduSessionList: CreateUeContextRequest.JsonData.PduSessionList, + }, + }, + }, + }, + } + + // Run test cases + for _, tc := range testCases { + t.Run(tc.testDescription, func(t *testing.T) { + httpRecorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(httpRecorder) + + processor.HandleCreateUEContextRequest(c, tc.request) + + httpResp := httpRecorder.Result() + if errClose := httpResp.Body.Close(); errClose != nil { + t.Fatalf("Failed to close response body: %+v", errClose) + } + + rawBytes, errReadAll := io.ReadAll(httpResp.Body) + if errReadAll != nil { + t.Fatalf("Failed to read response body: %+v", errReadAll) + } + + err = openapi.Deserialize(tc.responseBody, rawBytes, httpResp.Header.Get("Content-Type")) + if err != nil { + t.Fatalf("Failed to deserialize response body: %+v", err) + } + + respBytes, errMarshal := json.Marshal(tc.responseBody) + if errMarshal != nil { + t.Fatalf("Failed to marshal actual response body: %+v", errMarshal) + } + + expectedBytes, errMarshal := json.Marshal(tc.expectedHTTPResp.Body) + if errMarshal != nil { + t.Fatalf("Failed to marshal expected response body: %+v", errMarshal) + } + + require.Equal(t, tc.expectedHTTPResp.Status, httpResp.StatusCode) + require.Equal(t, expectedBytes, respBytes) + + // wait for another go-routine to execute following procedure + time.Sleep(100 * time.Millisecond) + }) + } + + require.NoError(t, err) +} diff --git a/internal/sbi/processor/ue_context.go b/internal/sbi/processor/ue_context.go index 9d3a45fa..5491850b 100644 --- a/internal/sbi/processor/ue_context.go +++ b/internal/sbi/processor/ue_context.go @@ -4,7 +4,9 @@ import ( "encoding/base64" "encoding/hex" "net/http" + "net/url" "strings" + "time" "github.com/gin-gonic/gin" @@ -12,7 +14,10 @@ import ( gmm_common "github.com/free5gc/amf/internal/gmm/common" "github.com/free5gc/amf/internal/logger" "github.com/free5gc/amf/internal/nas/nas_security" + ngap_message "github.com/free5gc/amf/internal/ngap/message" + "github.com/free5gc/aper" "github.com/free5gc/nas/security" + "github.com/free5gc/ngap/ngapType" "github.com/free5gc/openapi/models" "github.com/free5gc/util/metrics/sbi" ) @@ -41,6 +46,7 @@ func (p *Processor) CreateUEContextProcedure(ueContextID string, createUeContext if ueContextCreateData.UeContext == nil || ueContextCreateData.TargetId == nil || ueContextCreateData.PduSessionList == nil || ueContextCreateData.SourceToTargetData == nil || ueContextCreateData.N2NotifyUri == "" { + logger.CommLog.Errorf("Missing mandatory fields in UeContextCreateData: %+v", ueContextCreateData) ueCtxCreateError := models.UeContextCreateError{ Error: &models.ProblemDetails{ Status: http.StatusForbidden, @@ -57,24 +63,43 @@ func (p *Processor) CreateUEContextProcedure(ueContextID string, createUeContext ue.Lock.Lock() defer ue.Lock.Unlock() - // amfSelf.AmfRanSetByRanId(*ueContextCreateData.TargetId.RanNodeId) - // ue.N1N2Message[ueContextId] = &context.N1N2Message{} - // ue.N1N2Message[ueContextId].Request.JsonData = &models.N1N2MessageTransferReqData{ - // N2InfoContainer: &models.N2InfoContainer{ - // SmInfo: &models.N2SmInformation{ - // N2InfoContent: ueContextCreateData.SourceToTargetData, - // }, - // }, - // } ue.HandoverNotifyUri = ueContextCreateData.N2NotifyUri - amfSelf.AmfRanFindByRanID(*ueContextCreateData.TargetId.RanNodeId) + targetRan, ok := amfSelf.AmfRanFindByRanID(*ueContextCreateData.TargetId.RanNodeId) + if !ok { + logger.CommLog.Errorf("Target RAN Node ID not found, TargetRanNodeId: %+v", + ueContextCreateData.TargetId.RanNodeId) + logger.CommLog.Errorf("TargetRanNodeId.PlmnId: %+v", + ueContextCreateData.TargetId.RanNodeId.PlmnId) + logger.CommLog.Errorf("TargetRanNodeId.GNbId: %+v", + ueContextCreateData.TargetId.RanNodeId.GNbId) + ueContextCreateError := &models.CreateUeContextResponse403{ + JsonData: &models.UeContextCreateError{ + Error: &models.ProblemDetails{ + Status: http.StatusForbidden, + Cause: "HANDOVER_FAILURE", + }, + }, + } + return nil, ueContextCreateError + } supportedTAI := context.NewSupportedTAI() supportedTAI.Tai.Tac = ueContextCreateData.TargetId.Tai.Tac supportedTAI.Tai.PlmnId = ueContextCreateData.TargetId.Tai.PlmnId // ue.N1N2MessageSubscribeInfo[ueContextID] = &models.UeN1N2InfoSubscriptionCreateData{ // N2NotifyCallbackUri: ueContextCreateData.N2NotifyUri, // } + + ue.CopyDataFromUeContextModel(ueContextCreateData.UeContext) + ue.PlmnId = *targetRan.RanId.PlmnId + ue.AmPolicyAssociation = new(models.PcfAmPolicyControlPolicyAssociation) + logger.CommLog.Infof("pointer of AmPolicyAssociation: %p", ue.AmPolicyAssociation) + + ue.AmPolicyAssociation.ServAreaRes = new(models.ServiceAreaRestriction) + ueServAreaRes := ue.AmPolicyAssociation.ServAreaRes + ueServAreaRes.RestrictionType = ueContextCreateData.UeContext.ServiceAreaRestriction.RestrictionType + ueServAreaRes.Areas = append(ueServAreaRes.Areas, + ueContextCreateData.UeContext.ServiceAreaRestriction.Areas[0]) ue.UnauthenticatedSupi = ueContextCreateData.UeContext.SupiUnauthInd // should be smInfo list @@ -114,27 +139,168 @@ func (p *Processor) CreateUEContextProcedure(ueContextID string, createUeContext // ueContextCreateData.UeContext.MmContextList // ue.CurPduSession.PduSessionId = ueContextCreateData.UeContext.SessionContextList. // ue.TraceData = ueContextCreateData.UeContext.TraceData - createUeContextResponse := new(models.CreateUeContextResponse201) - createUeContextResponse.JsonData = &models.UeContextCreatedData{ - UeContext: &models.UeContext{ - Supi: ueContextCreateData.UeContext.Supi, - }, + + // TS 23.502 4.9.1.3.2 step 4 + var pduSessionReqList ngapType.PDUSessionResourceSetupListHOReq + + if len(ueContextCreateData.UeContext.SessionContextList) > 0 { + logger.CommLog.Infof("Send HandoverRequiredTransfer to SMF") + for i := range ueContextCreateData.UeContext.SessionContextList { + // Check if the S-NSSAI associated with the PDU session is available in the AMF. + // If not, the T-AMF does not invoke Nsmf_PDUSession_UpdateSMContext for this PDU session. + pduSessionContext := &(ueContextCreateData.UeContext.SessionContextList[i]) + pduSessionID := pduSessionContext.PduSessionId + if pduSessionContext.SNssai != nil && !amfSelf.InPlmnSupportList(*pduSessionContext.SNssai) { + continue + } + + pduSessionContextToAmfSmContext := func(pduSessionContext models.PduSessionContext) ( + smContext *context.SmContext, + ) { + smContext = context.NewSmContext(pduSessionContext.PduSessionId) + if pduSessionContext.SmContextRef != "" { + // Based on TS 29.518 V17.5.0 section 6.1.6.2.37 type: PduSessionContext + // (Refer to TS 29.502 V17.1.0 section 6.1.3.3.2 Resource Definition), + // smContextRef is formatted as follows: + // http://{apiRoot}/nsmf-pdusession/v1/sm-contexts/{smContextRef} + // e.g. "smf.free5gc.org/nsmf-pdusession/v1/sm-contexts/12345678" + logger.CommLog.Infof("smContextRef: %s", pduSessionContext.SmContextRef) + u, err := url.Parse(pduSessionContext.SmContextRef) + if err != nil { + logger.CommLog.Errorf("url Parse error: %s", err.Error()) + } + apiRoot := u.Scheme + "://" + u.Host + smContextRef := strings.TrimPrefix(u.Path, "/nsmf-pdusession/v1/sm-contexts/") + logger.CommLog.Infof("apiRoot: %s, smContextRef: %s", apiRoot, smContextRef) + smContext.SetSmfUri(apiRoot) + smContext.SetSmContextRef(smContextRef) + } + if pduSessionContext.SNssai != nil { + smContext.SetSnssai(*pduSessionContext.SNssai) + } + if pduSessionContext.Dnn != "" { + smContext.SetDnn(pduSessionContext.Dnn) + } + if pduSessionContext.AccessType != "" { + smContext.SetAccessType(pduSessionContext.AccessType) + } + if pduSessionContext.NsInstance != "" { + smContext.SetNsInstance(pduSessionContext.NsInstance) + } + // ueContextCreateData.UeContext.Location is introduced after Rel.18 + if pduSessionContext.PlmnId != nil { + smContext.SetPlmnID(*pduSessionContext.PlmnId) + } + if pduSessionContext.SmfServiceInstanceId != "" { + smContext.SetSmfID(pduSessionContext.SmfServiceInstanceId) + } + if pduSessionContext.HsmfId != "" { + smContext.SetHSmfID(pduSessionContext.HsmfId) + } + if pduSessionContext.VsmfId != "" { + smContext.SetVSmfID(pduSessionContext.VsmfId) + } + return smContext + } + + smContext := pduSessionContextToAmfSmContext(*pduSessionContext) + updateSmContextResponse200, _, _, errSendUpdateSmContext := p.Consumer(). + SendUpdateSmContextHandoverBetweenAMF( + ue, ueContextCreateData.TargetId, smContext, amfSelf.Name, &amfSelf.ServedGuamiList[0], false, + ) + if errSendUpdateSmContext != nil { + logger.CommLog.Errorf("consumer.GetConsumer().SendUpdateSmContextN2HandoverPreparing Error: %+v", + errSendUpdateSmContext) + } + if updateSmContextResponse200 == nil { + logger.CommLog.Errorf("SendUpdateSmContextN2HandoverPreparing Error for pduSessionID[%d]", pduSessionID) + continue + } else if updateSmContextResponse200.BinaryDataN2SmInformation != nil { + ngap_message.AppendPDUSessionResourceSetupListHOReq(&pduSessionReqList, pduSessionID, + smContext.Snssai(), updateSmContextResponse200.BinaryDataN2SmInformation) + + // Store SM Context into UE + ue.SmContextList.Store(pduSessionID, smContext) + } + } + } + if len(pduSessionReqList.List) == 0 { + logger.CommLog.Info("Handle Handover Preparation Failure. No PDU Session in the list.") + return nil, &models.CreateUeContextResponse403{ + JsonData: &models.UeContextCreateError{ + Error: &models.ProblemDetails{ + Status: http.StatusForbidden, + Cause: "HANDOVER_FAILURE", + }, + }, + } } - // response.JsonData.TargetToSourceData = - // ue.N1N2Message[ueContextId].Request.JsonData.N2InfoContainer.SmInfo.N2InfoContent - createUeContextResponse.JsonData.PduSessionList = ueContextCreateData.PduSessionList - createUeContextResponse.JsonData.PcfReselectedInd = false - // TODO: When Target AMF selects a nw PCF for AM policy, set the flag to true. + // Update NH + ue.UpdateSecurityContext(models.AccessType__3_GPP_ACCESS) - // response.UeContext = ueContextCreateData.UeContext - // response.TargetToSourceData = ue.N1N2Message[amfSelf.Uri].Request.JsonData.N2InfoContainer.SmInfo.N2InfoContent - // response.PduSessionList = ueContextCreateData.PduSessionList - // response.PcfReselectedInd = false // TODO:When Target AMF selects a nw PCF for AM policy, set the flag to true. - // + cause := new(ngapType.Cause) + if ueContextCreateData.NgapCause != nil { + // Based on ngapType.Cause{}, 0= Nothing, 1= RadioNetwork, etc. + cause.Present = int(ueContextCreateData.NgapCause.Group) + 1 + cause.RadioNetwork = new(ngapType.CauseRadioNetwork) + cause.RadioNetwork.Value = aper.Enumerated(ueContextCreateData.NgapCause.Value) + } - // return httpwrapper.NewResponse(http.StatusCreated, nil, createUeContextResponse) - return createUeContextResponse, nil + sourceToTargetTransparentContainer := new(ngapType.SourceToTargetTransparentContainer) + sourceToTargetTransparentContainer.Value = aper.OctetString(createUeContextRequest.BinaryDataN2Information) + + // Build RanUe context on T-AMF + var targetRanUe *context.RanUe + if targetUeTmp, err := targetRan.NewRanUe(context.RanUeNgapIdUnspecified); err != nil { + logger.CommLog.Errorf("Create target UE error: %+v", err) + } else { + targetRanUe = targetUeTmp + } + + if targetRanUe != nil { + logger.CommLog.Tracef("Target : AMF_UE_NGAP_ID[%d], RAN_UE_NGAP_ID[Unknown]", targetRanUe.AmfUeNgapId) + } + targetRanUe.AmfUe = ue + targetRanUe.HandOverStartTime = time.Now() + targetRanUe.Log = logger.NgapLog + + // send Handover Request to target RAN + ngap_message.SendHandoverRequestWithAMFChange( + targetRanUe, targetRan, *cause, pduSessionReqList, *sourceToTargetTransparentContainer, false) + + // create channel if not exist + var createUeContextResponse context.PendingHandoverResponse + pendingHOResponseChan := make(chan context.PendingHandoverResponse) + amfSelf.PendingHandovers.Store(ue.Supi, pendingHOResponseChan) + + // waiting 100ms for handover request acknowledge handler to finish. + // If the channel is closed, or the read times out, return the ueContextCreateError at the end of the function. + select { + case createUeContextResponse, ok = <-pendingHOResponseChan: + if ok { + if createUeContextResponse.Response201 != nil { + return createUeContextResponse.Response201, nil + } else if createUeContextResponse.Response403 != nil { + return nil, createUeContextResponse.Response403 + } + } else { + logger.CommLog.Info("PendingHandoverResponse channel closed.") + } + case <-time.After(100 * time.Millisecond): + logger.CommLog.Warnln("timeout: read from pendingHOResponseChan") + } + + ueCtxCreateError := models.UeContextCreateError{ + Error: &models.ProblemDetails{ + Status: http.StatusForbidden, + Cause: "HANDOVER_FAILURE", + }, + } + ueContextCreateError := &models.CreateUeContextResponse403{ + JsonData: &ueCtxCreateError, + } + return nil, ueContextCreateError } // TS 29.518 5.2.2.2.4 @@ -499,7 +665,7 @@ func (p *Processor) buildAmPolicyReqTriggers(triggers []models.PcfAmPolicyContro amPolicyReqTriggers = append(amPolicyReqTriggers, models.PolicyReqTrigger_ACCESS_TYPE_CHANGE) } } - return + return amPolicyReqTriggers } // TS 29.518 5.2.2.6