Free5GC源码研究(11) - SMF研究(下)

前文已经研究过SMF的概念和Nsmf_PDUSession的创建和释放,本文继续研究其更新,以及SMF的其他服务。

SMF实现

Nsmf_PDUSession

SMContext的更新是SMF中的一个核心功能,负责处理PDU会话的各种状态更新和转换。它需要处理多种不同类型的更新请求,并确保会话状态的正确转换。理解与之相关的HandlePDUSessionSMContextUpdate函数也需要一些前置知识,比如AMF与SMF之间的N1接口和N2接口,各种状态机的状态转移等。

N1、N2接口

回顾5G系统的架构设计,可以发现用户设备UE与核心网控制面的的连接只有一个,那就是N1接口,而接入网RAN与核心网控制面的连接也只有一个,那就是N2,且N1与N2接口在控制面的一端都是AMF。也就是说,UE与RAN任何与控制面的交流都智能通过AMF,如果希望与SMF或其他NF交互,就只能委托AMF将消息转发出去,而其他NF的回复也需要靠AMF中继。

UE会通过N1接口把对PDU Session的管理请求发给SMF,RAN也会通过N2接口把对PDU session资源的管理、无线资源的管理、移动性管理、UE上下文管理等命令发送给SMF。

为什么不直接让UE和RAN与SMF直接通信,非要让AMF中继?我认为这是出于两个考虑

  1. 安全性:AMF负责用户认证,通过AMF中继可以确保所有消息都经过安全检查,防止未经授权的访问和攻击
  2. 移动性:当UE移动时,可能需要切换到不同的SMF,当然也需要切换不同AMF,但AMF本身就负责追踪UE的位置和移动性管理。让AMF代为转发消息就可以利用AMF本身的移动性管理功能,而无需让SMF和其他NF也一同追踪UE的位置,避免不必要的夫复杂性。

PDU Session状态机

根据TS24.501,一个PDU Session的全生命周期可以有四个状态:InactiveActiveInactivePending、和ModificationPending,他们之间相互转换的关系和过程由下面的状态机图表示:

img

在free5gc中对于PDU Session的状态定义与标准文档有所不同

// https://github.com/free5gc/smf/blob/v1.2.5/internal/context/sm_context.go#L69
type SMContextState uint32
const (
	InActive SMContextState = iota
	ActivePending
	Active
	InActivePending
	ModificationPending
	PFCPModification
)

ActivePending不是在TS24.501中定义的标准PDU Session状态之一。在UE侧倒是有这么一个状态,但由于PDU Session的建立虽由UE发起,却由核心网主导,所以网络端不需要ActivePending这个状态。我认为这是free5gc实现中的一个内部状态,用来方便程序编写。

同理,PFCPModification这个状态也不是标准状态,而是free5gc实现中的一个内部状态,用于处理PFCP Session修改的中间态。在free5gc的实现中,当SMF需要修改UPF中的PFCP Session(例如更新PDR、FAR规则)时,会将PDU Session状态设置为``PFCPModification`。这个状态表明SMF正在等待UPF对PFCP Session Modification请求的响应。根据UPF的响应结果,会话状态会转移到:

  • 如果修改成功(SessionUpdateSuccess)-> Active
  • 如果修改失败(SessionUpdateFailed)-> Active(同时返回错误)
  • 如果是释放操作且成功(SessionReleaseSuccess)-> InActivePending
  • 如果是释放操作但失败(SessionReleaseFailed)-> Active(同时返回错误)
    这个状态的引入主要是为了处理PFCP协议交互的异步特性。虽然不是标准定义的状态,但它帮助SMF更好地管理与UPF之间的PFCP会话修改过程。

Handover状态机

Handover指的是UE因为位置移动触发的接入网的切换,以及PDU Session从一个RAN移交(Handover)到另一个RAN过程中的状态变化

  • NONE:初始状态,表示PDU会话当前没有进行任何切换操作。每个新建立的PDU会话都处于这个状态。
  • PREPARING: 旧的RAN发现需要进行切换,SMF准备建立到目标RAN的资源
  • PREPARED: 目标RAN的资源已经准备好,SMF处理切换请求确认信息,如果配置了间接转发(Indirect Forwarding),会建立数据转发通道
  • COMPLETED: UE已经成功切换到目标接入网,清理原有路径,如果有间接转发通道,这时也会被释放
  • CANCELLED: 切换由于各种原因被取消,此时PDU session回退到原来的RAN继续服务,清理为切换预留的资源

我没有在标准文档中找到一张描述Handover的状态机图示,所以自己画了一个:

stateDiagram-v2 direction LR [*] --> NONE NONE --> PREPARING: 开始切换准备 PREPARING --> PREPARED: 准备完成 PREPARED --> COMPLETED: 切换成功 PREPARING --> CANCELLED: 切换取消 PREPARED --> CANCELLED: 切换取消 COMPLETED --> [*] CANCELLED --> [*]

UP Connection 状态机

SMF在管理PDU Session的同时,也还要维护相应的与用户面的数据连接。UpCnxState(User Plane Connection State)就是用于表示用户面连接状态的类型

// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_up_cnx_state.go
type UpCnxState string
const (
	UpCnxState_ACTIVATED   UpCnxState = "ACTIVATED"
	UpCnxState_DEACTIVATED UpCnxState = "DEACTIVATED"
	UpCnxState_ACTIVATING  UpCnxState = "ACTIVATING"
)

可以看到这只是一个仅包含3个状态的简单模型,不过新版的TS29.502@v18.08引入一个新的状态SUSPENDED

img

所以最新的UP连接状态机状态转移图变成了这个样子(忽略掉SISPENDED相关的路径,就是代码中实现的状态转移逻辑):

stateDiagram-v2 direction LR [*] --> DEACTIVATED DEACTIVATED --> ACTIVATING: 开始激活 ACTIVATING --> ACTIVATED: 激活成功 ACTIVATED --> DEACTIVATED: 停用 ACTIVATED --> SUSPENDED: 挂起 SUSPENDED --> ACTIVATING: 恢复

掌握以上信息,基本就能理解下面(简化版)的HandlePDUSessionSMContextUpdate源代码了

// https://github.com/free5gc/smf/blob/v1.2.5/internal/sbi/processor/pdu_session.go#L245
func (p *Processor) HandlePDUSessionSMContextUpdate(
	c *gin.Context,
	body models.UpdateSmContextRequest,
	smContextRef string,
) {
	smContext := smf_context.GetSMContextByRef(smContextRef)
	var sendPFCPModification bool
	var pfcpResponseStatus smf_context.PFCPSessionResponseStatus
	var response models.UpdateSmContextResponse
	response.JsonData = new(models.SmContextUpdatedData)
	smContextUpdateData := body.JsonData

	// 处理N1接口的消息(AMF转发)
	if body.BinaryDataN1SmMessage != nil {
		m := nas.NewMessage()
		switch m.GsmHeader.GetMessageType() {
		case nas.MsgTypePDUSessionReleaseRequest:
			// release PDU Session
			smContext.SetState(smf_context.PFCPModification)
			pfcpResponseStatus = releaseSession(smContext)
		case nas.MsgTypePDUSessionReleaseComplete:
			// PDU Session released, wrap up things
			smContext.SetState(smf_context.InActive)
			response.JsonData.UpCnxState = models.UpCnxState_DEACTIVATED
			smContext.StopT3592()
			// If CN tunnel resource is released, should
			if smContext.Tunnel.ANInformation.IPAddress == nil {
				p.RemoveSMContextFromAllNF(smContext, true)
			}
		case nas.MsgTypePDUSessionModificationRequest:
			// modify PDU session
			p.HandlePDUSessionModificationRequest(smContext, m.PDUSessionModificationRequest)
			p.sendGSMPDUSessionModificationCommand(smContext, buf)
			smf_context.BuildPDUSessionResourceModifyRequestTransfer(smContext)
			c.Render(http.StatusOK, openapi.MultipartRelatedRender{Data: response})
			return
		case nas.MsgTypePDUSessionModificationComplete:
			smContext.StopT3591()
		case nas.MsgTypePDUSessionModificationReject:
			smContext.StopT3591()
		}
	}

	/* ================================================================ */

	// 变更与用户面连接的状态(UE与UPF之间的数据传输通道)
	switch smContextUpdateData.UpCnxState {
	case models.UpCnxState_ACTIVATING:
		smContext.SetState(smf_context.ModificationPending)
		n2Buf, err = smf_context.BuildPDUSessionResourceSetupRequestTransfer(smContext)
		smContext.UpCnxState = models.UpCnxState_ACTIVATING
	case models.UpCnxState_DEACTIVATED:
		smContext.SetState(smf_context.ModificationPending)
		smContext.UpCnxState = body.JsonData.UpCnxState
		// Set FAR and AN, N3 Release Info
		// 修改FAR规则,停止转发,开启缓存
		farList = []*smf_context.FAR{}
		for _, dataPath := range smContext.Tunnel.DataPathPool {
			ANUPF := dataPath.FirstDPNode
			DLPDR := ANUPF.DownLinkTunnel.PDR
			if DLPDR == nil {
				smContext.Log.Warnf("Access network resource is released")
			} else {
				DLPDR.FAR.State = smf_context.RULE_UPDATE
				DLPDR.FAR.ApplyAction.Forw = false
				DLPDR.FAR.ApplyAction.Buff = true
				DLPDR.FAR.ApplyAction.Nocp = true
				farList = append(farList, DLPDR.FAR)
				sendPFCPModification = true
				smContext.SetState(smf_context.PFCPModification)
			}
		}
	}

	/* ================================================================ */

	// 处理N2接口的消息(AMF转发)
	switch smContextUpdateData.N2SmInfoType {
	// setup PDU session resource
	case models.N2SmInfoType_PDU_RES_SETUP_RSP:
		smContext.SetState(smf_context.ModificationPending)
		pdrList = []*smf_context.PDR{}
		farList = []*smf_context.FAR{}

		for _, dataPath := range tunnel.DataPathPool {
			if dataPath.Activated {
				ANUPF := dataPath.FirstDPNode
				DLPDR := ANUPF.DownLinkTunnel.PDR
				DLPDR.FAR.ApplyAction = pfcpType.ApplyAction{
					Buff: false,
					Drop: false,
					Dupl: false,
					Forw: true,
					Nocp: false,
				}
				DLPDR.FAR.ForwardingParameters = &smf_context.ForwardingParameters{
					DestinationInterface: pfcpType.DestinationInterface{
						InterfaceValue: pfcpType.DestinationInterfaceAccess,
					},
					NetworkInstance: &pfcpType.NetworkInstance{
						NetworkInstance: smContext.Dnn,
						FQDNEncoding:    factory.SmfConfig.Configuration.NwInstFqdnEncoding,
					},
				}
				DLPDR.State = smf_context.RULE_UPDATE
				DLPDR.FAR.State = smf_context.RULE_UPDATE
				pdrList = append(pdrList, DLPDR)
				farList = append(farList, DLPDR.FAR)
			}
		}
		smf_context.HandlePDUSessionResourceSetupResponseTransfer(
			body.BinaryDataN2SmInformation, smContext)
		sendPFCPModification = true
		smContext.SetState(smf_context.PFCPModification)
		
	case models.N2SmInfoType_PDU_RES_SETUP_FAIL:
		smf_context.HandlePDUSessionResourceSetupUnsuccessfulTransfer(
			body.BinaryDataN2SmInformation, smContext)
	case models.N2SmInfoType_PDU_RES_MOD_RSP:
		smf_context.HandlePDUSessionResourceModifyResponseTransfer(
			body.BinaryDataN2SmInformation, smContext)

	// release PDU session resource
	case models.N2SmInfoType_PDU_RES_REL_RSP:
		// remove an tunnel info
		smContext.Tunnel.ANInformation = struct {
			IPAddress net.IP
			TEID      uint32
		}{nil, 0}
		p.RemoveSMContextFromAllNF(smContext, true)

	case models.N2SmInfoType_PATH_SWITCH_REQ:
		smf_context.HandlePathSwitchRequestTransfer(
			body.BinaryDataN2SmInformation, smContext)
		smf_context.BuildPathSwitchRequestAcknowledgeTransfer(smContext)
		for _, dataPath := range tunnel.DataPathPool {
			if dataPath.Activated {
				ANUPF := dataPath.FirstDPNode
				DLPDR := ANUPF.DownLinkTunnel.PDR
				pdrList = append(pdrList, DLPDR)
				farList = append(farList, DLPDR.FAR)
			}
		}
		smContext.SetState(smf_context.PFCPModification)

	case models.N2SmInfoType_PATH_SWITCH_SETUP_FAIL:
		smContext.SetState(smf_context.ModificationPending)
		smf_context.HandlePathSwitchRequestSetupFailedTransfer(
			body.BinaryDataN2SmInformation, smContext)
		
	case models.N2SmInfoType_HANDOVER_REQUIRED:
		smContext.SetState(smf_context.ModificationPending)
		response.JsonData.N2SmInfo = &models.RefToBinaryData{ContentId: "Handover"}
	}

	/* ================================================================ */

	// 处理HoState(Handover State,指UE在不同基站/接入网之间迁移过程中的状态)
	switch smContextUpdateData.HoState {
	case models.HoState_PREPARING:
		smContext.SetState(smf_context.ModificationPending)
		smContext.HoState = models.HoState_PREPARING
		smf_context.HandleHandoverRequiredTransfer(
			body.BinaryDataN2SmInformation, smContext)
		smf_context.BuildPDUSessionResourceSetupRequestTransfer(smContext)
		response.JsonData.HoState = models.HoState_PREPARING
	case models.HoState_PREPARED:
		smContext.SetState(smf_context.ModificationPending)
		smContext.HoState = models.HoState_PREPARED
		response.JsonData.HoState = models.HoState_PREPARED
		smf_context.HandleHandoverRequestAcknowledgeTransfer(
			body.BinaryDataN2SmInformation, smContext)
		
		// request UPF establish indirect forwarding path for DL
		if smContext.DLForwardingType == smf_context.IndirectForwarding {
			ANUPF := smContext.IndirectForwardingTunnel.FirstDPNode
			IndirectForwardingPDR := smContext.IndirectForwardingTunnel.FirstDPNode.UpLinkTunnel.PDR

			pdrList = append(pdrList, IndirectForwardingPDR)
			farList = append(farList, IndirectForwardingPDR.FAR)

			// release indirect forwading path
			if err = ANUPF.UPF.RemovePDR(IndirectForwardingPDR); err != nil {
				logger.PduSessLog.Errorln("release indirect path: ", err)
			}

			sendPFCPModification = true
			smContext.SetState(smf_context.PFCPModification)
		}
		smf_context.BuildHandoverCommandTransfer(smContext)
		response.JsonData.HoState = models.HoState_PREPARING
	case models.HoState_COMPLETED:
		for _, dataPath := range tunnel.DataPathPool {
			if dataPath.Activated {
				ANUPF := dataPath.FirstDPNode
				DLPDR := ANUPF.DownLinkTunnel.PDR
				pdrList = append(pdrList, DLPDR)
				farList = append(farList, DLPDR.FAR)
			}
		}
		// remove indirect forwarding path
		if smContext.DLForwardingType == smf_context.IndirectForwarding {
			indirectForwardingPDR := smContext.IndirectForwardingTunnel.FirstDPNode.GetUpLinkPDR()
			indirectForwardingPDR.State = smf_context.RULE_REMOVE
			indirectForwardingPDR.FAR.State = smf_context.RULE_REMOVE
			pdrList = append(pdrList, indirectForwardingPDR)
			farList = append(farList, indirectForwardingPDR.FAR)
		}
		smContext.SetState(smf_context.PFCPModification)
		smContext.HoState = models.HoState_COMPLETED
		response.JsonData.HoState = models.HoState_COMPLETED
	}

	/* ================================================================ */

	// 最后,根据PDU Session状态机的转移采取行动
	switch smContext.State() {
	case smf_context.PFCPModification:
		pfcpResponseStatus = p.updateAnUpfPfcpSession(
			smContext, pdrList, farList, barList, qerList, urrList)

		// 处理PFCP更新的结果
		switch pfcpResponseStatus {
		case smf_context.SessionUpdateSuccess:
			smContext.SetState(smf_context.Active)
			c.Render(http.StatusOK, openapi.MultipartRelatedRender{Data: response})
		case smf_context.SessionUpdateFailed:
			smContext.SetState(smf_context.Active)
			updateSmContextError := models.UpdateSmContextErrorResponse{
				JsonData: &models.SmContextUpdateError{
					Error: &Nsmf_PDUSession.N1SmError,
				},
			} // Depends on the reason why N4 fail
			c.JSON(http.StatusForbidden, updateSmContextError)

		case smf_context.SessionReleaseSuccess:
			p.ReleaseChargingSession(smContext)
			smContext.SetState(smf_context.InActivePending)
			c.Render(http.StatusOK, openapi.MultipartRelatedRender{Data: response})

		case smf_context.SessionReleaseFailed:
			// Update SmContext Request(N1 PDU Session Release Request)
			// Send PDU Session Release Reject
			smContext.SetState(smf_context.Active)
			// problemDetail := models.ProblemDetails{
				// 。。。。。。
			c.JSON(int(problemDetail.Status), errResponse)
		}
		smContext.PostRemoveDataPath()

	case smf_context.ModificationPending:
		smContext.SetState(smf_context.Active)
		c.Render(http.StatusOK, openapi.MultipartRelatedRender{Data: response})
	case smf_context.InActive, smf_context.InActivePending:
		c.Render(http.StatusOK, openapi.MultipartRelatedRender{Data: response})
	default:
		c.Render(http.StatusOK, openapi.MultipartRelatedRender{Data: response})
	}
}

至此,对SMF的研究暂告一段落。目前为止展现出来的SMF的内容仅仅是冰山一角(当然是最重要的一角),更多具体细节隐藏在了Nsmf_PDUSession三个基本操作的深层调用里面,比如怎样构建UE到UPF的数据链路,怎样通过PFCP配置UPF的各种转发规则,怎样配置QoS参数等。smf/internal/context/中各种眼花缭乱的数据类型更是一个都没有拎出来展示。然而细节总是研究不完的,更多的内容,还是留待以后研究吧。

posted @ 2024-11-29 12:59  zrq96  阅读(1)  评论(0编辑  收藏  举报