Free5GC原始碼研究(11) - SMF研究(下)

zrq96發表於2024-11-29

前文已經研究過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中繼。

Free5GC原始碼研究(11) - SMF研究(下)

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

相關文章