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/nas和free5gc/ngap分别是对这两个协议的具体实现,而amf/internal/
中的两个包则是AMF对这两个协议的调用。gmm、nas、ngap三者的关系可以用下面这张图来概括:
简单来说,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中使用了一个状态机来实现。下面的有向图是这个状态机的状态转移图,其中节点代表状态,而边则代表状态间的转移和触发状态转移的事件
状态图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
这个状态机乍看起来有点复杂,但其实可以看成一条从Deregistered
到Registered
的路径,其他所有路径都是出现错误而回退到路径前面的某个状态,以及从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的消息为例
构建消息的函数遵循类似的模板:先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***
函数可以很简单,仅仅是打印一些日志;也可以很复杂,调用AMFContext
、amfUe
、各种内部函数,发送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, ¶m) 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内的实现与pfcp、nas-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的函数来实现,具体来说是SendDownlinkNasTransport
、SendN2Message
、SendInitialContextSetupRequest
等函数。但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的状态机。