Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/implicit deregistration #107

Merged
merged 18 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion internal/context/amf_ue.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type AmfUe struct {
UdmGroupId string
SubscribedNssai []models.SubscribedSnssai
AccessAndMobilitySubscriptionData *models.AccessAndMobilitySubscriptionData
BackupAmfInfo []models.BackupAmfInfo
/* contex abut ausf */
AusfGroupId string
AusfId string
Expand Down Expand Up @@ -850,7 +851,6 @@ func (ue *AmfUe) CopyDataFromUeContextModel(ueContext models.UeContext) {
}

// SM Context realted function

func (ue *AmfUe) StoreSmContext(pduSessionID int32, smContext *SmContext) {
ue.SmContextList.Store(pduSessionID, smContext)
}
Expand All @@ -862,6 +862,19 @@ func (ue *AmfUe) SmContextFindByPDUSessionID(pduSessionID int32) (*SmContext, bo
return nil, false
}

func (ue *AmfUe) UpdateBackupAmfInfo(backupAmfInfo models.BackupAmfInfo) {
isExist := false
for _, amfInfo := range ue.BackupAmfInfo {
if amfInfo.BackupAmf == backupAmfInfo.BackupAmf {
isExist = true
break
}
}
if !isExist {
ue.BackupAmfInfo = append(ue.BackupAmfInfo, backupAmfInfo)
}
}

func (ue *AmfUe) StopT3513() {
if ue.T3513 == nil {
return
Expand Down
12 changes: 6 additions & 6 deletions internal/gmm/common/user_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ func RemoveAmfUe(ue *context.AmfUe, notifyNF bool) {

func PurgeAmfUeSubscriberData(ue *context.AmfUe) {
if ue.RanUe[models.AccessType__3_GPP_ACCESS] != nil {
err := purgeSubscriberData(ue, models.AccessType__3_GPP_ACCESS)
err := PurgeSubscriberData(ue, models.AccessType__3_GPP_ACCESS)
if err != nil {
logger.GmmLog.Errorf("Purge subscriber data Error[%v]", err.Error())
}
}
if ue.RanUe[models.AccessType_NON_3_GPP_ACCESS] != nil {
err := purgeSubscriberData(ue, models.AccessType_NON_3_GPP_ACCESS)
err := PurgeSubscriberData(ue, models.AccessType_NON_3_GPP_ACCESS)
if err != nil {
logger.GmmLog.Errorf("Purge subscriber data Error[%v]", err.Error())
}
Expand All @@ -69,8 +69,8 @@ func AttachRanUeToAmfUeAndReleaseOldIfAny(ue *context.AmfUe, ranUe *context.RanU
ue.AttachRanUe(ranUe)
}

func purgeSubscriberData(ue *context.AmfUe, accessType models.AccessType) error {
logger.GmmLog.Debugln("purgeSubscriberData")
func PurgeSubscriberData(ue *context.AmfUe, accessType models.AccessType) error {
logger.GmmLog.Debugln("PurgeSubscriberData")

if !ue.ContextValid {
return nil
Expand All @@ -89,9 +89,9 @@ func purgeSubscriberData(ue *context.AmfUe, accessType models.AccessType) error
if ue.UeCmRegistered[accessType] {
problemDetails, err := consumer.UeCmDeregistration(ue, accessType)
if problemDetails != nil {
logger.GmmLog.Errorf("UECM_Registration Failed Problem[%+v]", problemDetails)
logger.GmmLog.Errorf("UECM Deregistration Failed Problem[%+v]", problemDetails)
} else if err != nil {
logger.GmmLog.Errorf("UECM_Registration Error[%+v]", err)
logger.GmmLog.Errorf("UECM Deregistration Error[%+v]", err)
}
ue.UeCmRegistered[accessType] = false
}
Expand Down
5 changes: 5 additions & 0 deletions internal/gmm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,11 @@ func HandleInitialRegistration(ue *context.AmfUe, anType models.AccessType) erro
param.PreferredLocality = optional.NewString(amfSelf.Locality)
}

// TODO: (step 15) Should use PCF ID to select PCF
// Retrieve PCF ID from old AMF
// if ue.PcfId != "" {

// }
for {
resp, err := consumer.SendSearchNFInstances(amfSelf.NrfUri, models.NfType_PCF, models.NfType_AMF, &param)
if err != nil {
Expand Down
9 changes: 9 additions & 0 deletions internal/sbi/consumer/ue_context_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package consumer

import (
"context"
"fmt"

amf_context "github.com/free5gc/amf/internal/context"
"github.com/free5gc/amf/internal/logger"
"github.com/free5gc/amf/pkg/factory"
"github.com/free5gc/openapi"
"github.com/free5gc/openapi/Nudm_UEContextManagement"
"github.com/free5gc/openapi/models"
Expand All @@ -21,11 +23,18 @@ func UeCmRegistration(ue *amf_context.AmfUe, accessType models.AccessType, initi

switch accessType {
case models.AccessType__3_GPP_ACCESS:
deregCallbackUri := fmt.Sprintf("%s%s/deregistration/%s",
amfSelf.GetIPv4Uri(),
factory.AmfCallbackResUriPrefix,
ue.Supi,
)

registrationData := models.Amf3GppAccessRegistration{
AmfInstanceId: amfSelf.NfId,
InitialRegistrationInd: initialRegistrationInd,
Guami: &amfSelf.ServedGuamiList[0],
RatType: ue.RatType,
DeregCallbackUri: deregCallbackUri,
// TODO: not support Homogenous Support of IMS Voice over PS Sessions this stage
ImsVoPs: models.ImsVoPs_HOMOGENEOUS_NON_SUPPORT,
}
Expand Down
126 changes: 126 additions & 0 deletions internal/sbi/httpcallback/api_handle_dereg_notification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package httpcallback

import (
"net/http"

"github.com/gin-gonic/gin"

amf_context "github.com/free5gc/amf/internal/context"
"github.com/free5gc/amf/internal/logger"
"github.com/free5gc/amf/internal/sbi/consumer"
"github.com/free5gc/openapi"
"github.com/free5gc/openapi/models"
)

func HTTPHandleDeregistrationNotification(c *gin.Context) {
// TS 23.502 - 4.2.2.2.2 - step 14d
logger.CallbackLog.Infoln("Handle Deregistration Notification")

var deregData models.DeregistrationData

requestBody, err := c.GetRawData()
if err != nil {
logger.CallbackLog.Errorf("Get Request Body error: %+v", err)
problemDetails := models.ProblemDetails{
Title: "System failure",
Status: http.StatusInternalServerError,
Detail: err.Error(),
Cause: "SYSTEM_FAILURE",
}
c.JSON(http.StatusInternalServerError, problemDetails)
return
}

err = openapi.Deserialize(&deregData, requestBody, "application/json")
if err != nil {
problemDetails := models.ProblemDetails{
Title: "Malformed request syntax",
Status: http.StatusBadRequest,
Detail: "[Request Body] " + err.Error(),
}
logger.CallbackLog.Errorln(problemDetails.Detail)
c.JSON(http.StatusBadRequest, problemDetails)
return
}

ueid := c.Param("ueid")
ue, ok := amf_context.GetSelf().AmfUeFindByUeContextID(ueid)
if !ok {
logger.CallbackLog.Errorf("AmfUe Context[%s] not found", ueid)
problemDetails := models.ProblemDetails{
Status: http.StatusNotFound,
Cause: "CONTEXT_NOT_FOUND",
}
c.JSON(http.StatusNotFound, problemDetails)
return
}

problemDetails, err := DeregistrationNotificationProcedure(ue, deregData)
if problemDetails != nil {
ue.GmmLog.Errorf("Deregistration Notification Procedure Failed Problem[%+v]", problemDetails)
} else if err != nil {
ue.GmmLog.Errorf("Deregistration Notification Procedure Error[%v]", err.Error())
}
// TS 23.503 - 5.3.2.3.2 UDM initiated NF Deregistration
// The AMF acknowledges the Nudm_UECM_DeRegistrationNotification to the UDM.
c.JSON(http.StatusNoContent, nil)
}

// TS 23.502 - 4.2.2.3.3 Network-initiated Deregistration
// The AMF can initiate this procedure for either explicit (e.g. by O&M intervention) or
// implicit (e.g. expiring of Implicit Deregistration timer)
func DeregistrationNotificationProcedure(ue *amf_context.AmfUe, deregData models.DeregistrationData) (
problemDetails *models.ProblemDetails, err error,
) {
// The AMF does not send the Deregistration Request message to the UE for Implicit Deregistration.
switch deregData.DeregReason {
case models.DeregistrationReason_UE_INITIAL_REGISTRATION:
// TS 23.502 - 4.2.2.2.2 General Registration
// Invokes the Nsmf_PDUSession_ReleaseSMContext for the corresponding access type
ue.SmContextList.Range(func(key, value interface{}) bool {
smContext := value.(*amf_context.SmContext)
if smContext.AccessType() == deregData.AccessType {
problemDetails, err = consumer.SendReleaseSmContextRequest(ue, smContext, nil, "", nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SendReleaseSmContextRequest() needs to read the attribute of the ue, it will occur the concurrent read/write if another ngap/nas message for the same ue is sent to AMF.
Please help to check do we need a mutex lock here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreover, it seems like the behavior of AMF in this case is the same with gmm_common.RemoveAmfUe(amfUe, false).
@iamelisahi @YouShengLiu

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SendReleaseSmContextRequest() needs to read the attribute of the ue, it will occur the concurrent read/write if another ngap/nas message for the same ue is sent to AMF.
Please help to check do we need a mutex lock here.

SendReleaseSmContextRequest() will only use the ue.TimeZone and smContext.
Since the smContext is stored in ue.SmContextList and protected under the sync.Map, we can leave it alone.
However, the ue.TimeZone is not protected, it may need mutex lock to protect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreover, it seems like the behavior of AMF in this case is the same with gmm_common.RemoveAmfUe(amfUe, false).

Yes, it is very similar but there are still some differences.

According to the TS 29.503 - Table 6.2.6.2.7-1: Definition of type Amf3GppAccessRegistrationModification - backupAmfInfo:

This IE shall be included if the NF service consumer is an AMF and the AMF supports the AMF management without UDSF for the Modification of the BackupAmfInfo.
The UDM uses this attribute to do an NRF query in order to invoke later services in a backup AMF, e.g. Namf_EventExposure

We think we should provide the BackupAmfInfo but the UeCmDeregistration() didn't.
Should we need to modify UeCmDeregistration(), e.g. add one more parameter to decide if it needs to put the BackupAmfInfo into Amf3GppAccessRegistrationModification?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YouShengLiu

Yes, it is very similar but there are still some differences.

Is it possible to reuse the duplicated part?

We think we should provide the BackupAmfInfo but the UeCmDeregistration() didn't.
Should we need to modify UeCmDeregistration(), e.g. add one more parameter to decide if it needs to put the BackupAmfInfo into Amf3GppAccessRegistrationModification?

I think you are right, please help to fill the BackupAmfInfo.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does spec mention to use old amf as the backup amf? I think it's strange to notify UDM that the backup amf of e.g. amf1 is amf1 itself😂 Backup AMF may be a pre-config field to this AMF's config in my opnion.

Copy link
Contributor Author

@YouShengLiu YouShengLiu Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec didn't mention using old AMF as the backup AMF directly. I was thinking that the old AMF could be one of the backup AMFs when the new AMF is serving the UE.
I checked the data stored in the database, the serving AMF was the new AMF, and the old AMF became the backup AMF:

{
  "_id": {
    "$oid": "650059c26b8b38f7f8ab93dc"
  },
  "ueId": "imsi-2089300007487",
  "amfInstanceId": "faf609ad-2cbd-4082-bf41-7a554e4aa626",
  "imsVoPs": "HOMOGENEOUS_NON_SUPPORT",
  "deregCallbackUri": "http://127.0.0.19:8000/namf-callback/v1/amf-implicit-deregistration/imsi-2089300007487",
  "initialRegistrationInd": true,
  "guami": {
    "amfId": "cafe01",
    "plmnId": {
      "mcc": "208",
      "mnc": "93"
    }
  },
  "ratType": "",
  "backupAmfInfo": [
    {
      "backupAmf": "AMF",
      "guamiList": [
        {
          "amfId": "cafe00",
          "plmnId": {
            "mcc": "208",
            "mnc": "93"
          }
        }
      ]
    }
  ],
  "purgeFlag": true
}

I think it's strange to notify UDM that the backup amf of e.g. amf1 is amf1 itself😂 Backup AMF may be a pre-config field to this AMF's config in my opinion.

I agree that using pre-config is a better solution.

if problemDetails != nil {
ue.GmmLog.Errorf("Release SmContext Failed Problem[%+v]", problemDetails)
} else if err != nil {
ue.GmmLog.Errorf("Release SmContext Error[%v]", err.Error())
}
}
return true
})
}
// TS 23.502 - 4.2.2.2.2 General Registration - 14e
// TODO: (R16) If old AMF does not have UE context for another access type (i.e. non-3GPP access),
// the Old AMF unsubscribes with the UDM for subscription data using Nudm_SDM_unsubscribe
if ue.SdmSubscriptionId != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ianchen0119 check udm implementation to make sure old amf will not unsubscribe new amf's subscription

problemDetails, err = consumer.SDMUnsubscribe(ue)
if problemDetails != nil {
logger.GmmLog.Errorf("SDM Unubscribe Failed Problem[%+v]", problemDetails)
} else if err != nil {
logger.GmmLog.Errorf("SDM Unubscribe Error[%+v]", err)
}
ue.SdmSubscriptionId = ""
}

// TS 23.502 - 4.2.2.2.2 General Registration - 20 AMF-Initiated Policy Association Termination
// For UE_INITIAL_REGISTRATION and SUBSCRIPTION_WITHDRAW, do AMF-Initiated Policy Association Termination directly.
if ue.PolicyAssociationId != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ianchen0119 check pcf implementation to make sure old amf will not unsubscribe new amf's ampolicy association

// TODO: For REGISTRATION_AREA_CHANGE, old AMF performs an AMF-initiated Policy Association Termination
// procedure if the old AMF has established an AM Policy Association and a UE Policy Association with the PCF(s)
// and the old AMF did not transfer the PCF ID(s) to the new AMF. (Ref: TS 23.502 - 4.2.2.2.2)
// Currently, old AMF will transfer the PCF ID but new AMF will not utilize the PCF ID
problemDetails, err := consumer.AMPolicyControlDelete(ue)
if problemDetails != nil {
logger.GmmLog.Errorf("Delete AM policy Failed Problem[%+v]", problemDetails)
} else if err != nil {
logger.GmmLog.Errorf("Delete AM policy Error[%+v]", err)
}
}
Copy link

@ss920386 ss920386 Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original RemoveAmfUe at dereg when notifyNf is true also considers AMPolicyControlDelete. Should AMPolicyControlDelete be included here according to TS 23.502 4.2.2.2.2 14d?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't find the description in R15 - TS 23.502 - 4.2.2.2.2 - 14d and I am not sure if we need to delete AmPolicy here.
Based on my understanding of implicitly deregistering AMF, the goal of it is only to remove the UE context from old AMF rather than deregister the UE from the network.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I think that's step 20. in R15.

  1. [Conditional] old AMF to (V-)PCF: AMF-Initiated Policy Association Termination.
    If the old AMF previously initiated a Policy Association to the PCF, and the old AMF did not transfer the PCF
    ID(s) to the new AMF (e.g. new AMF is in different PLMN), the old AMF performs an AMF-initiated Policy
    Association Termination procedure, as defined in clause 4.16.3.2, to delete the association with the PCF.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For UE_INITIAL_REGISTRATION and SUBSCRIPTION_WITHDRAW, it is fine to do AMF-Initiated Policy Association Termination directly. However, if it is REGISTRATION_AREA_CHANGE, it is possible that the new AMF reuses the associated PCF ID, which hence should not be terminated.
R15 spec mentioned the condition:

If the old AMF previously initiated a Policy Association to the PCF, and the old AMF did not transfer the PCF
ID(s) to the new AMF

R16 has clearer instructions:

If the old AMF has established an AM Policy Association and a UE Policy Association with the PCF(s), and the
old AMF did not transfer the PCF ID(s) to the new AMF (e.g. new AMF is in different PLMN), the old AMF
performs an AMF-initiated Policy Association Termination procedure, as defined in clause 4.16.3.2, and
performs an AMF-initiated UE Policy Association Termination procedure, as defined in clause 4.16.13.1. In
addition, if the old AMF transferred the PCF ID(s) in the UE context but the new AMF informed in step 10 that
the AM Policy Association information and UE Policy Association information in the UE context will not be
used then the old AMF performs an AMF-initiated Policy Association Termination procedure, as defined in
clause 4.16.3.2, and performs an AMF-initiated UE Policy Association Termination procedure, as defined in
clause 4.16.13.1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For UE_INITIAL_REGISTRATION and SUBSCRIPTION_WITHDRAW, it is fine to do AMF-Initiated Policy Association Termination directly. However, if it is REGISTRATION_AREA_CHANGE, it is possible that the new AMF reuses the associated PCF ID, which hence should not be terminated.

I agree with that, however, free5GC currently doesn't utilize the PCF ID to select the PCF, therefore, I leave the TODO comments for it.
Should we complete the feature (searching PCF by using PCF ID) in this PR or open another PR for this feature?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is fine to leave for a new PR. Lastly, please help to change the TODO comments (// TODO: It also needs to check if the PCF ID is tranfered to new AMF...) to the followings:

// For UE_INITIAL_REGISTRATION and SUBSCRIPTION_WITHDRAW, do AMF-Initiated Policy Association Termination directly.
// TODO: For REGISTRATION_AREA_CHANGE, old AMF performs an AMF-initiated Policy Association Termination 
// procedure if the old AMF has established an AM Policy Association and a UE Policy Association with the PCF(s), 
// and the old AMF did not transfer the PCF ID(s) to the new AMF. (Ref: TS 23.502 - 4.2.2.2.2) 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK


// The old AMF should clean the UE context
// TODO: (R16) Only remove the target access UE context
ue.Remove()

return nil, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,11 @@ var routes = Routes{
"/n1-message-notify",
HTTPN1MessageNotify,
},

{
"HandleDeregistrationNotification",
strings.ToUpper("Post"),
"/deregistration/:ueid",
HTTPHandleDeregistrationNotification,
},
}
11 changes: 7 additions & 4 deletions internal/sbi/producer/ue_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,9 @@ func UEContextTransferProcedure(ueContextID string, ueContextTransferRequest mod
ue.Lock.Lock()
defer ue.Lock.Unlock()

ueContextTransferResponse := new(models.UeContextTransferResponse)
ueContextTransferResponse.JsonData = new(models.UeContextTransferRspData)
ueContextTransferResponse := &models.UeContextTransferResponse{
JsonData: new(models.UeContextTransferRspData),
}
ueContextTransferRspData := ueContextTransferResponse.JsonData

//if ue.GetAnType() != UeContextTransferReqData.AccessType {
Expand Down Expand Up @@ -603,8 +604,10 @@ func RegistrationStatusUpdateProcedure(ueContextID string, ueRegStatusUpdateReqD
logger.GmmLog.Errorf("AM Policy Control Delete Error[%v]", err.Error())
}
}

gmm_common.RemoveAmfUe(ue, false)
// TODO: Currently only consider the 3GPP access type
if !ue.UeCmRegistered[models.AccessType__3_GPP_ACCESS] {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will 3gpp and non-3gpp have different behavior here? I think a better implementation here is to remove the context according to different access type (which is specified during context transfer procedure), but I understand it seems that currently we can only remove the complete AmfUe context (3gpp+non-3gpp). In this case, I don't quite understand why non-3gpp is excluded here😂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we are not sure how to deal with the non-3GPP case, we temporarily considered the 3GPP case.

gmm_common.RemoveAmfUe(ue, false)
}
} else {
// NOT_TRANSFERRED
logger.CommLog.Debug("[AMF] RegistrationStatusUpdate: NOT_TRANSFERRED")
Expand Down