Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1d289f3
test: HandleCreateUEContext
Apr 18, 2025
bcc313b
fix: change logrus to testing.T.Fatal()
Apr 29, 2025
5c7f97a
feat: N2 Handover between different AMF
Jun 11, 2025
4debe76
feat: N2 handover between different AMFs
Jun 26, 2025
2033aaa
Merge branch 'main' of github.com:InertGas01/amf into main
Jun 26, 2025
142491e
fix: redund code in last merge.
Jun 26, 2025
4c06b72
fix: change context type PendingHandoverResponse variables from priva…
Jul 6, 2025
cc20ecb
fix: error handling and logging in CreateUeContext procedure and rela…
Jul 11, 2025
8b127d5
fix: recover go.mod & linter fix
Jul 21, 2025
9afeac2
fix: Skip 'Target RAN Node ID not found' error. Add error/debug log m…
Sep 10, 2025
e5ebd17
Merge branch 'main' into main
InertGas01 Sep 13, 2025
c66ad32
fix: Add UE context information for UpdateSMContext request and HANDO…
Oct 9, 2025
b180fa2
fix: Complete Create UE Context test
Oct 14, 2025
f1728f7
Merge branch 'free5gc:main' into main
InertGas01 Oct 15, 2025
a3ab9aa
fix: ci-lint for consumer/smf_service.go, createUeContext_test.go and…
Oct 15, 2025
042a19a
Merge branch 'main' of github.com:InertGas01/amf into main
Oct 15, 2025
116dd38
fix: error handling condition for SCTPConn.Close() in createUeContext…
Oct 15, 2025
003b729
fix: gci for consumer/smf_service.go and processor/ue_context.go
Oct 15, 2025
fb23ab1
fix: golangci-lint for ngap/handler.go, processor/ue_context.go
Oct 15, 2025
b3b2037
fix: gci for ue_context.go
Oct 15, 2025
432b090
fix: golangci-lint v2.1.6 for ue_context.go
Oct 16, 2025
44e3d6e
refactor: add comments and remove unused struct type in context.go
Oct 22, 2025
544d77b
fix: fix logger usage in context/context.go and add helper function f…
Oct 31, 2025
b305859
fix: ci-lint
Oct 31, 2025
954cbfb
fix: create channel using make instead of declaration
Oct 31, 2025
166ec15
fix: error handling for pendingHOResponseChan
Nov 12, 2025
9f16b7a
fix: golangci-lint
Nov 12, 2025
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -422,6 +432,9 @@ func (context *AMFContext) AmfRanFindByRanID(ranNodeID models.GlobalRanNodeId) (
}
return true
})
if isEmpty {
logger.CommLog.Warnf("AmfRanPool is empty\n")
Copy link
Contributor

Choose a reason for hiding this comment

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

use Warnln to instead

}
return ran, ok
}

Expand Down
226 changes: 226 additions & 0 deletions internal/ngap/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1490,7 +1490,82 @@ func handleHandoverRequestAcknowledgeMain(ran *context.AmfRan,
if sourceUe == nil {
// TODO: Send Namf_Communication_CreateUEContext Response to S-AMF
Copy link
Member

Choose a reason for hiding this comment

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

After implementation, this TODO line can be removed.

ran.Log.Error("handover between different Ue has not been implement yet")
var ueContextCreatedData models.UeContextCreatedData
Copy link
Contributor

Choose a reason for hiding this comment

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

add helper function to build the models.UeContextCreatedData from provided amfUeContext.


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),
})
}

resp201 := models.CreateUeContextResponse201{
JsonData: &ueContextCreatedData,
BinaryDataN2Information: targetToSourceTransparentContainer.Value,
}

amfSelf := amfUe.ServingAMF()

// Create channel if not exist
pendingHOResponseChan := make(chan context.PendingHandoverResponse)
Copy link
Contributor

@ianchen0119 ianchen0119 Oct 27, 2025

Choose a reason for hiding this comment

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

	var pendingHOResponseChan chan context.PendingHandoverResponse
	value, loaded := amfSelf.PendingHandovers.LoadOrStore(ue.Supi, pendingHOResponseChan)
	if loaded {
		logger.CommLog.Info("PendingHandoverResponse channel created by HandoverRequestAcknowledge handler.")
		pendingHOResponseChan = value.(chan context.PendingHandoverResponse)
	} else {
        // handle the error
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly, the pendingHOResponseChan should be crated already when receiving the HO Req Ack.
Please add error handling for the case of PendingHandovers not loaded.

Copy link
Author

@InertGas01 InertGas01 Oct 31, 2025

Choose a reason for hiding this comment

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

Yes. The pendingHOResponseChan should be created in CreateUEContextProcedure() right after the HO Req is sent. In this case, the loaded is set to true, and the pendingHOResponseChan is replaced by the loaded value. Otherwise, the pendingHOResponseChan created here is stored to the sync.Map and used afterwards. That's why I think the error handling for the if statement is not needed.

value, loaded := amfSelf.PendingHandovers.LoadOrStore(amfUe.Supi, pendingHOResponseChan)
if loaded {
pendingHOResponseChan = value.(chan context.PendingHandoverResponse)
}

// 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 {
Expand Down Expand Up @@ -1633,10 +1708,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
/*
Copy link
Member

Choose a reason for hiding this comment

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

Why did this section remain in comment condition? If it doesn't need now, please remove it or leave the comment with why you make it as comment.

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
Expand Down
62 changes: 62 additions & 0 deletions internal/ngap/message/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading