Free5GC源码研究(13) - AMF研究(二)

前文再续,本文研究AMF与用户设备通信的的nas协议,和与接入网基站通信的ngap协议

AM的实现

考察free5gc/amf/internal/的文件目录,会发现与之前研究的NF相比多了三个陌生的包

amf/internal/
├─context
├─gmm
├─logger
├─nas
├─ngap
├─sbi
└─util

这里面,gmm (5G Mobility Management),是5G移动性管理的核心模块,负责管理设备的注册、连接、和移动。nas (Non-Access Stratum),是UE和AMF之间的通信协议,而ngap (Next Generation Application Protocol)则是RAN和AMF之间的通信协议。free5gc/nasfree5gc/ngap分别是对这两个协议的具体实现,而amf/internal/中的两个包则是AMF对这两个协议的调用。gmm、nas、ngap三者的关系可以用下面这张图来概括:

img

简单来说,nas包括gmm(以及SMF中的gsm),是ngap的上一层协议。nas的关注点是怎样提供一套包含所有控制面消息类型的用户设备与核心网之间的信令传输框架,并提供安全性功能;ngap的关注点是怎样把消息从接入网传输到核心网;而gmm是nas中关于移动性管理的部分。这体现在代码中,就是internal/nas/目录并没有多少代码。这个包里只保留了dispatcher,而最重要最常被调用的功能是nas_security,而更多的功能被实现在internal/gmm/中。gmm会调用free5gc/nas协议构建nas消息,然后调用ngap协议发送构建好的nas消息。

// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/message/send.go#L434
func SendRegistrationAccept(...) {
    // GMM模块:处理移动性业务逻辑
    amfUe.GmmLog.Info("Send Registration Accept")

    // NAS 层:构建 NAS 消息
    nasMsg, err := BuildRegistrationAccept(amfUe, anType, ...)
    if err != nil {
        return
    }

    // NGAP 层:传输消息
    ngap_message.SendN2Message(amfUe, anType, nasMsg, ...)
}

由此可见,虽然在5G核心网架构图中,N1接口显示的是AMF与UE直接相连,但它们仅仅是一个逻辑链接,强调它们之间的通信是使用nas协议的端到端的通信过程,无需考虑底层实际传输过程细节,但实际上UE与AMF的消息通信还是要经过接入网,也就是UE -> RAN -> AMF这条路径。

nas-mm

nas-mm的核心功能,注册、连接、和移动性管理,free5gc/amf中使用了一个状态机来实现。下面的有向图是这个状态机的状态转移图,其中节点代表状态,而边则代表状态间的转移和触发状态转移的事件

img

状态图mermaid代码
stateDiagram-v2
    %% 定义状态样式
    classDef stateStyle fill:#e1f5fe,stroke:#01579b
    
    %% 定义事件样式(通过note实现)
    classDef eventStyle fill:#ffebee,stroke:#c62828,color:#c62828
    
    state "Deregistered" as Deregistered
    state "Authentication" as Authentication
    state "SecurityMode" as SecurityMode
    state "ContextSetup" as ContextSetup
    state "Registered" as Registered
    state "DeregistrationInitiated" as DeregistrationInitiated
    
    Deregistered --> Deregistered: <font color="red">GmmMessageEvent</font>
    Deregistered --> Authentication: <font color="red">StartAuthEvent</font>
    
    Authentication --> Authentication: <font color="red">GmmMessageEvent</font>
    Authentication --> Authentication: <font color="red">AuthRestartEvent</font>
    Authentication --> SecurityMode: <font color="red">AuthSuccessEvent</font>
    Authentication --> Deregistered: <font color="red">AuthFailEvent</font>
    Authentication --> Deregistered: <font color="red">AuthErrorEvent</font>
    
    SecurityMode --> SecurityMode: <font color="red">GmmMessageEvent</font>
    SecurityMode --> ContextSetup: <font color="red">SecurityModeSuccessEvent</font>
    SecurityMode --> Deregistered: <font color="red">SecurityModeFailEvent</font>
    
    ContextSetup --> ContextSetup: <font color="red">GmmMessageEvent</font>
    ContextSetup --> Registered: <font color="red">ContextSetupSuccessEvent</font>
    ContextSetup --> Deregistered: <font color="red">ContextSetupFailEvent</font>
    
    Registered --> Registered: <font color="red">GmmMessageEvent</font>
    Registered --> Authentication: <font color="red">StartAuthEvent</font>
    Registered --> DeregistrationInitiated: <font color="red">InitDeregistrationEvent</font>
    
    DeregistrationInitiated --> Deregistered: <font color="red">DeregistrationAcceptEvent</font>

    %% 应用样式
    class Deregistered,Authentication,SecurityMode,ContextSetup,Registered,DeregistrationInitiated stateStyle

这个状态机乍看起来有点复杂,但其实可以看成一条从DeregisteredRegistered的路径,其他所有路径都是出现错误而回退到路径前面的某个状态,以及从Registered经过注销DeregistrationInitiated回到Deregistered的初始状态。

在前文SMF的研究HandlePDUSessionSMContextUpdate函数里也用到了状态机的编程范式,还一用就是3个。不过它们相对简单,所以在代码里可以使用“switch-case”来实现。但是gmm的状态机有6个状态16条转移方式,“switch-case”的方式就有点不够用了。free5gc里专门编写了一个FSM类型(Finite State Machine)来实现gmm的有限状态机。

// https://github.com/free5gc/util/blob/v1.0.6/fsm/state.go
type StateType string
type State struct {
	current StateType
	stateMutex sync.RWMutex
}

// https://github.com/free5gc/util/blob/v1.0.6/fsm/fsm.go
type (
	EventType string
	ArgsType  map[string]interface{}
    Callback  func(*State, EventType, ArgsType)
	Callbacks map[StateType]Callback
)
type eventKey struct {
	Event EventType
	From  StateType
}
type Transition struct {
	Event EventType
	From  StateType
	To    StateType
}
type Transitions []Transition
type FSM struct {
	transitions map[eventKey]Transition
	callbacks map[StateType]Callback
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/init.go
var transitions = fsm.Transitions{
	{Event: GmmMessageEvent, From: context.Deregistered, To: context.Deregistered},
	{Event: GmmMessageEvent, From: context.Authentication, To: context.Authentication},
	{Event: GmmMessageEvent, From: context.SecurityMode, To: context.SecurityMode},
	{Event: GmmMessageEvent, From: context.ContextSetup, To: context.ContextSetup},
	{Event: GmmMessageEvent, From: context.Registered, To: context.Registered},
	{Event: StartAuthEvent, From: context.Deregistered, To: context.Authentication},
	{Event: StartAuthEvent, From: context.Registered, To: context.Authentication},
	{Event: AuthRestartEvent, From: context.Authentication, To: context.Authentication},
	{Event: AuthSuccessEvent, From: context.Authentication, To: context.SecurityMode},
	{Event: AuthFailEvent, From: context.Authentication, To: context.Deregistered},
	{Event: AuthErrorEvent, From: context.Authentication, To: context.Deregistered},
	{Event: SecurityModeSuccessEvent, From: context.SecurityMode, To: context.ContextSetup},
	{Event: SecurityModeFailEvent, From: context.SecurityMode, To: context.Deregistered},
	{Event: ContextSetupSuccessEvent, From: context.ContextSetup, To: context.Registered},
	{Event: ContextSetupFailEvent, From: context.ContextSetup, To: context.Deregistered},
	{Event: InitDeregistrationEvent, From: context.Registered, To: context.DeregistrationInitiated},
	{Event: DeregistrationAcceptEvent, From: context.DeregistrationInitiated, To: context.Deregistered},
}
var callbacks = fsm.Callbacks{
	context.Deregistered:            DeRegistered,
	context.Authentication:          Authentication,
	context.SecurityMode:            SecurityMode,
	context.ContextSetup:            ContextSetup,
	context.Registered:              Registered,
	context.DeregistrationInitiated: DeregisteredInitiated,
}
var GmmFSM *fsm.FSM
func init() {
	if f, err := fsm.NewFSM(transitions, callbacks); err != nil {
		logger.GmmLog.Errorf("Initialize Gmm FSM Error: %+v", err)
	} else {
		GmmFSM = f
	}
}

状态机的状态转移由事件Event来触发,到达某个状态后执行该状态的Callback函数。事件不过是一个字符串,重要的是怎么触发这些事件,以及每个状态的回调函数在做什么事情。触发事件很简单,只需要调用GmmmFSM.SendEvent函数即可,比如要触发一个AuthSuccessEvent,只需调用GmmFSM.SendEvent(state, AuthSuccessEvent, fsm.ArgsType{...},就可触发事件,进行状态转移(Authentication--[AuthSuccessEvent]-->SecurityMode,并执行回调函数。

/* https://github.com/free5gc/util/blob/v1.0.6/fsm/fsm.go */
// Entry&Exit event are defined by fsm package
const (
    EntryEvent EventType = "Entry event"
    ExitEvent  EventType = "Exit event"
)
// SendEvent triggers a callback with an event, and do transition after callback if need
// There are 3 types of callback: on exit callback, event callback, on entry callback
func (fsm *FSM) SendEvent(state *State, event EventType, args ArgsType, log *logrus.Entry) error {
    key := eventKey{
        From:  state.Current(),
        Event: event,
    }
	trans, ok := fsm.transitions[key]
	fsm.callbacks[trans.From](state, event, args)  // event callback
    if trans.From != trans.To {                    // exit callback
        fsm.callbacks[trans.From](state, ExitEvent, args)
    }
    if trans.From != trans.To {                    // entry callback
        state.Set(trans.To)
        fsm.callbacks[trans.To](state, EntryEvent, args)
    }
}

而gmm状态机的所有6个回调函数都写在internal/gmm/sm.go。每个回调函数内部也是“switch-case”的模式,甚至case里面还有“switch-case”,先判断当前发生了什么事件,然后执行相应的逻辑,比如触发一个新的事件。这些回调函数的执行和状态机的状态转移过程,后文会以初始注册流程为例详细研究。

GmmFSM回调函数
// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/sm.go#L17C1-L61C1
func DeRegistered(state *fsm.State, event fsm.EventType, args fsm.ArgsType) {
	switch event {
	case fsm.EntryEvent:
		amfUe.ClearRegistrationRequestData(accessType)
	case GmmMessageEvent:
		gmmMessage := args[ArgNASMessage].(*nas.GmmMessage)
		switch gmmMessage.GetMessageType() {
		case nas.MsgTypeRegistrationRequest:
			if err := HandleRegistrationRequest(amfUe, accessType, procedureCode, gmmMessage.RegistrationRequest); err != nil {
				logger.GmmLog.Errorln(err)
			} else {
				GmmFSM.SendEvent(state, StartAuthEvent, fsm.ArgsType{...})
			}
		// If UE that considers itself Registared and CM-IDLE throws a ServiceRequest
		case nas.MsgTypeServiceRequest:
			HandleServiceRequest(amfUe, accessType, gmmMessage.ServiceRequest)
		// default: logging
		}
	// case StartAuthEvent; case ExitEvent; default: logger.GmmLog.Debugln(event)
	}
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/sm.go#L133C1-L239C1
func Authentication(state *fsm.State, event fsm.EventType, args fsm.ArgsType) {
	switch event {
	case AuthRestartEvent:
		amfUe = args[ArgAmfUe].(*context.AmfUe)
		accessType := args[ArgAccessType].(models.AccessType)
		pass, err := AuthenticationProcedure(amfUe, accessType)
		if err != nil {
			GmmFSM.SendEvent(state, AuthErrorEvent, fsm.ArgsType{...}, logger.GmmLog)
		}
		if pass {
			GmmFSM.SendEvent(state, AuthSuccessEvent, fsm.ArgsType{...}, logger.GmmLog)
		}
	case GmmMessageEvent:
		switch gmmMessage.GetMessageType() {
		case nas.MsgTypeIdentityResponse:
			HandleIdentityResponse(amfUe, gmmMessage.IdentityResponse)
 			// update identity type used for reauthentication
	    	mobileIdentityContents := gmmMessage.IdentityResponse.MobileIdentity.GetMobileIdentityContents()
			amfUe.IdentityTypeUsedForRegistration = nasConvert.GetTypeOfIdentity(mobileIdentityContents[0])
			GmmFSM.SendEvent(state,AuthRestartEvent,fsm.ArgsType{...}, logger.GmmLog,)
		case nas.MsgTypeAuthenticationResponse:
			HandleAuthenticationResponse(amfUe, accessType, gmmMessage.AuthenticationResponse)
		case nas.MsgTypeAuthenticationFailure:
			HandleAuthenticationFailure(amfUe, accessType, gmmMessage.AuthenticationFailure)
		case nas.MsgTypeStatus5GMM:
			HandleStatus5GMM(amfUe, accessType, gmmMessage.Status5GMM)
		}
	case AuthSuccessEvent:
		logger.GmmLog.Debugln(event)
	case AuthErrorEvent:
		HandleAuthenticationError(amfUe, accessType)
	case AuthFailEvent:
        ngap_message.SendUEContextReleaseCommand(amfUe.RanUe[accessType], context.UeContextN2NormalRelease,
            ngapType.CausePresentNas, ngapType.CauseNasPresentAuthenticationFailure)
        amfUe.RanUe[accessType].Remove()
		gmm_common.RemoveAmfUe(amfUe, true)
	case fsm.ExitEvent:
		// clear authentication related data at exit
		amfUe := args[ArgAmfUe].(*context.AmfUe)
		amfUe.GmmLog.Debugln(event)
		amfUe.AuthenticationCtx = nil
		amfUe.AuthFailureCauseSynchFailureTimes = 0
		amfUe.IdentityRequestSendTimes = 0
	default:
		logger.GmmLog.Errorf("Unknown event [%+v]", event)
	}
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/sm.go#L240C1-L321C1
func SecurityMode(state *fsm.State, event fsm.EventType, args fsm.ArgsType) {
	switch event {
	case fsm.EntryEvent:
		if amfUe.SecurityContextIsValid() {
			GmmFSM.SendEvent(state, SecurityModeSuccessEvent, fsm.ArgsType{...}, logger.GmmLog)
		} else {
			if err := amfUe.SelectSecurityAlg(amfSelf.SecurityAlgorithm.IntegrityOrder,
				amfSelf.SecurityAlgorithm.CipheringOrder); err != nil {
				gmm_message.SendRegistrationReject(amfUe.RanUe[accessType], nasMessage.Cause5GMMUESecurityCapabilitiesMismatch, "")
				err = GmmFSM.SendEvent(state, SecurityModeFailEvent, fsm.ArgsType{...}, logger.GmmLog)
				return
			}
			// Generate KnasEnc, KnasInt
			amfUe.DerivateAlgKey()
			gmm_message.SendSecurityModeCommand(amfUe.RanUe[accessType], accessType, eapSuccess, eapMessage)
		}
	case GmmMessageEvent:
		switch gmmMessage.GetMessageType() {
		case nas.MsgTypeSecurityModeComplete:
			HandleSecurityModeComplete(amfUe, accessType, procedureCode, gmmMessage.SecurityModeComplete)
		case nas.MsgTypeSecurityModeReject:
			HandleSecurityModeReject(amfUe, accessType, gmmMessage.SecurityModeReject)
			GmmFSM.SendEvent(state, SecurityModeFailEvent, fsm.ArgsType{...}, logger.GmmLog)
		case nas.MsgTypeStatus5GMM:
			HandleStatus5GMM(amfUe, accessType, gmmMessage.Status5GMM)
		}
        // more cases: logging
	}
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/sm.go#L322C1-L437C1
func ContextSetup(state *fsm.State, event fsm.EventType, args fsm.ArgsType) {
	switch event {
	case fsm.EntryEvent:
		switch message := gmmMessage.(type) {
		case *nasMessage.RegistrationRequest:
			amfUe.RegistrationRequest = message
			switch amfUe.RegistrationType5GS {
			case nasMessage.RegistrationType5GSInitialRegistration:
				if err := HandleInitialRegistration(amfUe, accessType); err != nil {
					GmmFSM.SendEvent(state, ContextSetupFailEvent, fsm.ArgsType{...}, logger.GmmLog)
				}
			case nasMessage.RegistrationType5GSMobilityRegistrationUpdating:
				fallthrough
			case nasMessage.RegistrationType5GSPeriodicRegistrationUpdating:
				if err := HandleMobilityAndPeriodicRegistrationUpdating(amfUe, accessType); err != nil {
					GmmFSM.SendEvent(state, ContextSetupFailEvent, fsm.ArgsType{...}, logger.GmmLog)
				}
			}
		case *nasMessage.ServiceRequest:
			HandleServiceRequest(amfUe, accessType, message)
		}
	case GmmMessageEvent:
		switch gmmMessage.GetMessageType() {
		case nas.MsgTypeIdentityResponse:
			HandleIdentityResponse(amfUe, gmmMessage.IdentityResponse)
				switch amfUe.RegistrationType5GS {
				case nasMessage.RegistrationType5GSInitialRegistration:
					if err2 := HandleInitialRegistration(amfUe, accessType); err2 != nil {
						GmmFSM.SendEvent(state, ContextSetupFailEvent, fsm.ArgsType{...}, logger.GmmLog)
                    }
				case nasMessage.RegistrationType5GSMobilityRegistrationUpdating:
					fallthrough
				case nasMessage.RegistrationType5GSPeriodicRegistrationUpdating:
					if err2 := HandleMobilityAndPeriodicRegistrationUpdating(amfUe, accessType); err2 != nil {
						GmmFSM.SendEvent(state, ContextSetupFailEvent, fsm.ArgsType{...}, logger.GmmLog)
					}
				}
			}
		case nas.MsgTypeRegistrationComplete:
			HandleRegistrationComplete(amfUe, accessType, gmmMessage.RegistrationComplete)
		case nas.MsgTypeStatus5GMM:
			HandleStatus5GMM(amfUe, accessType, gmmMessage.Status5GMM)
		}
	case ContextSetupFailEvent:
		consumer.GetConsumer().UeCmDeregistration(amfUe, accessType)
    // case ContextSetupSuccessEvent, case fsm.ExitEvent, default: logging ...
	}
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/sm.go#L62C1-L131C2
func Registered(state *fsm.State, event fsm.EventType, args fsm.ArgsType) {
	switch event {
	case fsm.EntryEvent:
		amfUe.ClearRegistrationRequestData(accessType)
	case GmmMessageEvent:
		switch gmmMessage.GetMessageType() {
		// Mobility Registration update / Periodic Registration update
		case nas.MsgTypeRegistrationRequest:
			if err := HandleRegistrationRequest(amfUe, accessType, procedureCode, gmmMessage.RegistrationRequest); err != nil {
				logger.GmmLog.Errorln(err)
			} else {
				GmmFSM.SendEvent(state, StartAuthEvent, fsm.ArgsType{...}, logger.GmmLog)
			}
		case nas.MsgTypeULNASTransport:
			HandleULNASTransport(amfUe, accessType, gmmMessage.ULNASTransport)
		case nas.MsgTypeConfigurationUpdateComplete:
			HandleConfigurationUpdateComplete(amfUe, gmmMessage.ConfigurationUpdateComplete)
		case nas.MsgTypeServiceRequest:
			HandleServiceRequest(amfUe, accessType, gmmMessage.ServiceRequest)
		case nas.MsgTypeNotificationResponse:
			HandleNotificationResponse(amfUe, gmmMessage.NotificationResponse)
		case nas.MsgTypeDeregistrationRequestUEOriginatingDeregistration:
			GmmFSM.SendEvent(state, InitDeregistrationEvent, fsm.ArgsType{...}, logger.GmmLog)
		case nas.MsgTypeStatus5GMM:
			HandleStatus5GMM(amfUe, accessType, gmmMessage.Status5GMM)
		}
	// case StartAuthEvent, InitDeregistrationEvent, fsm.ExitEvent, default: logging ...
	}
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/sm.go#L438
func DeregisteredInitiated(state *fsm.State, event fsm.EventType, args fsm.ArgsType) {
	switch event {
	case fsm.EntryEvent:
		HandleDeregistrationRequest(amfUe, accessType, gmmMessage.DeregistrationRequestUEOriginatingDeregistration)
	case GmmMessageEvent:
		switch gmmMessage.GetMessageType() {
		case nas.MsgTypeDeregistrationAcceptUETerminatedDeregistration:
			HandleDeregistrationAccept(amfUe, accessType,
				gmmMessage.DeregistrationAcceptUETerminatedDeregistration)
		}
	// case DeregistrationAcceptEvent, case fsm.ExitEvent default: logging
	}
}

简单来说,gmm状态机的回调函数里主要做的事情就四件

  • 调用GmmFSM.SendEvent进行状态转移

  • 调用amfUe的方法对其修改

  • 调用nas协议gmm_message.Send**或ngap协议ngap_message.Send**对UE和RAN发消息

    internal/gmm/message/

    nas和ngap协议各自都定义了多种消息类型,internal/gmm/message/包里封装了构建nas-mm消息并发送的过程。以AMF向UE请求ID的消息为例
    img

    构建消息的函数遵循类似的模板:先new一个消息,然后new一个特定的消息类型,把具体数据填入这个消息里,最后调用nas_security编码消息

    // https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/message/build.go#L90
    func BuildIdentityRequest(ue *context.AmfUe, accessType models.AccessType, typeOfIdentity uint8) ([]byte, error) {
    	m := nas.NewMessage()
    	m.GmmMessage = nas.NewGmmMessage()
    	m.GmmHeader.SetMessageType(nas.MsgTypeIdentityRequest)
    	if ue.SecurityContextAvailable {
    		m.SecurityHeader = nas.SecurityHeader{
    			ProtocolDiscriminator: nasMessage.Epd5GSMobilityManagementMessage,
    			SecurityHeaderType:    nas.SecurityHeaderTypeIntegrityProtectedAndCiphered,
    		}
    	}
    	identityRequest := nasMessage.NewIdentityRequest(0)
    	identityRequest.SetExtendedProtocolDiscriminator(nasMessage.Epd5GSMobilityManagementMessage)
    	identityRequest.SpareHalfOctetAndSecurityHeaderType.SetSecurityHeaderType(nas.SecurityHeaderTypePlainNas)
    	identityRequest.SpareHalfOctetAndSecurityHeaderType.SetSpareHalfOctet(0)
    	identityRequest.IdentityRequestMessageIdentity.SetMessageType(nas.MsgTypeIdentityRequest)
    	identityRequest.SpareHalfOctetAndIdentityType.SetTypeOfIdentity(typeOfIdentity)
    
    	m.GmmMessage.IdentityRequest = identityRequest
    	return nas_security.Encode(ue, m, accessType)
    }
    

    而发送消息的函数模板也差不多,先调用构建消息的函数构建好一个消息,然后调用ngap协议发送消息

    // https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/message/send.go#L72
    func SendIdentityRequest(ue *context.RanUe, accessType models.AccessType, typeOfIdentity uint8) {
    	amfUe := ue.AmfUe
    	nasMsg, err := BuildIdentityRequest(amfUe, accessType, typeOfIdentity)
    
    	ngap_message.SendDownlinkNasTransport(ue, nasMsg, nil)
    
    	amfUe.RequestIdentityType = typeOfIdentity
    	if context.GetSelf().T3570Cfg.Enable {
    		cfg := context.GetSelf().T3570Cfg
    		amfUe.T3570 = context.NewTimer(cfg.ExpireTime, cfg.MaxRetryTimes, func(expireTimes int32) {
    			amfUe.GmmLog.Warnf("T3570 expires, retransmit Identity Request (retry: %d)", expireTimes)
    			ngap_message.SendDownlinkNasTransport(ue, nasMsg, nil)
    		}, func() {
    			amfUe.GmmLog.Warnf("T3570 Expires %d times, abort identification procedure & ongoing 5GMM procedure",
    				cfg.MaxRetryTimes)
    			gmm_common.RemoveAmfUe(amfUe, false)
    		})
    	}
    }
    
  • 调用internal/gmm/handler.go里面的Handle***进行后续处理。这些Handle***函数可以很简单,仅仅是打印一些日志;也可以很复杂,调用AMFContextamfUe、各种内部函数,发送gmm_message给UE等

    HandleSecurityModeReject,HandleInitialRegistration
    // https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/handler.go#L2300
    func HandleSecurityModeReject(ue *context.AmfUe, anType models.AccessType,
        securityModeReject *nasMessage.SecurityModeReject,
    ) error {
        ue.StopT3560()
        cause := securityModeReject.Cause5GMM.GetCauseValue()
        ue.GmmLog.Warnf("Reject Cause: %s", nasMessage.Cause5GMMToString(cause))
        ue.GmmLog.Error("UE reject the security mode command, abort the ongoing procedure")
    }
    
    
    // https://github.com/free5gc/amf/blob/v1.2.5/internal/gmm/handler.go#L620
    func HandleInitialRegistration(ue *context.AmfUe, anType models.AccessType) error {
        amfSelf := context.GetSelf()
        // update Kgnb/Kn3iwf
        ue.UpdateSecurityContext(anType)
        // Registration with AMF re-allocation (TS 23.502 4.2.2.2.3)
        if len(ue.SubscribedNssai) == 0 {
            getSubscribedNssai(ue)
        }
        handleRequestedNssai(ue, anType)
        storeLastVisitedRegisteredTAI(ue, ue.RegistrationRequest.LastVisitedRegisteredTAI)
        // Negotiate DRX value if need (TS 23.501 5.4.5)
        negotiateDRXParameters(ue, ue.RegistrationRequest.RequestedDRXParameters)
    
        // TODO (step 10 optional): send Namf_Communication_RegistrationCompleteNotify to old AMF if need
        if ue.ServingAmfChanged {
            // If the AMF has changed the new AMF notifies the old AMF that the registration of the UE in the new AMF is completed
            req := models.UeRegStatusUpdateReqData{
                TransferStatus: models.UeContextTransferStatus_TRANSFERRED,
            }
            // TODO: based on locol policy, decide if need to change serving PCF for UE
            consumer.GetConsumer().RegistrationStatusUpdate(ue, req)
        }
        if len(ue.Pei) == 0 {
            gmm_message.SendIdentityRequest(ue.RanUe[anType], anType, nasMessage.MobileIdentity5GSTypeImei)
            return nil
        }
        // TODO (step 12 optional): the new AMF initiates ME identity check by invoking the
        // N5g-eir_EquipmentIdentityCheck_Get service operation
        if ue.ServingAmfChanged || ue.State[models.AccessType_NON_3_GPP_ACCESS].Is(context.Registered) || !ue.ContextValid {
            communicateWithUDM(ue, anType)
        }
        param := Nnrf_NFDiscovery.SearchNFInstancesParamOpts{
            Supi: optional.NewString(ue.Supi),
        }
        if amfSelf.Locality != "" {
            param.PreferredLocality = optional.NewString(amfSelf.Locality)
        }
        for {
            resp, err := consumer.GetConsumer().SendSearchNFInstances(
                amfSelf.NrfUri, models.NfType_PCF, models.NfType_AMF, &param)
            if err != nil {
                ue.GmmLog.Error("AMF can not select an PCF by NRF")
            } else {
                // select the first PCF, TODO: select base on other info
                var pcfUri string
                for _, nfProfile := range resp.NfInstances {
                    pcfUri = util.SearchNFServiceUri(nfProfile, models.ServiceName_NPCF_AM_POLICY_CONTROL, models.NfServiceStatus_REGISTERED)
                    if pcfUri != "" {
                        ue.PcfId = nfProfile.NfInstanceId
                        break
                    }
                }
                if ue.PcfUri = pcfUri; ue.PcfUri == "" {
                    ue.GmmLog.Error("AMF can not select an PCF by NRF")
                } else {
                    break
                }
            }
        }
        consumer.GetConsumer().AMPolicyControlCreate(ue, anType)
        // Service Area Restriction are applicable only to 3GPP access
        if anType == models.AccessType__3_GPP_ACCESS {
            if ue.AmPolicyAssociation != nil && ue.AmPolicyAssociation.ServAreaRes != nil {
                servAreaRes := ue.AmPolicyAssociation.ServAreaRes
                if servAreaRes.RestrictionType == models.RestrictionType_ALLOWED_AREAS {
                    numOfallowedTAs := 0
                    for _, area := range servAreaRes.Areas {
                        numOfallowedTAs += len(area.Tacs)
                    }
                }
            }
        }
        // TODO (step 18 optional):
        // If the AMF has changed and the old AMF has indicated an existing NGAP UE association towards a N3IWF, the new AMF
        // creates an NGAP UE association towards the N3IWF to which the UE is connectedsend N2 AMF mobility request to N3IWF
        // if anType == models.AccessType_NON_3_GPP_ACCESS && ue.ServingAmfChanged {
        // 	TODO: send N2 AMF Mobility Request
        // }
        amfSelf.AllocateRegistrationArea(ue, anType)
        assignLadnInfo(ue, anType)
        amfSelf.AddAmfUeToUePool(ue, ue.Supi)
        ue.T3502Value = amfSelf.T3502Value
        if anType == models.AccessType__3_GPP_ACCESS {
            ue.T3512Value = amfSelf.T3512Value
        } else {
            ue.Non3gppDeregTimerValue = amfSelf.Non3gppDeregTimerValue
        }
        gmm_message.SendRegistrationAccept(ue, anType, nil, nil, nil, nil, nil)
        return nil
    }
    

ngap

nas的消息都要通过ngap来发送。ngap协议在NF内的实现与pfcpnas-mm等类似

NFs/amf/internal/ngap/          |NFs/smf/internal/pfcp/                 |NFs/amf/internal/gmm/
├── asn1                        |├── dispatcher.go                      |├── common
│   └── 38413-fd0.asn           |├── handler                            |│   ├── timer.go
├── dispatcher_generated.go     |│   ├── handler.go                     |│   └── user_profile.go
├── dispatcher.go               |│   └── handler_test.go                |├── handler.go
├── handler_generated.go        |├── message                            |├── init.go
├── handler.go                  |│   ├── build.go                       |├── init_test.go
├── handler_test.go             |│   ├── send.go                        |├── message
├── message                     |│   └── send_test.go                   |│   ├── build.go
│   ├── build.go                |├── reliable_pfcp_request_test.go      |│   └── send.go
│   ├── forward_ie.go           |└── udp                                |└── sm.go
│   ├── send.go                 |    ├── udp.go                         |                         
│   └── send_test.go.test       |    └── udp_test.go                    |                         
├── ngap_generate.go            |                                       |                         
├── ngap_generator.go           |                                       |                         
├── service                     |                                       |                         
│   └── service.go              |                                       |                         
└── testing                     |                                       |                         
    └── conn_stub.go            |                                       |                         
                                |                                       | 

都有message模块、dispatcher模块、和handler模块,其中message/build.go负责构建协议规定的消息类型,message/send.go负责把消息发送出去,而dispatcher模块负责接收消息(nas-mm的dispatcher在internal/nas/dispatcher.go),并分发给相应的hanlder处理。

nas的消息的发送调用ngap的函数来实现,具体来说是SendDownlinkNasTransportSendN2MessageSendInitialContextSetupRequest等函数。但ngap不只服务于nas,还可以被多个上层模块调用进行RAN与核心网之间的通信。比如当amPolicy发生变更时,AMF可以通知RAN这些变更

// https://github.com/free5gc/amf/blob/v1.2.5/internal/sbi/processor/callback.go#L107
func (p *Processor) AmPolicyControlUpdateNotifyUpdateProcedure(polAssoID string,
	policyUpdate models.PolicyUpdate,
) *models.ProblemDetails {
    // 。。。
    pkg, err := ngap_message.BuildPaging(ue, nil, false)
    ngap_message.SendPaging(ue, pkg)
	// 。。。
}

上面举的例子,都是ngap向UE/RAN发送消息的方向,然而ngap协议也需要负责接收来自UE/RAN的消息,再分发给hanlder或上层各个模块。因此,AMF启动时,除了启动一个sbi服务器外,还会启动一个ngap服务器,用以接收来自UE/RAN的消息(其实是只接收来自RAN的消息,而来自UE的消息都需要经由RAN转发给AMF)。

// https://github.com/free5gc/amf/blob/v1.2.5/pkg/service/init.go
import (
	"github.com/free5gc/amf/internal/ngap"
	ngap_service "github.com/free5gc/amf/internal/ngap/service"
)

func (a *AmfApp) Start() {
	// ......
	ngapHandler := ngap_service.NGAPHandler{
		HandleMessage:         ngap.Dispatch,
		HandleNotification:    ngap.HandleSCTPNotification,
		HandleConnectionError: ngap.HandleSCTPConnError,
	}
	sctpConfig := ngap_service.NewSctpConfig(factory.AmfConfig.GetSctpConfig())
	ngap_service.Run(a.Context().NgapIpList, a.Context().NgapPort, ngapHandler, sctpConfig)
	// ......
}

ngap服务器跑起来以后,会监听指定端口,建立来自特定ip的网络连接:

/* https://github.com/free5gc/amf/blob/v1.2.5/internal/ngap/service/service.go */
func Run(addresses []string, port int, handler NGAPHandler, sctpConfig *sctp.SocketConfig) {
	ips := []net.IPAddr{}
	for _, addr := range addresses {
		netAddr := net.ResolveIPAddr("ip", addr)
		ips = append(ips, *netAddr)
	}
	addr := &sctp.SCTPAddr{IPAddrs: ips, Port: port,}
	go listenAndServe(addr, handler, sctpConfig)
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/ngap/service/service.go#L70
func listenAndServe(addr *sctp.SCTPAddr, handler NGAPHandler, sctpConfig *sctp.SocketConfig) {
	for {
		newConn, err := sctpListener.AcceptSCTP(notimeout)
		// ...
		go handleConnection(newConn, readBufSize, handler)
	}
}

// https://github.com/free5gc/amf/blob/v1.2.5/internal/ngap/service/service.go#L182
func handleConnection(conn *sctp.SCTPConn, bufsize uint32, handler NGAPHandler) {
	for {
		buf := make([]byte, bufsize)
		n, info, notification, err := conn.SCTPRead(buf)
		// ......
		handler.HandleMessage(conn, buf[:n])
		}
	}
}

从网络连接处读取到的来自RAN的消息会被传给handler.HandleMessage函数处理,而AmfApp.Start告诉我们这个实际的函数是ngap.Dispatch,亦即分发给不同模块handler处理。以我们最关心的初始注册过程为例,继续追踪消息的分发流程,并看看来自UE/RAN的注册请求消息如何触发gmm的状态机。

posted @ 2024-12-09 15:42  zrq96  阅读(27)  评论(0编辑  收藏  举报